diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..d768668 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@matrix-org:registry=https://gitlab.matrix.org/api/v4/projects/27/packages/npm/ diff --git a/package-lock.json b/package-lock.json index c2fe675..a235c57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "keanuapp-weblite", - "version": "0.1.16", + "version": "0.1.19", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1248,6 +1248,11 @@ } } }, + "@matrix-org/olm": { + "version": "3.2.8", + "resolved": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz", + "integrity": "sha1-jVNjbQReF3biouxmE+VzMN2c6FY=" + }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -1836,6 +1841,17 @@ "color-convert": "^2.0.1" } }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, "cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -1868,6 +1884,13 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1903,6 +1926,28 @@ "ansi-regex": "^5.0.0" } }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "optional": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "vue-loader-v16": { + "version": "npm:vue-loader@16.8.3", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz", + "integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==", + "dev": true, + "optional": true, + "requires": { + "chalk": "^4.1.0", + "hash-sum": "^2.0.0", + "loader-utils": "^2.0.0" + } + }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -2731,9 +2776,9 @@ } }, "base-x": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz", - "integrity": "sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", "requires": { "safe-buffer": "^5.0.1" } @@ -7971,9 +8016,9 @@ "integrity": "sha512-wRJtOo1v1ch+gN8PRsj0IGJznk+kQ8mz13ds/nuhLI+Qyf/931ZlRpd92oq0IRPpZIb+bhX8pRjzIVdcPDKmiQ==" }, "matrix-js-sdk": { - "version": "12.4.1", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-12.4.1.tgz", - "integrity": "sha512-C9aSGX9e4GoCm0Rli+iGBXmcnRxnwETw7MvgNcSBfPaLHOMZi/wz4YOV7HEZK8R+OXuDrDYyglncWSJkkoDpAQ==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-15.2.0.tgz", + "integrity": "sha512-jZOM8Fn86oNvU3zVQcc+JTKKrtYq4ADN6rPZs4Mwxj/X/GDP+2YIP5176GtviF0GM6VO1dcnPZY73ykl8DayjA==", "requires": { "@babel/runtime": "^7.12.5", "another-json": "^0.2.0", @@ -8706,10 +8751,6 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, - "olm": { - "version": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", - "integrity": "sha512-B87bTpGIGieuV2FNauChjjQtVltwTGagQFoHm+3Dcse4amKAAGJB/I54dnP/JtbHZ+RYVoApM2OQ46Z4VH6eNg==" - }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -10760,9 +10801,9 @@ } }, "qs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", - "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.2.tgz", + "integrity": "sha512-mSIdjzqznWgfd4pMii7sHtaYF8rx8861hBO80SraY5GT0XQibWZWJSid0avzHGkDIZLImux2S5mXO0Hfct2QCw==", "requires": { "side-channel": "^1.0.4" } @@ -13230,75 +13271,6 @@ } } }, - "vue-loader-v16": { - "version": "npm:vue-loader@16.5.0", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.5.0.tgz", - "integrity": "sha512-WXh+7AgFxGTgb5QAkQtFeUcHNIEq3PGVQ8WskY5ZiFbWBkOwcCPRs4w/2tVyTbh2q6TVRlO3xfvIukUtjsu62A==", - "dev": true, - "optional": true, - "requires": { - "chalk": "^4.1.0", - "hash-sum": "^2.0.0", - "loader-utils": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "optional": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "optional": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "optional": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "optional": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "optional": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "optional": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "vue-property-decorator": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/vue-property-decorator/-/vue-property-decorator-9.1.2.tgz", diff --git a/package.json b/package.json index b9eda89..4d742e7 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "create-sticker-config": "node ./create_sticker_config.js $1" }, "dependencies": { + "@matrix-org/olm": "^3.2.8", "aes-js": "^3.1.2", "axios": "^0.21.0", "clean-insights-sdk": "^2.4", @@ -23,9 +24,8 @@ "json-web-key": "^0.4.0", "linkifyjs": "3.0.0-beta.3", "material-design-icons-iconfont": "^6.1", - "matrix-js-sdk": "^12.4", + "matrix-js-sdk": "^15.2.0", "md-gum-polyfill": "^1.0.0", - "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "pretty-bytes": "^5.6.0", "qrcode": "^1.4.4", "raw-loader": "^4.0.2", @@ -86,4 +86,4 @@ "last 2 versions", "not dead" ] -} \ No newline at end of file +} diff --git a/public/js/olm.wasm b/public/js/olm.wasm deleted file mode 100644 index 97cce63..0000000 Binary files a/public/js/olm.wasm and /dev/null differ diff --git a/src/services/matrix.service.js b/src/services/matrix.service.js index be52c1c..c1db9c1 100644 --- a/src/services/matrix.service.js +++ b/src/services/matrix.service.js @@ -1,850 +1,966 @@ -global.Olm = require("olm"); +import olm from "@matrix-org/olm/olm"; +global.Olm = olm; import sdk from "matrix-js-sdk"; import { TimelineWindow, EventTimeline } from "matrix-js-sdk"; import util from "../plugins/utils"; import User from "../models/user"; const LocalStorageCryptoStore = require("matrix-js-sdk/lib/crypto/store/localStorage-crypto-store") - .LocalStorageCryptoStore; + .LocalStorageCryptoStore; export default { - install(Vue, options) { - if (!options || !options.store) { - throw new Error('Please initialise plugin with a Vuex store.') - } - - // Set User-Agent headers. - // Update: browser do not allow this, "Refused to set unsafe header "User-Agent"" - // Keep this code around however, since it's an example of how to add headers to a request... - // sdk.wrapRequest((orig, opts, callback) => { - // opts.headers = opts.headers || {} - // opts.headers['User-Agent'] = "Keanu"; - // var ret = orig(opts, callback); - // return ret; - // }); - const store = options.store; - const i18n = options.i18n; - - const matrixService = new Vue({ - store, - i18n, - data() { - return { - matrixClient: null, - matrixClientReady: false, - rooms: [], - userDisplayName: null, - userAvatar: null, - currentRoom: null, - notificationCount: 0 - } - }, - mounted() { - console.log("Matrix service mounted"); - }, - - computed: { - ready() { - return this.matrixClient != null && this.matrixClientReady; - }, - - currentUser() { - return this.$store.state.auth.user; - }, - - currentUserId() { - const user = this.currentUser || {} - return user.user_id; - }, - - currentUserDisplayName() { - if (this.ready) { - const user = this.matrixClient.getUser(this.currentUserId) || {} - return this.userDisplayName || user.displayName; - } - return null; - }, - - currentUserHomeServer() { - return User.serverName(this.currentUserId); - }, - - currentRoomId() { - return this.$store.state.currentRoomId; - }, - - joinedRooms() { - return this.rooms.filter(room => { - return room.selfMembership === 'join' - }); - }, - - invites() { - return this.rooms.filter(room => { - return room.selfMembership === 'invite' - }); - } - }, - - watch: { - currentRoomId: { - immediate: true, - handler(roomId) { - this.currentRoom = this.getRoom(roomId); - } - }, - }, - - methods: { - createCryptoStore() { - console.log("create crypto store"); - return new LocalStorageCryptoStore(this.$store.getters.storage); - }, - login(user) { - const tempMatrixClient = sdk.createClient(User.homeServerUrl(user.home_server)); - var promiseLogin; - - const self = this; - if (user.access_token) { - // Logged in on "real" account - promiseLogin = Promise.resolve(user); - } else if (user.is_guest && !user.user_id) { - // Generate random username and password. We don't user REAL matrix - // guest accounts because 1. They are not allowed to post media, 2. They - // can not use avatars and 3. They can not seamlessly be upgraded to real accounts. - // - // Instead, we use an ILAG approach, Improved Landing as Guest. - const user = util.randomUser(); - const pass = util.randomPass(); - promiseLogin = tempMatrixClient - .register(user, pass, null, { - type: "m.login.dummy", - initial_device_display_name: this.$config.appName - }) - .then((response) => { - console.log("Response", response); - var u = Object.assign({}, response); - u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response. - u.password = pass; - u.is_guest = true; - this.$store.commit("setUser", u); - return u; - }) - } else { - var data = { user: User.localPart(user.user_id), password: user.password, type: "m.login.password", initial_device_display_name: this.$config.appName }; - if (user.device_id) { - data.device_id = user.device_id; - } - promiseLogin = tempMatrixClient - .login("m.login.password", data) - .then((response) => { - var u = Object.assign({}, response); - if (user.is_guest) { - // Copy over needed properties - u = Object.assign(user, response); - } - u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response. - this.$store.commit("setUser", u); - return u; - }) - } - - return promiseLogin - .then((user) => { - return self.getMatrixClient(user); - }); - }, - - clearCryptoStore() { - // Clear crypto related data - // TODO - for some reason "clearStores" called in "logout" only clears the "account" crypto - // data item, not all sessions etc. Why? We need to do that manually here! - const toRemove = []; - const storage = this.$store.getters.storage; - for (let i = 0; i < storage.length; ++i) { - const key = storage.key(i); - if (key.startsWith("crypto.")) toRemove.push(key); - } - for (const key of toRemove) { - storage.removeItem(key); - } - }, - - logout() { - if (this.matrixClient) { - this.removeMatrixClientListeners(this.matrixClient); - this.matrixClient.stopClient(); - this.matrixClient.clearStores().then(() => { - this.clearCryptoStore() - }) - this.matrixClient = null; - this.matrixClientReady = false; - } else { - this.clearCryptoStore(); - } - - - this.$store.commit("setUser", null); - this.$store.commit("setCurrentRoomId", null); - this.rooms = []; - this.userDisplayName = null; - this.userAvatar = null; - this.currentRoom = null; - this.notificationCount = 0; - }, - - initClient() { - this.reloadRooms(); - this.matrixClientReady = true; - this.matrixClient.emit('Matrix.initialized', this.matrixClient); - this.matrixClient.getProfileInfo(this.currentUserId) - .then(info => { - console.log("Got user profile: " + JSON.stringify(info)); - this.userDisplayName = info.displayname; - this.userAvatar = info.avatar_url; - }) - .catch(err => { - console.log("Failed to get user profile: ", err); - }) - }, - - getMatrixClient(user) { - if (user === undefined) { - user = this.$store.state.auth.user; - } - if (this.matrixClientReady) { - return new Promise((resolve, ignoredreject) => { - resolve(user); - }) - } else if (this.matrixClient) { - return new Promise((resolve, ignoredreject) => { - this.matrixClient.once('Matrix.initialized', (ignoredclient) => { - resolve(user); - }); - }) - } - - const matrixStore = new sdk.MemoryStore(this.$store.getters.storage); - const webStorageSessionStore = new sdk.WebStorageSessionStore( - this.$store.getters.storage - ); - - var homeServer = user.home_server; - if (!homeServer.startsWith("https://")) { - homeServer = "https://" + homeServer; - } - - var opts = { - baseUrl: homeServer, - userId: user.user_id, - store: matrixStore, - sessionStore: webStorageSessionStore, - deviceId: user.device_id, - accessToken: user.access_token, - timelineSupport: true, - unstableClientRelationAggregation: true, - //useAuthorizationHeader: true - } - this.matrixClient = sdk.createClient(opts); - // if (user.is_guest) { - // this.matrixClient.setGuest(true); - // } - return this.matrixClient - .initCrypto() - .then(() => { - console.log("Crypto initialized"); - - this.addMatrixClientListeners(this.matrixClient); - - this.matrixClient.startClient(); - return this.matrixClient; - }) - .then(matrixClient => { - if (matrixClient.isInitialSyncComplete()) { - console.log("Initial sync done already!"); - return matrixClient; - } else { - return new Promise((resolve, reject) => { - matrixClient.once( - "sync", - function (state, ignoredprevState, ignoredres) { - console.log(state); // state will be 'PREPARED' when the client is ready to use - if (state == "PREPARED") { - resolve(matrixClient); - } else if (state == "ERROR") { - reject("Error syncing"); - } - } - ) - }); - } - }) - .then(() => { - // Ready to use! Start by loading rooms. - this.initClient(); - return user; - }) - }, - - /** - * Returns a promise that will log us into the Matrix. - * - * Will use a real account, if we have one, otherwise will create - * a random account. - */ - getLoginPromise() { - if (this.ready) { - return Promise.resolve(this.currentUser); - } - return this.$store.dispatch("login", this.currentUser || new User(this.$config.defaultServer, "", "", true)); - }, - - addMatrixClientListeners(client) { - if (client) { - client.on("event", this.onEvent); - client.on("Room", this.onRoom); - client.on("Session.logged_out", this.onSessionLoggedOut); - client.on("Room.myMembership", this.onRoomMyMembership); - } - }, - - removeMatrixClientListeners(client) { - if (client) { - client.off("event", this.onEvent); - client.off("Room", this.onRoom); - client.off("Session.logged_out", this.onSessionLoggedOut); - client.off("Room.myMembership", this.onRoomMyMembership); - } - }, - - onEvent(event) { - switch (event.getType()) { - case "m.room.topic": { - const room = this.matrixClient.getRoom(event.getRoomId()); - if (room) { - Vue.set(room, "topic", event.getContent().topic); - } - } - break; - - case "m.room.avatar": { - const room = this.matrixClient.getRoom(event.getRoomId()); - if (room) { - Vue.set(room, "avatar", room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true)); - } - } - break; - - case "m.room.member": { - if (this.currentRoom && event.getRoomId() == this.currentRoom.roomId) { // Don't use this.currentRoomId, may be an alias. We need the real id! - if (event.getContent().membership == "leave" && (event.getPrevContent() || {}).membership == "join" && event.getStateKey() == this.currentUserId && event.getSender() != this.currentUserId) { - // We were kicked - const wasPurged = (event.getContent().reason == "Room Deleted"); - this.$navigation.push({ name: "Goodbye", params: { roomWasPurged: wasPurged } }, -1); - } - } - } - break; - } - this.updateNotificationCount(); - }, - - onRoom(ignoredroom) { - console.log("Got room", ignoredroom); - this.reloadRooms(); - this.updateNotificationCount(); - }, - - onRoomMyMembership(room) { - if (room.selfMembership === "invite") { - // Invitation. Need to call "recalculate" to pick - // up room name, not sure why exactly. - room.recalculate(); - this.reloadRooms(); - } - }, - - onSessionLoggedOut() { - console.log("Logged out!"); - if (this.matrixClient) { - this.removeMatrixClientListeners(this.matrixClient); - this.matrixClient.stopClient(); - this.matrixClient = null; - this.matrixClientReady = false; - } - - // For "real" accounts we totally wipe the user object, but for "guest" - // accounts (i.e. created from random data and with password never changed) - // we need to hang on to the generated password and use that to login to a new - // session, so only wipe the token in s that case. - // Clear the access token - var user = JSON.parse(this.$store.state.auth.user); - if (user.is_guest) { - delete user.access_token; - this.$store.commit("setUser", user); - // Login again - this.login(user); - } else { - this.$store.commit("setUser", null); - this.$store.commit("setCurrentRoomId", null); - this.$navigation.push({ path: "/login" }, -1); - } - }, - - reloadRooms() { - // TODO - do incremental update instead of replacing the whole array - // each time! - var updatedRooms = this.matrixClient.getVisibleRooms(); - updatedRooms = updatedRooms.filter(room => { - return room.selfMembership && (room.selfMembership == "invite" || room.selfMembership == "join"); - }); - updatedRooms.forEach(room => { - if (!room.avatar) { - Vue.set(room, "avatar", room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true)); - } - }); - console.log("Reload rooms", updatedRooms); - Vue.set(this, "rooms", updatedRooms); - const currentRoom = this.getRoom(this.$store.state.currentRoomId); - if (this.currentRoom != currentRoom) { - this.currentRoom = currentRoom; - } - }, - - setCurrentRoomId(roomId) { - this.$store.commit("setCurrentRoomId", roomId); - this.currentRoom = this.getRoom(roomId); - }, - - getRoom(roomId) { - if (!roomId) { - return null; - } - var room = null; - if (this.matrixClient) { - const visibleRooms = this.matrixClient.getRooms(); - room = visibleRooms.find(room => { - if (roomId.startsWith("#")) { - return room.getCanonicalAlias() == roomId; - } - return room.roomId == roomId; - }); - } - return room || null; - }, - - /** - * Return all users we are in a "invite" only room with! - */ - getAllFriends() { - var ids = {}; - const ret = []; - for (const room of this.rooms) { - if (room.selfMembership == 'join' && this.getRoomJoinRule(room) == 'invite') { - for (const member of room.getJoinedMembers()) { - if (member.userId != this.currentUserId && !ids[member.userId]) { - ids[member.userId] = member; - ret.push(member); - } - } - } - } - ret.sort((a, b) => { - const aName = a.user ? a.user.displayName : a.name; - const bName = b.user ? b.user.displayName : b.name; - return aName.localeCompare(bName); - }); - return ret; - }, - - getRoomJoinRule(room) { - if (room) { - const joinRules = room.currentState.getStateEvents( - "m.room.join_rules", - "" - ); - return joinRules && joinRules.getContent().join_rule; - } - return null; - }, - - getRoomHistoryVisibility(room) { - if (room) { - const historyVisibility = room.currentState.getStateEvents( - "m.room.history_visibility", - "" - ); - return historyVisibility && historyVisibility.getContent().history_visibility; - } - return null; - }, - - leaveRoom(roomId) { - return this.matrixClient.leave(roomId, undefined) - .then(() => { - this.rooms = this.rooms.filter(room => { - room.roomId != roomId; - }); - this.matrixClient.store.removeRoom(roomId); - //this.matrixClient.forget(roomId, true, undefined); - }) - }, - - /** - * Purge the room with the given id! This means: - * - Make room invite only - * - Disallow guest access - * - Set history visibility to 'joined' - * - Redact all events - * - Kick all members - * @param roomId - */ - purgeRoom(roomId, statusCallback) { - const oldGlobalErrorSetting = this.matrixClient.getGlobalErrorOnUnknownDevices(); - return new Promise((resolve, reject) => { - const room = this.getRoom(roomId); - if (!room) { - reject("Room not found!"); - return; - } - - const timelineWindow = new TimelineWindow( - this.matrixClient, - room.getUnfilteredTimelineSet(), - {} - ); - const self = this; - - //console.log("Purge: set invite only"); - statusCallback(this.$t('room.purge_set_room_state')); - this.matrixClient.sendStateEvent( - roomId, - "m.room.join_rules", - { join_rule: "invite" }, - "" - ) - .then(() => { - //console.log("Purge: forbid guest access"); - return this.matrixClient.sendStateEvent( - roomId, - "m.room.guest_access", - { guest_access: "forbidden" }, - "" - ); - }) - .then(() => { - //console.log("Purge: set history visibility to 'joined'"); - return this.matrixClient.sendStateEvent(roomId, "m.room.history_visibility", { - history_visibility: "joined", - }); - }) - .then(() => { - //console.log("Purge: create timeline"); - return timelineWindow.load(null, 100) - }) - .then(() => { - const getMoreIfAvailable = function _getMoreIfAvailable() { - if (timelineWindow.canPaginate(EventTimeline.BACKWARDS) - ) { - //console.log("Purge: page back"); - return timelineWindow - .paginate(EventTimeline.BACKWARDS, 100, true, 5) - .then((ignoredsuccess) => { - return _getMoreIfAvailable.call(self); - }); - } else { - return Promise.resolve("Done"); - } - }.bind(self); - return getMoreIfAvailable(); - }) - .then(() => { - //console.log("Purge: redact events"); - statusCallback(this.$t('room.purge_redacting_events')); - // First ignore unknown device errors - this.matrixClient.setGlobalErrorOnUnknownDevices(false); - var redactionPromises = []; - timelineWindow.getEvents().forEach(event => { - if (!event.isRedacted() && !event.isRedaction() && !event.isState()) { - // Redact! - redactionPromises.push(this.matrixClient.redactEvent(event.getRoomId(), event.getId())); - } - }); - return Promise.all(redactionPromises); - }) - .then(() => { - //console.log("Purge: kick members"); - statusCallback(this.$t('room.purge_removing_members')); - var joined = room.getMembersWithMembership("join"); - var invited = room.getMembersWithMembership("invite"); - var members = joined.concat(invited); - - var kickPromises = []; - members.forEach(member => { - if (member.userId != self.currentUserId) { - kickPromises.push(this.matrixClient.kick(roomId, member.userId, "Room Deleted")); - } - }); - return Promise.all(kickPromises); - }) - .then(() => { - statusCallback(null); - this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting); - return this.leaveRoom(roomId); - }) - .then(() => { - resolve(true); // Done! - }) - .catch((err) => { - console.error("Error purging room", err); - this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting); - reject(err); - }); - }) - }, - - /** - * Get a private chat room with the given user. Searches through our rooms to see - * if a suitable room already exists. If not, one is created. - * @param {*} userId The user to chat with. - */ - getOrCreatePrivateChat(userId) { - return new Promise((resolve, reject) => { - for (const room of this.rooms) { - // Is the other member the one we are looking for? - if (this.isDirectRoomWith(room, userId)) { - var member = room.getMember(userId); - if (member && member.membership != "join") { - // Resend invite - this.matrixClient.invite(room.roomId, userId); - } - resolve(room); - return; - } - } - - // No room found, create one - // - const createRoomOptions = { - visibility: "private", // Not listed! - preset: "private_chat", - initial_state: [ - { - type: "m.room.encryption", - state_key: "", - content: { - algorithm: "m.megolm.v1.aes-sha2", - }, - }, - { - type: "m.room.guest_access", - state_key: "", - content: { - guest_access: "forbidden", - }, - }, - { - type: "m.room.history_visibility", - state_key: "", - content: { - history_visibility: "joined", - }, - }, - ], - invite: [userId], - }; - return this.matrixClient - .createRoom(createRoomOptions) - .then(({ room_id, room_alias }) => { - resolve(this.getRoom(room_alias || room_id)); - }) - .catch((error) => { - reject(error); - }); - }) - }, - - /** - * Return true if this room is a direct room with the given user. - * @param { } room - * @param {*} userId - */ - isDirectRoomWith(room, userId) { - if (room.selfMembership == "join" && room.getInvitedAndJoinedMemberCount() == 2) { - // Is the other member the one we are looking for? - if (room.getMembersWithMembership("join").some(item => item.userId == userId)) { - return true; - } else if (room.getMembersWithMembership("invite").some(item => item.userId == userId)) { - return true; - } - } - return false; - }, - - on(event, handler) { - if (this.matrixClient) { - this.matrixClient.on(event, handler); - } - }, - - off(event, handler) { - if (this.matrixClient) { - this.matrixClient.off(event, handler); - } - }, - - setPassword(oldPassword, newPassword) { - if (this.matrixClient && this.currentUser) { - const authDict = { - type: 'm.login.password', - identifier: { - type: 'm.id.user', - user: this.currentUser.user_id, - }, - // TODO: Remove `user` once servers support proper UIA - // See https://github.com/matrix-org/synapse/issues/5665 - user: this.currentUser.user_id, - password: oldPassword, - }; - const self = this; - return this.matrixClient.setPassword(authDict, newPassword) - .then(() => { - // Forget password and remove the 'is_guest' flag, we are now a "real" user! - self.currentUser.password = undefined; - self.currentUser.is_guest = false; - self.$store.commit("setUser", self.currentUser); - }) - .then(() => { - return true; - }) - } - return Promise.resolve(false); - }, - - uploadFile(file, opts) { - return this.matrixClient.uploadContent(file, opts); - }, - - getPublicRoomInfo(roomId) { - if (!roomId) { - return Promise.reject("Invalid parameters"); - } - - const parts = roomId.split(':'); - if (parts.length != 2) { - return Promise.reject("Unknown room server"); - } - const server = parts[1]; - - var clientPromise; - if (this.matrixClient) { - clientPromise = this.getMatrixClient().then(() => { - return this.matrixClient; - }) - } else { - const tempMatrixClient = sdk.createClient(this.$config.defaultServer); - var tempUserString = this.$store.state.tempuser; - var tempUser = null; - if (tempUserString) { - tempUser = JSON.parse(tempUserString); - } - - // Need to create an account? - // - if (tempUser) { - clientPromise = Promise.resolve(tempUser); - } else { - const user = util.randomUser(); - const pass = util.randomPass(); - clientPromise = tempMatrixClient - .register(user, pass, null, { - type: "m.login.dummy", - initial_device_display_name: this.$config.appName - }) - .then((response) => { - console.log("Response", response); - response.password = pass; - response.is_guest = true; - this.$store.commit("setTempUser", response); - return response; - }); - } - - // Get an access token - clientPromise = clientPromise.then(user => { - var data = { user: User.localPart(user.user_id), password: user.password, type: "m.login.password", initial_device_display_name: this.$config.appName }; - if (user.device_id) { - data.device_id = user.device_id; - } - return tempMatrixClient.login("m.login.password", data) - }) - - // Then login - // - // Create a slimmed down client, without crypto. This one is - // Only used to get public room info from. - clientPromise = clientPromise.then(user => { - var opts = { - baseUrl: this.$config.defaultServer, - userId: user.user_id, - accessToken: user.access_token, - timelineSupport: false, - } - var matrixClient = sdk.createClient(opts); - matrixClient.startClient(); - return matrixClient; - }); - } - - const findOrGetMore = function _findOrGetMore(client, response) { - for (var room of response.chunk) { - if ((roomId.startsWith("#") && room.canonical_alias == roomId) || (roomId.startsWith("!") && room.room_id == roomId)) { - if (room.avatar_url) { - room.avatar = client.mxcUrlToHttp(room.avatar_url, 80, 80, 'scale', true); - } - return Promise.resolve(room); - } - } - if (response.next_batch) { - return client.publicRooms({ server: server, limit: 1000, since: response.next_batch }) - .then(response => { - return _findOrGetMore(client, response); - }) - .catch(err => { - return Promise.reject("Failed to find room: " + err); - }); - } else { - return Promise.reject("No more data"); - } - }; - - var matrixClient; - return clientPromise - .then(client => { - matrixClient = client; - return matrixClient.publicRooms({ server: server, limit: 1000 }) - }) - .then(response => { - return findOrGetMore(matrixClient, response); - }) - .catch(err => { - return Promise.reject("Failed to find room: " + err); - }); - }, - - updateNotificationCount() { - var count = 0; - this.rooms.forEach(room => { - count += room.getUnreadNotificationCount('total') || 0; - }); - this.notificationCount = count; - } - - } - }) - - sdk.setCryptoStoreFactory(matrixService.createCryptoStore.bind(matrixService)); - - Vue.prototype.$matrix = matrixService; + install(Vue, options) { + if (!options || !options.store) { + throw new Error("Please initialise plugin with a Vuex store."); } -} + + // Set User-Agent headers. + // Update: browser do not allow this, "Refused to set unsafe header "User-Agent"" + // Keep this code around however, since it's an example of how to add headers to a request... + // sdk.wrapRequest((orig, opts, callback) => { + // opts.headers = opts.headers || {} + // opts.headers['User-Agent'] = "Keanu"; + // var ret = orig(opts, callback); + // return ret; + // }); + const store = options.store; + const i18n = options.i18n; + + const matrixService = new Vue({ + store, + i18n, + data() { + return { + matrixClient: null, + matrixClientReady: false, + rooms: [], + userDisplayName: null, + userAvatar: null, + currentRoom: null, + notificationCount: 0, + }; + }, + mounted() { + console.log("Matrix service mounted"); + }, + + computed: { + ready() { + return this.matrixClient != null && this.matrixClientReady; + }, + + currentUser() { + return this.$store.state.auth.user; + }, + + currentUserId() { + const user = this.currentUser || {}; + return user.user_id; + }, + + currentUserDisplayName() { + if (this.ready) { + const user = this.matrixClient.getUser(this.currentUserId) || {}; + return this.userDisplayName || user.displayName; + } + return null; + }, + + currentUserHomeServer() { + return User.serverName(this.currentUserId); + }, + + currentRoomId() { + return this.$store.state.currentRoomId; + }, + + joinedRooms() { + return this.rooms.filter((room) => { + return room.selfMembership === "join"; + }); + }, + + invites() { + return this.rooms.filter((room) => { + return room.selfMembership === "invite"; + }); + }, + }, + + watch: { + currentRoomId: { + immediate: true, + handler(roomId) { + this.currentRoom = this.getRoom(roomId); + }, + }, + }, + + methods: { + createCryptoStore() { + console.log("create crypto store"); + return new LocalStorageCryptoStore(this.$store.getters.storage); + }, + login(user) { + const tempMatrixClient = sdk.createClient( + User.homeServerUrl(user.home_server) + ); + var promiseLogin; + + const self = this; + if (user.access_token) { + // Logged in on "real" account + promiseLogin = Promise.resolve(user); + } else if (user.is_guest && !user.user_id) { + // Generate random username and password. We don't user REAL matrix + // guest accounts because 1. They are not allowed to post media, 2. They + // can not use avatars and 3. They can not seamlessly be upgraded to real accounts. + // + // Instead, we use an ILAG approach, Improved Landing as Guest. + const user = util.randomUser(); + const pass = util.randomPass(); + promiseLogin = tempMatrixClient + .register(user, pass, null, { + type: "m.login.dummy", + initial_device_display_name: this.$config.appName, + }) + .then((response) => { + console.log("Response", response); + var u = Object.assign({}, response); + u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response. + u.password = pass; + u.is_guest = true; + this.$store.commit("setUser", u); + return u; + }); + } else { + var data = { + user: User.localPart(user.user_id), + password: user.password, + type: "m.login.password", + initial_device_display_name: this.$config.appName, + }; + if (user.device_id) { + data.device_id = user.device_id; + } + promiseLogin = tempMatrixClient + .login("m.login.password", data) + .then((response) => { + var u = Object.assign({}, response); + if (user.is_guest) { + // Copy over needed properties + u = Object.assign(user, response); + } + u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response. + this.$store.commit("setUser", u); + return u; + }); + } + + return promiseLogin.then((user) => { + return self.getMatrixClient(user); + }); + }, + + clearCryptoStore() { + // Clear crypto related data + // TODO - for some reason "clearStores" called in "logout" only clears the "account" crypto + // data item, not all sessions etc. Why? We need to do that manually here! + const toRemove = []; + const storage = this.$store.getters.storage; + for (let i = 0; i < storage.length; ++i) { + const key = storage.key(i); + if (key.startsWith("crypto.")) toRemove.push(key); + } + for (const key of toRemove) { + storage.removeItem(key); + } + }, + + logout() { + if (this.matrixClient) { + this.removeMatrixClientListeners(this.matrixClient); + this.matrixClient.stopClient(); + this.matrixClient.clearStores().then(() => { + this.clearCryptoStore(); + }); + this.matrixClient = null; + this.matrixClientReady = false; + } else { + this.clearCryptoStore(); + } + + this.$store.commit("setUser", null); + this.$store.commit("setCurrentRoomId", null); + this.rooms = []; + this.userDisplayName = null; + this.userAvatar = null; + this.currentRoom = null; + this.notificationCount = 0; + }, + + initClient() { + this.reloadRooms(); + this.matrixClientReady = true; + this.matrixClient.emit("Matrix.initialized", this.matrixClient); + this.matrixClient + .getProfileInfo(this.currentUserId) + .then((info) => { + console.log("Got user profile: " + JSON.stringify(info)); + this.userDisplayName = info.displayname; + this.userAvatar = info.avatar_url; + }) + .catch((err) => { + console.log("Failed to get user profile: ", err); + }); + }, + + getMatrixClient(user) { + if (user === undefined) { + user = this.$store.state.auth.user; + } + if (this.matrixClientReady) { + return new Promise((resolve, ignoredreject) => { + resolve(user); + }); + } else if (this.matrixClient) { + return new Promise((resolve, ignoredreject) => { + this.matrixClient.once("Matrix.initialized", (ignoredclient) => { + resolve(user); + }); + }); + } + + const matrixStore = new sdk.MemoryStore(this.$store.getters.storage); + const webStorageSessionStore = new sdk.WebStorageSessionStore( + this.$store.getters.storage + ); + + var homeServer = user.home_server; + if (!homeServer.startsWith("https://")) { + homeServer = "https://" + homeServer; + } + + var opts = { + baseUrl: homeServer, + userId: user.user_id, + store: matrixStore, + sessionStore: webStorageSessionStore, + deviceId: user.device_id, + accessToken: user.access_token, + timelineSupport: true, + unstableClientRelationAggregation: true, + //useAuthorizationHeader: true + }; + this.matrixClient = sdk.createClient(opts); + // if (user.is_guest) { + // this.matrixClient.setGuest(true); + // } + return this.matrixClient + .initCrypto() + .then(() => { + console.log("Crypto initialized"); + + this.addMatrixClientListeners(this.matrixClient); + + this.matrixClient.startClient(); + return this.matrixClient; + }) + .then((matrixClient) => { + if (matrixClient.isInitialSyncComplete()) { + console.log("Initial sync done already!"); + return matrixClient; + } else { + return new Promise((resolve, reject) => { + matrixClient.once("sync", function( + state, + ignoredprevState, + ignoredres + ) { + console.log(state); // state will be 'PREPARED' when the client is ready to use + if (state == "PREPARED") { + resolve(matrixClient); + } else if (state == "ERROR") { + reject("Error syncing"); + } + }); + }); + } + }) + .then(() => { + // Ready to use! Start by loading rooms. + this.initClient(); + return user; + }); + }, + + /** + * Returns a promise that will log us into the Matrix. + * + * Will use a real account, if we have one, otherwise will create + * a random account. + */ + getLoginPromise() { + if (this.ready) { + return Promise.resolve(this.currentUser); + } + return this.$store.dispatch( + "login", + this.currentUser || + new User(this.$config.defaultServer, "", "", true) + ); + }, + + addMatrixClientListeners(client) { + if (client) { + client.on("event", this.onEvent); + client.on("Room", this.onRoom); + client.on("Session.logged_out", this.onSessionLoggedOut); + client.on("Room.myMembership", this.onRoomMyMembership); + } + }, + + removeMatrixClientListeners(client) { + if (client) { + client.off("event", this.onEvent); + client.off("Room", this.onRoom); + client.off("Session.logged_out", this.onSessionLoggedOut); + client.off("Room.myMembership", this.onRoomMyMembership); + } + }, + + onEvent(event) { + switch (event.getType()) { + case "m.room.topic": + { + const room = this.matrixClient.getRoom(event.getRoomId()); + if (room) { + Vue.set(room, "topic", event.getContent().topic); + } + } + break; + + case "m.room.avatar": + { + const room = this.matrixClient.getRoom(event.getRoomId()); + if (room) { + Vue.set( + room, + "avatar", + room.getAvatarUrl( + this.matrixClient.getHomeserverUrl(), + 80, + 80, + "scale", + true + ) + ); + } + } + break; + + case "m.room.member": + { + if ( + this.currentRoom && + event.getRoomId() == this.currentRoom.roomId + ) { + // Don't use this.currentRoomId, may be an alias. We need the real id! + if ( + event.getContent().membership == "leave" && + (event.getPrevContent() || {}).membership == "join" && + event.getStateKey() == this.currentUserId && + event.getSender() != this.currentUserId + ) { + // We were kicked + const wasPurged = + event.getContent().reason == "Room Deleted"; + this.$navigation.push( + { name: "Goodbye", params: { roomWasPurged: wasPurged } }, + -1 + ); + } + } + } + break; + } + this.updateNotificationCount(); + }, + + onRoom(ignoredroom) { + console.log("Got room", ignoredroom); + this.reloadRooms(); + this.updateNotificationCount(); + }, + + onRoomMyMembership(room) { + if (room.selfMembership === "invite") { + // Invitation. Need to call "recalculate" to pick + // up room name, not sure why exactly. + room.recalculate(); + this.reloadRooms(); + } + }, + + onSessionLoggedOut() { + console.log("Logged out!"); + if (this.matrixClient) { + this.removeMatrixClientListeners(this.matrixClient); + this.matrixClient.stopClient(); + this.matrixClient = null; + this.matrixClientReady = false; + } + + // For "real" accounts we totally wipe the user object, but for "guest" + // accounts (i.e. created from random data and with password never changed) + // we need to hang on to the generated password and use that to login to a new + // session, so only wipe the token in s that case. + // Clear the access token + var user = JSON.parse(this.$store.state.auth.user); + if (user.is_guest) { + delete user.access_token; + this.$store.commit("setUser", user); + // Login again + this.login(user); + } else { + this.$store.commit("setUser", null); + this.$store.commit("setCurrentRoomId", null); + this.$navigation.push({ path: "/login" }, -1); + } + }, + + reloadRooms() { + // TODO - do incremental update instead of replacing the whole array + // each time! + var updatedRooms = this.matrixClient.getVisibleRooms(); + updatedRooms = updatedRooms.filter((room) => { + return ( + room.selfMembership && + (room.selfMembership == "invite" || room.selfMembership == "join") + ); + }); + updatedRooms.forEach((room) => { + if (!room.avatar) { + Vue.set( + room, + "avatar", + room.getAvatarUrl( + this.matrixClient.getHomeserverUrl(), + 80, + 80, + "scale", + true + ) + ); + } + }); + console.log("Reload rooms", updatedRooms); + Vue.set(this, "rooms", updatedRooms); + const currentRoom = this.getRoom(this.$store.state.currentRoomId); + if (this.currentRoom != currentRoom) { + this.currentRoom = currentRoom; + } + }, + + setCurrentRoomId(roomId) { + this.$store.commit("setCurrentRoomId", roomId); + this.currentRoom = this.getRoom(roomId); + }, + + getRoom(roomId) { + if (!roomId) { + return null; + } + var room = null; + if (this.matrixClient) { + const visibleRooms = this.matrixClient.getRooms(); + room = visibleRooms.find((room) => { + if (roomId.startsWith("#")) { + return room.getCanonicalAlias() == roomId; + } + return room.roomId == roomId; + }); + } + return room || null; + }, + + /** + * Return all users we are in a "invite" only room with! + */ + getAllFriends() { + var ids = {}; + const ret = []; + for (const room of this.rooms) { + if ( + room.selfMembership == "join" && + this.getRoomJoinRule(room) == "invite" + ) { + for (const member of room.getJoinedMembers()) { + if ( + member.userId != this.currentUserId && + !ids[member.userId] + ) { + ids[member.userId] = member; + ret.push(member); + } + } + } + } + ret.sort((a, b) => { + const aName = a.user ? a.user.displayName : a.name; + const bName = b.user ? b.user.displayName : b.name; + return aName.localeCompare(bName); + }); + return ret; + }, + + getRoomJoinRule(room) { + if (room) { + const joinRules = room.currentState.getStateEvents( + "m.room.join_rules", + "" + ); + return joinRules && joinRules.getContent().join_rule; + } + return null; + }, + + getRoomHistoryVisibility(room) { + if (room) { + const historyVisibility = room.currentState.getStateEvents( + "m.room.history_visibility", + "" + ); + return ( + historyVisibility && + historyVisibility.getContent().history_visibility + ); + } + return null; + }, + + leaveRoom(roomId) { + return this.matrixClient.leave(roomId, undefined).then(() => { + this.rooms = this.rooms.filter((room) => { + room.roomId != roomId; + }); + this.matrixClient.store.removeRoom(roomId); + //this.matrixClient.forget(roomId, true, undefined); + }); + }, + + /** + * Purge the room with the given id! This means: + * - Make room invite only + * - Disallow guest access + * - Set history visibility to 'joined' + * - Redact all events + * - Kick all members + * @param roomId + */ + purgeRoom(roomId, statusCallback) { + const oldGlobalErrorSetting = this.matrixClient.getGlobalErrorOnUnknownDevices(); + return new Promise((resolve, reject) => { + const room = this.getRoom(roomId); + if (!room) { + reject("Room not found!"); + return; + } + + const timelineWindow = new TimelineWindow( + this.matrixClient, + room.getUnfilteredTimelineSet(), + {} + ); + const self = this; + + //console.log("Purge: set invite only"); + statusCallback(this.$t("room.purge_set_room_state")); + this.matrixClient + .sendStateEvent( + roomId, + "m.room.join_rules", + { join_rule: "invite" }, + "" + ) + .then(() => { + //console.log("Purge: forbid guest access"); + return this.matrixClient.sendStateEvent( + roomId, + "m.room.guest_access", + { guest_access: "forbidden" }, + "" + ); + }) + .then(() => { + //console.log("Purge: set history visibility to 'joined'"); + return this.matrixClient.sendStateEvent( + roomId, + "m.room.history_visibility", + { + history_visibility: "joined", + } + ); + }) + .then(() => { + //console.log("Purge: create timeline"); + return timelineWindow.load(null, 100); + }) + .then(() => { + const getMoreIfAvailable = function _getMoreIfAvailable() { + if (timelineWindow.canPaginate(EventTimeline.BACKWARDS)) { + //console.log("Purge: page back"); + return timelineWindow + .paginate(EventTimeline.BACKWARDS, 100, true, 5) + .then((ignoredsuccess) => { + return _getMoreIfAvailable.call(self); + }); + } else { + return Promise.resolve("Done"); + } + }.bind(self); + return getMoreIfAvailable(); + }) + .then(() => { + //console.log("Purge: redact events"); + statusCallback(this.$t("room.purge_redacting_events")); + // First ignore unknown device errors + this.matrixClient.setGlobalErrorOnUnknownDevices(false); + var redactionPromises = []; + timelineWindow.getEvents().forEach((event) => { + if ( + !event.isRedacted() && + !event.isRedaction() && + !event.isState() + ) { + // Redact! + redactionPromises.push( + this.matrixClient.redactEvent( + event.getRoomId(), + event.getId() + ) + ); + } + }); + return Promise.all(redactionPromises); + }) + .then(() => { + //console.log("Purge: kick members"); + statusCallback(this.$t("room.purge_removing_members")); + var joined = room.getMembersWithMembership("join"); + var invited = room.getMembersWithMembership("invite"); + var members = joined.concat(invited); + + var kickPromises = []; + members.forEach((member) => { + if (member.userId != self.currentUserId) { + kickPromises.push( + this.matrixClient.kick( + roomId, + member.userId, + "Room Deleted" + ) + ); + } + }); + return Promise.all(kickPromises); + }) + .then(() => { + statusCallback(null); + this.matrixClient.setGlobalErrorOnUnknownDevices( + oldGlobalErrorSetting + ); + return this.leaveRoom(roomId); + }) + .then(() => { + resolve(true); // Done! + }) + .catch((err) => { + console.error("Error purging room", err); + this.matrixClient.setGlobalErrorOnUnknownDevices( + oldGlobalErrorSetting + ); + reject(err); + }); + }); + }, + + /** + * Get a private chat room with the given user. Searches through our rooms to see + * if a suitable room already exists. If not, one is created. + * @param {*} userId The user to chat with. + */ + getOrCreatePrivateChat(userId) { + return new Promise((resolve, reject) => { + for (const room of this.rooms) { + // Is the other member the one we are looking for? + if (this.isDirectRoomWith(room, userId)) { + var member = room.getMember(userId); + if (member && member.membership != "join") { + // Resend invite + this.matrixClient.invite(room.roomId, userId); + } + resolve(room); + return; + } + } + + // No room found, create one + // + const createRoomOptions = { + visibility: "private", // Not listed! + preset: "private_chat", + initial_state: [ + { + type: "m.room.encryption", + state_key: "", + content: { + algorithm: "m.megolm.v1.aes-sha2", + }, + }, + { + type: "m.room.guest_access", + state_key: "", + content: { + guest_access: "forbidden", + }, + }, + { + type: "m.room.history_visibility", + state_key: "", + content: { + history_visibility: "joined", + }, + }, + ], + invite: [userId], + }; + return this.matrixClient + .createRoom(createRoomOptions) + .then(({ room_id, room_alias }) => { + resolve(this.getRoom(room_alias || room_id)); + }) + .catch((error) => { + reject(error); + }); + }); + }, + + /** + * Return true if this room is a direct room with the given user. + * @param { } room + * @param {*} userId + */ + isDirectRoomWith(room, userId) { + if ( + room.selfMembership == "join" && + room.getInvitedAndJoinedMemberCount() == 2 + ) { + // Is the other member the one we are looking for? + if ( + room + .getMembersWithMembership("join") + .some((item) => item.userId == userId) + ) { + return true; + } else if ( + room + .getMembersWithMembership("invite") + .some((item) => item.userId == userId) + ) { + return true; + } + } + return false; + }, + + on(event, handler) { + if (this.matrixClient) { + this.matrixClient.on(event, handler); + } + }, + + off(event, handler) { + if (this.matrixClient) { + this.matrixClient.off(event, handler); + } + }, + + setPassword(oldPassword, newPassword) { + if (this.matrixClient && this.currentUser) { + const authDict = { + type: "m.login.password", + identifier: { + type: "m.id.user", + user: this.currentUser.user_id, + }, + // TODO: Remove `user` once servers support proper UIA + // See https://github.com/matrix-org/synapse/issues/5665 + user: this.currentUser.user_id, + password: oldPassword, + }; + const self = this; + return this.matrixClient + .setPassword(authDict, newPassword) + .then(() => { + // Forget password and remove the 'is_guest' flag, we are now a "real" user! + self.currentUser.password = undefined; + self.currentUser.is_guest = false; + self.$store.commit("setUser", self.currentUser); + }) + .then(() => { + return true; + }); + } + return Promise.resolve(false); + }, + + uploadFile(file, opts) { + return this.matrixClient.uploadContent(file, opts); + }, + + getPublicRoomInfo(roomId) { + if (!roomId) { + return Promise.reject("Invalid parameters"); + } + + const parts = roomId.split(":"); + if (parts.length != 2) { + return Promise.reject("Unknown room server"); + } + const server = parts[1]; + + var clientPromise; + if (this.matrixClient) { + clientPromise = this.getMatrixClient().then(() => { + return this.matrixClient; + }); + } else { + const tempMatrixClient = sdk.createClient( + this.$config.defaultServer + ); + var tempUserString = this.$store.state.tempuser; + var tempUser = null; + if (tempUserString) { + tempUser = JSON.parse(tempUserString); + } + + // Need to create an account? + // + if (tempUser) { + clientPromise = Promise.resolve(tempUser); + } else { + const user = util.randomUser(); + const pass = util.randomPass(); + clientPromise = tempMatrixClient + .register(user, pass, null, { + type: "m.login.dummy", + initial_device_display_name: this.$config.appName, + }) + .then((response) => { + console.log("Response", response); + response.password = pass; + response.is_guest = true; + this.$store.commit("setTempUser", response); + return response; + }); + } + + // Get an access token + clientPromise = clientPromise.then((user) => { + var data = { + user: User.localPart(user.user_id), + password: user.password, + type: "m.login.password", + initial_device_display_name: this.$config.appName, + }; + if (user.device_id) { + data.device_id = user.device_id; + } + return tempMatrixClient.login("m.login.password", data); + }); + + // Then login + // + // Create a slimmed down client, without crypto. This one is + // Only used to get public room info from. + clientPromise = clientPromise.then((user) => { + var opts = { + baseUrl: this.$config.defaultServer, + userId: user.user_id, + accessToken: user.access_token, + timelineSupport: false, + }; + var matrixClient = sdk.createClient(opts); + matrixClient.startClient(); + return matrixClient; + }); + } + + const findOrGetMore = function _findOrGetMore(client, response) { + for (var room of response.chunk) { + if ( + (roomId.startsWith("#") && room.canonical_alias == roomId) || + (roomId.startsWith("!") && room.room_id == roomId) + ) { + if (room.avatar_url) { + room.avatar = client.mxcUrlToHttp( + room.avatar_url, + 80, + 80, + "scale", + true + ); + } + return Promise.resolve(room); + } + } + if (response.next_batch) { + return client + .publicRooms({ + server: server, + limit: 1000, + since: response.next_batch, + }) + .then((response) => { + return _findOrGetMore(client, response); + }) + .catch((err) => { + return Promise.reject("Failed to find room: " + err); + }); + } else { + return Promise.reject("No more data"); + } + }; + + var matrixClient; + return clientPromise + .then((client) => { + matrixClient = client; + return matrixClient.publicRooms({ server: server, limit: 1000 }); + }) + .then((response) => { + return findOrGetMore(matrixClient, response); + }) + .catch((err) => { + return Promise.reject("Failed to find room: " + err); + }); + }, + + updateNotificationCount() { + var count = 0; + this.rooms.forEach((room) => { + count += room.getUnreadNotificationCount("total") || 0; + }); + this.notificationCount = count; + }, + }, + }); + + sdk.setCryptoStoreFactory( + matrixService.createCryptoStore.bind(matrixService) + ); + + Vue.prototype.$matrix = matrixService; + }, +}; diff --git a/vue.config.js b/vue.config.js index 043039e..bab83bd 100644 --- a/vue.config.js +++ b/vue.config.js @@ -1,33 +1,35 @@ -const CopyWebpackPlugin = require('copy-webpack-plugin') +const CopyWebpackPlugin = require("copy-webpack-plugin"); module.exports = { - "transpileDependencies": [ - "vuetify" - ], + transpileDependencies: ["vuetify"], - publicPath: process.env.NODE_ENV === 'production' - ? './' - : './', + publicPath: process.env.NODE_ENV === "production" ? "./" : "./", - chainWebpack: config => { - config.plugin('html').tap(args => { + chainWebpack: (config) => { + config.plugin("html").tap((args) => { var c = require("./src/assets/config.json"); args[0].title = c.appName; return args; - }) + }); }, configureWebpack: { - devtool: 'source-map', + devtool: "source-map", plugins: [ - new CopyWebpackPlugin([{ - from: "./src/assets/config.json", - to: "./", - }]) - ] + new CopyWebpackPlugin([ + { + from: "./src/assets/config.json", + to: "./", + }, + { + from: "./node_modules/@matrix-org/olm/olm.wasm", + to: "./js/olm.wasm", + }, + ]), + ], }, devServer: { - //https: true + //https: true, }, -} \ No newline at end of file +};