From 6176b596967564b47b06e1fce7087e40da357822 Mon Sep 17 00:00:00 2001 From: saul Date: Fri, 5 May 2023 11:22:17 +1200 Subject: [PATCH 01/48] Prepare another example. --- examples/crypto-node/package.json | 15 +++++++++++++++ examples/crypto-node/tsconfig.json | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 examples/crypto-node/package.json create mode 100644 examples/crypto-node/tsconfig.json diff --git a/examples/crypto-node/package.json b/examples/crypto-node/package.json new file mode 100644 index 00000000000..6d88e3b6a66 --- /dev/null +++ b/examples/crypto-node/package.json @@ -0,0 +1,15 @@ +{ + "name": "example-app", + "type": "module", + "version": "0.0.0", + "description": "", + "main": "src/index.js", + "scripts": { + "preinstall": "npm install ../.." + }, + "author": "", + "license": "Apache 2.0", + "dependencies": { + "matrix-js-sdk": "file:../.." + } +} diff --git a/examples/crypto-node/tsconfig.json b/examples/crypto-node/tsconfig.json new file mode 100644 index 00000000000..aa9d91941cd --- /dev/null +++ b/examples/crypto-node/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2016", + "esModuleInterop": true, + "moduleResolution": "node", + "noUnusedLocals": true, + "noImplicitAny": false, + "noImplicitThis": true, + "strictNullChecks": true, + "noEmit": true, + "declaration": true, + "strict": true + }, + + "ts-node": { + "esm": true + }, + + "include": ["./src/**/*.ts"] +} From 53bf29b3ef4c06585c1f9f4da7885ce3331b883c Mon Sep 17 00:00:00 2001 From: saul Date: Fri, 5 May 2023 14:02:21 +1200 Subject: [PATCH 02/48] Fix tsconfig, add missing packages and get login working. --- examples/crypto-node/.gitignore | 1 + examples/crypto-node/package.json | 7 ++++- examples/crypto-node/src/index.ts | 46 ++++++++++++++++++++++++++++++ examples/crypto-node/tsconfig.json | 33 ++++++++++----------- 4 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 examples/crypto-node/.gitignore create mode 100644 examples/crypto-node/src/index.ts diff --git a/examples/crypto-node/.gitignore b/examples/crypto-node/.gitignore new file mode 100644 index 00000000000..065e5330b54 --- /dev/null +++ b/examples/crypto-node/.gitignore @@ -0,0 +1 @@ +src/credentials.ts diff --git a/examples/crypto-node/package.json b/examples/crypto-node/package.json index 6d88e3b6a66..68fb193c0c0 100644 --- a/examples/crypto-node/package.json +++ b/examples/crypto-node/package.json @@ -10,6 +10,11 @@ "author": "", "license": "Apache 2.0", "dependencies": { - "matrix-js-sdk": "file:../.." + "cli-color": "^1.0.0", + "matrix-js-sdk": "file:../..", + "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz" + }, + "devDependencies": { + "@types/cli-color": "^2.0.2" } } diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts new file mode 100644 index 00000000000..9a6ccf9ec2d --- /dev/null +++ b/examples/crypto-node/src/index.ts @@ -0,0 +1,46 @@ +import olm from "olm"; +import fs from "fs/promises"; +import credentials from "./credentials.js"; + +const oldFetch = fetch; + +global.fetch = async function (input: RequestInfo | URL | string, init?: RequestInit): Promise { + if (typeof input == "string" && input.charAt(0) === "/") { + return await fs.readFile(input).then(d => new Response(d)); + } + + return await oldFetch.apply(this, [input, init]); +}; + +global.Olm = olm; + +import * as sdk from "../../../lib/index.js"; + +const startWithAccessToken = async (accessToken: string, deviceId: string) => { + const client = sdk.createClient({ + userId: credentials.userId, + baseUrl: credentials.baseUrl, + accessToken, + deviceId + }); + + await client.initCrypto(); + await client.startClient({ initialSyncLimit: 0 }); + + return client; +}; + +const start = async () => { + const loginClient = sdk.createClient({ baseUrl: credentials.baseUrl }); + + const res = await loginClient.login("m.login.password", { + user: credentials.userId, + password: credentials.password + }); + + loginClient.stopClient(); + + return await startWithAccessToken(res.access_token, res.device_id); +}; + +const client = await start(); diff --git a/examples/crypto-node/tsconfig.json b/examples/crypto-node/tsconfig.json index aa9d91941cd..0fefd5e2840 100644 --- a/examples/crypto-node/tsconfig.json +++ b/examples/crypto-node/tsconfig.json @@ -1,20 +1,21 @@ { - "compilerOptions": { - "target": "es2016", - "esModuleInterop": true, - "moduleResolution": "node", - "noUnusedLocals": true, - "noImplicitAny": false, - "noImplicitThis": true, - "strictNullChecks": true, - "noEmit": true, - "declaration": true, - "strict": true - }, + "compilerOptions": { + "target": "es2017", + "module": "es2022", + "esModuleInterop": true, + "moduleResolution": "node", + "noUnusedLocals": true, + "noImplicitAny": false, + "noImplicitThis": true, + "strictNullChecks": true, + "noEmit": true, + "declaration": true, + "strict": true + }, - "ts-node": { - "esm": true - }, + "ts-node": { + "esm": true + }, - "include": ["./src/**/*.ts"] + "include": ["./src/**/*.ts"] } From 80ae44b5d7bc2a340512dc63412e0fb47eb354a6 Mon Sep 17 00:00:00 2001 From: saul Date: Fri, 5 May 2023 14:56:05 +1200 Subject: [PATCH 03/48] Reduce logging and display rooms. --- examples/crypto-node/src/index.ts | 69 ++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index 9a6ccf9ec2d..99ca429190b 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -6,7 +6,9 @@ const oldFetch = fetch; global.fetch = async function (input: RequestInfo | URL | string, init?: RequestInit): Promise { if (typeof input == "string" && input.charAt(0) === "/") { - return await fs.readFile(input).then(d => new Response(d)); + return await fs.readFile(input).then(d => new Response(d, { + headers: { "content-type": "application/wasm" } + })); } return await oldFetch.apply(this, [input, init]); @@ -15,6 +17,13 @@ global.fetch = async function (input: RequestInfo | URL | string, init?: Request global.Olm = olm; import * as sdk from "../../../lib/index.js"; +import { logger } from "../../../lib/logger.js"; +import type { MatrixClient, Room } from "../../../lib/index.js"; + +logger.setLevel(5); + +let roomList: Room[] = []; +let viewingRoom: Room | null = null; const startWithAccessToken = async (accessToken: string, deviceId: string) => { const client = sdk.createClient({ @@ -25,6 +34,7 @@ const startWithAccessToken = async (accessToken: string, deviceId: string) => { }); await client.initCrypto(); + await client.startClient({ initialSyncLimit: 0 }); return client; @@ -43,4 +53,61 @@ const start = async () => { return await startWithAccessToken(res.access_token, res.device_id); }; +const setRoomList = (client: MatrixClient) => { + roomList = client.getRooms(); + roomList.sort((a, b) => { + const aEvents = a.getLiveTimeline().getEvents(); + const bEvents = b.getLiveTimeline().getEvents(); + + const aMsg = aEvents[aEvents.length - 1]; + + if (aMsg == null) { + return -1; + } + + const bMsg = bEvents[bEvents.length - 1]; + + if (bMsg == null) { + return 1; + } + + if (aMsg.getTs() === bMsg.getTs()) { + return 0; + } + + return aMsg.getTs() > bMsg.getTs() ? 1 : -1; + }); +}; + +const fixWidth = (str: string, len: number) => { + if (str.length === len) { + return str; + } + + return str.length > len ? `${str.substring(0, len - 1)}\u2026` : str.padEnd(len); +}; + +const printRoomList = () => { + console.log("\nRoom List:"); + + for (let i = 0; i < roomList.length; i++) { + const events = roomList[i].getLiveTimeline().getEvents(); + const msg = events[events.length - 1]; + const dateStr = msg ? new Date(msg.getTs()).toISOString().replace(/T/, " ").replace(/\..+/, "") : "---"; + + const roomName = fixWidth(roomList[i].name, 25); + const memberCount = roomList[i].getJoinedMembers().length; + + console.log(`[${i}] ${roomName} (${memberCount} members) ${dateStr}`); + } +}; + const client = await start(); + +client.on(sdk.ClientEvent.Room, () => { + setRoomList(client); + + if (!viewingRoom) { + printRoomList(); + } +}); From f592b3e1f53b1796a554af60b59ba4610cb7e312 Mon Sep 17 00:00:00 2001 From: saul Date: Fri, 5 May 2023 15:23:19 +1200 Subject: [PATCH 04/48] Update olm lib. --- examples/crypto-node/.npmrc | 1 + examples/crypto-node/package.json | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 examples/crypto-node/.npmrc diff --git a/examples/crypto-node/.npmrc b/examples/crypto-node/.npmrc new file mode 100644 index 00000000000..45c69bbe015 --- /dev/null +++ b/examples/crypto-node/.npmrc @@ -0,0 +1 @@ +@matrix-org:registry=https://gitlab.matrix.org/api/v4/packages/npm/ diff --git a/examples/crypto-node/package.json b/examples/crypto-node/package.json index 68fb193c0c0..24ac9cd1438 100644 --- a/examples/crypto-node/package.json +++ b/examples/crypto-node/package.json @@ -10,9 +10,8 @@ "author": "", "license": "Apache 2.0", "dependencies": { - "cli-color": "^1.0.0", - "matrix-js-sdk": "file:../..", - "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz" + "@matrix-org/olm": "^3.2.15", + "matrix-js-sdk": "file:../.." }, "devDependencies": { "@types/cli-color": "^2.0.2" From 0da301be3eb86cc0f1fa6c0bf48d39f560689856 Mon Sep 17 00:00:00 2001 From: saul Date: Fri, 5 May 2023 16:01:54 +1200 Subject: [PATCH 05/48] Join rooms and display messages. --- examples/crypto-node/src/index.ts | 81 +++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 10 deletions(-) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index 99ca429190b..c498d46a6e9 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -1,5 +1,6 @@ -import olm from "olm"; +import olm from "@matrix-org/olm"; import fs from "fs/promises"; +import readline from "readline"; import credentials from "./credentials.js"; const oldFetch = fetch; @@ -20,11 +21,18 @@ import * as sdk from "../../../lib/index.js"; import { logger } from "../../../lib/logger.js"; import type { MatrixClient, Room } from "../../../lib/index.js"; -logger.setLevel(5); +logger.setLevel(4); let roomList: Room[] = []; let viewingRoom: Room | null = null; +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +rl.setPrompt("$ "); + const startWithAccessToken = async (accessToken: string, deviceId: string) => { const client = sdk.createClient({ userId: credentials.userId, @@ -35,7 +43,13 @@ const startWithAccessToken = async (accessToken: string, deviceId: string) => { await client.initCrypto(); - await client.startClient({ initialSyncLimit: 0 }); + await client.startClient({ initialSyncLimit: 20 }); + + const state: string = await new Promise(resolve => client.once(sdk.ClientEvent.Sync, resolve)); + + if (state !== "PREPARED") { + throw new Error("Sync failed."); + } return client; }; @@ -79,13 +93,8 @@ const setRoomList = (client: MatrixClient) => { }); }; -const fixWidth = (str: string, len: number) => { - if (str.length === len) { - return str; - } - - return str.length > len ? `${str.substring(0, len - 1)}\u2026` : str.padEnd(len); -}; +const fixWidth = (str: string, len: number) => + str.length > len ? `${str.substring(0, len - 1)}\u2026` : str.padEnd(len); const printRoomList = () => { console.log("\nRoom List:"); @@ -102,6 +111,23 @@ const printRoomList = () => { } }; +const printMessages = () => { + if (!viewingRoom) { + printRoomList(); + return; + } + + const events = viewingRoom.getLiveTimeline().getEvents(); + + for (const event of events) { + if (event.getType() !== sdk.EventType.RoomMessage) { + continue; + } + + console.log(event.getContent().body); + } +}; + const client = await start(); client.on(sdk.ClientEvent.Room, () => { @@ -110,4 +136,39 @@ client.on(sdk.ClientEvent.Room, () => { if (!viewingRoom) { printRoomList(); } + + rl.prompt(); +}); + +rl.on("line", (line: string) => { + if (line.trim().length === 0) { + rl.prompt(); + return; + } + + if (viewingRoom == null) { + if (line.indexOf("/join ") === 0) { + const index = line.split(" ")[1]; + + if (roomList[index] == null) { + console.log("invalid room"); + rl.prompt(); + return; + } + + viewingRoom = roomList[index]; + + printMessages(); + + rl.prompt(); + return; + } + } + + console.log("invalid command"); + rl.prompt(); }); + +setRoomList(client); +printRoomList(); +rl.prompt(); From 6b7c47e0dfd09167a991691e0a6b70bbe9c40f7b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 5 May 2023 09:13:07 +0100 Subject: [PATCH 06/48] Update types to match spec (#3330) --- src/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index cb8df7ae47f..f21743bdb25 100644 --- a/src/client.ts +++ b/src/client.ts @@ -635,7 +635,7 @@ interface IJoinRequestBody { interface ITagMetadata { [key: string]: any; - order: number; + order?: number; } interface IMessagesResponse { @@ -4179,7 +4179,7 @@ export class MatrixClient extends TypedEventEmitter { + public setRoomTag(roomId: string, tagName: string, metadata: ITagMetadata = {}): Promise<{}> { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { $userId: this.credentials.userId!, $roomId: roomId, From 52536ec9f5ddba7fa3ee4c9c94e4e728e74e8496 Mon Sep 17 00:00:00 2001 From: saul Date: Mon, 8 May 2023 13:40:48 +1200 Subject: [PATCH 07/48] Verify devices in rooms. --- examples/crypto-node/src/index.ts | 51 ++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index c498d46a6e9..94cde9855b8 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -67,6 +67,30 @@ const start = async () => { return await startWithAccessToken(res.access_token, res.device_id); }; +const verify = async (userId: string, deviceId: string) => { + await client.setDeviceKnown(userId, deviceId); + await client.setDeviceVerified(userId, deviceId); +}; + +const verifyAll = async (room: Room) => { + const members = await room.getEncryptionTargetMembers(); + const verificationPromises: Promise[] = []; + + for (const member of members) { + const devices = client.getStoredDevicesForUser(member.userId); + + for (const device of devices) { + + if (device.isUnverified()) { + verificationPromises.push( verify(member.userId, device.deviceId) ); + } + } + } + + await Promise.all(verificationPromises); +}; + + const setRoomList = (client: MatrixClient) => { roomList = client.getRooms(); roomList.sort((a, b) => { @@ -140,7 +164,7 @@ client.on(sdk.ClientEvent.Room, () => { rl.prompt(); }); -rl.on("line", (line: string) => { +rl.on("line", async (line: string) => { if (line.trim().length === 0) { rl.prompt(); return; @@ -156,19 +180,44 @@ rl.on("line", (line: string) => { return; } + if (roomList[index].getMember(client.getUserId()).membership === sdk.JoinRule.Invite) { + await client.joinRoom(roomList[index].roomId); + } + + await verifyAll(roomList[index]); + viewingRoom = roomList[index]; + await client.roomInitialSync(roomList[index].roomId, 20); printMessages(); rl.prompt(); return; } + } else { + const message = { + msgtype: sdk.MsgType.Text, + body: line + }; + + await client.sendMessage(viewingRoom.roomId, message); + rl.prompt(); + return; } console.log("invalid command"); rl.prompt(); }); +client.on(sdk.RoomEvent.Timeline, (event) => { + if (event.getType() !== "m.room.message") { + return; + } + + console.log("GOT MESSAGE", event.getContent()); +}); + + setRoomList(client); printRoomList(); rl.prompt(); From 45f1a172b19de2008eb1e7f1da69c141cfcbedf2 Mon Sep 17 00:00:00 2001 From: saul Date: Mon, 8 May 2023 14:26:16 +1200 Subject: [PATCH 08/48] Clear old devices. --- examples/crypto-node/src/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index 94cde9855b8..54d06a80f3d 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -33,6 +33,16 @@ const rl = readline.createInterface({ rl.setPrompt("$ "); +const clearDevices = async (client: MatrixClient) => { + const devices = await client.getDevices(); + + const devicesIds = devices.devices + .map(device => device.device_id) + .filter(id => id !== client.getDeviceId()); + + await Promise.all(devicesIds.map(id => client.deleteDevice(id))); +}; + const startWithAccessToken = async (accessToken: string, deviceId: string) => { const client = sdk.createClient({ userId: credentials.userId, @@ -51,6 +61,8 @@ const startWithAccessToken = async (accessToken: string, deviceId: string) => { throw new Error("Sync failed."); } + await clearDevices(client); + return client; }; From c198139bfaaf3512c140a493b129bf0e08b41c2c Mon Sep 17 00:00:00 2001 From: saul Date: Mon, 8 May 2023 15:01:06 +1200 Subject: [PATCH 09/48] Only display incoming messages when in the room. --- examples/crypto-node/src/index.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index 54d06a80f3d..b171e0b78b1 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -221,12 +221,21 @@ rl.on("line", async (line: string) => { rl.prompt(); }); -client.on(sdk.RoomEvent.Timeline, (event) => { - if (event.getType() !== "m.room.message") { +client.on(sdk.RoomEvent.Timeline, async(event, room) => { + if (!["m.room.message", "m.room.encrypted"].includes(event.getType())) { return; } - console.log("GOT MESSAGE", event.getContent()); + if (room != null && room.roomId !== viewingRoom?.roomId) { + return; + } + + await client.decryptEventIfNeeded(event); + + process.stdout.clearLine(-1); + process.stdout.cursorTo(0); + console.log(event.getContent().body); + rl.prompt(); }); From 98f4566ea3f84e5a4db1ea9449cef4ad07ee5ac0 Mon Sep 17 00:00:00 2001 From: saul Date: Mon, 8 May 2023 15:44:27 +1200 Subject: [PATCH 10/48] Refactor matrix setup into separate file. --- examples/crypto-node/src/matrix-importer.ts | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 examples/crypto-node/src/matrix-importer.ts diff --git a/examples/crypto-node/src/matrix-importer.ts b/examples/crypto-node/src/matrix-importer.ts new file mode 100644 index 00000000000..5d4f1c0c17f --- /dev/null +++ b/examples/crypto-node/src/matrix-importer.ts @@ -0,0 +1,49 @@ +/** + * This file is responsible for setting up the globals correctly before + * importing matrix and then exporting it. + */ + +/** + * We must import olm and assign it to the global before importing matrix. + */ +import olm from "@matrix-org/olm"; + +global.Olm = olm; + +/** + * We must also override the default fetch global to use the FS module when + * attempting to fetch the wasm since the default fetch does not support local + * files. + */ +import fs from "fs/promises"; + +const oldFetch = fetch; + +global.fetch = async (input: RequestInfo | URL | string, init?: RequestInit): Promise => { + // Here we need to check if it is attempting to fetch the wasm file. + if (typeof input == "string" && input.charAt(0) === "/") { + const data = await fs.readFile(input); + + // Return the wasm data as a typical response. + return new Response(data, { + headers: { "content-type": "application/wasm" } + }); + } + + // Since this is not fetching the wasm we can just use the old implementation. + return await oldFetch.apply(this, [input, init]); +}; + +/** + * We will increase the logger severity to reduce clutter. + */ +import { logger } from "../../../lib/logger.js"; + +logger.setLevel(5); + +/** + * Now we can import and export the matrix sdk. + */ +import * as sdk from "../../../lib/index.js"; + +export default sdk; From c9d502a1928b5ad2c03be59be6ff66e6a818d86b Mon Sep 17 00:00:00 2001 From: saul Date: Mon, 8 May 2023 16:03:20 +1200 Subject: [PATCH 11/48] Refactor matrix helper methods into separate file. --- examples/crypto-node/src/matrix.ts | 134 +++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 examples/crypto-node/src/matrix.ts diff --git a/examples/crypto-node/src/matrix.ts b/examples/crypto-node/src/matrix.ts new file mode 100644 index 00000000000..80b2bae2983 --- /dev/null +++ b/examples/crypto-node/src/matrix.ts @@ -0,0 +1,134 @@ +/** + * This file contains methods to help perform various actions through the matrix + * sdk. + */ + +import sdk from "./matrix-importer.js"; +import type { MatrixClient, Room } from "../../../lib/index.js"; + +/** + * This interface provides the details needed to perform a password login. + */ +export interface PasswordLogin { + baseUrl: string + userId: string + password: string +} + +/** + * This interface provide the details needed to perform a token login. + */ +export interface TokenLogin { + baseUrl: string + userId: string + accessToken: string + deviceId: string +} + +/** + * Create a matrix client using a token login. + */ +export const startWithToken = async (tokenLogin: TokenLogin | sdk.ICreateClientOpts): Promise => { + const client = sdk.createClient(tokenLogin); + + // We must initialize the crypto before starting the client. + await client.initCrypto(); + + // Now that crypto is initialized we can start the client. + await client.startClient({ initialSyncLimit: 20 }); + + // Wait until it finishes syncing. + const state: string = await new Promise(resolve => client.once(sdk.ClientEvent.Sync, resolve)); + + // If we do not recieve the correct state something has gone wrong. + if (state !== "PREPARED") { + throw new Error("Sync failed."); + } + + return client; +}; + +/** + * Get the access token and other details needed to perform a token login. + */ +export const getTokenLogin = async (passwordLogin: PasswordLogin): Promise => { + // Create a dummy client pointing to the right homeserver. + const loginClient = sdk.createClient({ baseUrl: passwordLogin.baseUrl }); + + // Perform a password login. + const response = await loginClient.login(sdk.AuthType.Password, { + user: passwordLogin.userId, + password: passwordLogin.password + }); + + // Stop the client now that we have got the access token. + loginClient.stopClient(); + + return { + baseUrl: passwordLogin.baseUrl, + userId: passwordLogin.userId, + accessToken: response.access_token, + deviceId: response.device_id + }; +}; + +/** + * Clear all devices associated with this account except for the one currently + * in use. + */ +export const clearDevices = async (client: MatrixClient) => { + const devices = await client.getDevices(); + + const devicesIds = devices.devices + .map(device => device.device_id) + .filter(id => id !== client.getDeviceId()); + + await Promise.all(devicesIds.map(id => client.deleteDevice(id))); +}; + +/** + * Start the client with a password login. + */ +export const start = async (passwordLogin: PasswordLogin, options?: { forgetDevices?: boolean }): Promise => { + // Get the token login details. + const tokenLogin = await getTokenLogin(passwordLogin); + + // Start the client with the token. + const client = await startWithToken(tokenLogin); + + // Clear other devices - this can help resolve olm session issues. + if (options?.forgetDevices) { + await clearDevices(client); + } + + return client; +} + +/** + * Mark a device associated with a user as verified. + */ +export const verifyDevice = async (client: MatrixClient, userId: string, deviceId: string): Promise => { + await client.setDeviceKnown(userId, deviceId); + await client.setDeviceVerified(userId, deviceId); +}; + +/** + * Verify all unverified devices in a room. + */ +export const verifyRoom = async (client: MatrixClient, room: Room): Promise => { + const members = await room.getEncryptionTargetMembers(); + const verificationPromises: Promise[] = []; + + for (const member of members) { + const devices = client.getStoredDevicesForUser(member.userId); + + for (const device of devices) { + + if (device.isUnverified()) { + verificationPromises.push( verifyDevice(client, member.userId, device.deviceId) ); + } + } + } + + await Promise.all(verificationPromises); +}; From 99fbb841aec64efb4e730433f522156a34d17850 Mon Sep 17 00:00:00 2001 From: saul Date: Mon, 8 May 2023 16:19:45 +1200 Subject: [PATCH 12/48] Refactor the IO to a separate file. --- examples/crypto-node/src/io.ts | 65 +++++++++++++++++++++ examples/crypto-node/src/matrix-importer.ts | 1 + 2 files changed, 66 insertions(+) create mode 100644 examples/crypto-node/src/io.ts diff --git a/examples/crypto-node/src/io.ts b/examples/crypto-node/src/io.ts new file mode 100644 index 00000000000..294585f421e --- /dev/null +++ b/examples/crypto-node/src/io.ts @@ -0,0 +1,65 @@ +import readline from "readline"; +import { EventType, Room } from "../../../lib/index.js" + +/** + * Setup the line reader. + */ +export const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +rl.setPrompt("$ "); + +/** + * Clear any text on the current line. + */ +export const clearLine = (): void => { + process.stdout.clearLine(-1); + process.stdout.cursorTo(0); +}; + +/** + * Fix a string to a specific width. + */ +export const fixWidth = (str: string, len: number): string => + str.length > len ? `${str.substring(0, len - 1)}\u2026` : str.padEnd(len); + +/** + * Create a human readable string from a timestamp. + */ +export const tsToDateString = (ts: number): string => + new Date(ts).toISOString().replace(/T/, " ").replace(/\..+/, ""); + +/** + * Print a list of rooms to the console. + */ +export const printRoomList = (rooms: Room[]): void => { + console.log("\nRoom List:"); + + for (const [i, room] of rooms.entries()) { + const events = room.getLiveTimeline().getEvents(); + const msg = events[events.length - 1]; + const date = msg ? tsToDateString(msg.getTs()) : "---"; + const name = fixWidth(room.name, 25); + const count = room.getJoinedMembers().length; + + console.log(`[${i}] ${name} (${count} members) ${date}`); + } +}; + +/** + * Print a list of messages for a room. + */ +export const printMessages = (room: Room): void => { + const events = room.getLiveTimeline().getEvents(); + + for (const event of events) { + // Ignore events that are not messages. + if (event.getType() !== EventType.RoomMessage) { + continue; + } + + console.log(event.getContent().body); + } +}; diff --git a/examples/crypto-node/src/matrix-importer.ts b/examples/crypto-node/src/matrix-importer.ts index 5d4f1c0c17f..de04ffcd237 100644 --- a/examples/crypto-node/src/matrix-importer.ts +++ b/examples/crypto-node/src/matrix-importer.ts @@ -8,6 +8,7 @@ */ import olm from "@matrix-org/olm"; +// @ts-ignore TS2322 Ignore slight olm signature mismatch. global.Olm = olm; /** From 680086d908a45264dd5335ab6b2dd745082486e0 Mon Sep 17 00:00:00 2001 From: saul Date: Mon, 8 May 2023 16:23:13 +1200 Subject: [PATCH 13/48] Add method for getting the list of rooms. --- examples/crypto-node/src/matrix.ts | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/examples/crypto-node/src/matrix.ts b/examples/crypto-node/src/matrix.ts index 80b2bae2983..d08d27abd85 100644 --- a/examples/crypto-node/src/matrix.ts +++ b/examples/crypto-node/src/matrix.ts @@ -132,3 +132,35 @@ export const verifyRoom = async (client: MatrixClient, room: Room): Promise { + const rooms = client.getRooms(); + + rooms.sort((a, b) => { + const aEvents = a.getLiveTimeline().getEvents(); + const bEvents = b.getLiveTimeline().getEvents(); + + const aMsg = aEvents[aEvents.length - 1]; + + if (aMsg == null) { + return -1; + } + + const bMsg = bEvents[bEvents.length - 1]; + + if (bMsg == null) { + return 1; + } + + if (aMsg.getTs() === bMsg.getTs()) { + return 0; + } + + return aMsg.getTs() > bMsg.getTs() ? 1 : -1; + }); + + return rooms; +}; From 6e1e825496c3ecb4cf63fac07b37308847dea972 Mon Sep 17 00:00:00 2001 From: saul Date: Mon, 8 May 2023 16:27:13 +1200 Subject: [PATCH 14/48] Cleanup main logic using refactors. --- examples/crypto-node/src/index.ts | 187 +++--------------------------- 1 file changed, 18 insertions(+), 169 deletions(-) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index b171e0b78b1..71131854f0f 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -1,176 +1,19 @@ -import olm from "@matrix-org/olm"; -import fs from "fs/promises"; -import readline from "readline"; import credentials from "./credentials.js"; - -const oldFetch = fetch; - -global.fetch = async function (input: RequestInfo | URL | string, init?: RequestInit): Promise { - if (typeof input == "string" && input.charAt(0) === "/") { - return await fs.readFile(input).then(d => new Response(d, { - headers: { "content-type": "application/wasm" } - })); - } - - return await oldFetch.apply(this, [input, init]); -}; - -global.Olm = olm; - -import * as sdk from "../../../lib/index.js"; -import { logger } from "../../../lib/logger.js"; -import type { MatrixClient, Room } from "../../../lib/index.js"; - -logger.setLevel(4); +import { rl, printRoomList, printMessages } from "./io.js"; +import { start, verifyRoom, getRoomList } from "./matrix.js"; +import sdk from "./matrix-importer.js"; +import type { Room, EventType } from "../../../lib/index.js"; let roomList: Room[] = []; let viewingRoom: Room | null = null; -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout -}); - -rl.setPrompt("$ "); - -const clearDevices = async (client: MatrixClient) => { - const devices = await client.getDevices(); - - const devicesIds = devices.devices - .map(device => device.device_id) - .filter(id => id !== client.getDeviceId()); - - await Promise.all(devicesIds.map(id => client.deleteDevice(id))); -}; - -const startWithAccessToken = async (accessToken: string, deviceId: string) => { - const client = sdk.createClient({ - userId: credentials.userId, - baseUrl: credentials.baseUrl, - accessToken, - deviceId - }); - - await client.initCrypto(); - - await client.startClient({ initialSyncLimit: 20 }); - - const state: string = await new Promise(resolve => client.once(sdk.ClientEvent.Sync, resolve)); - - if (state !== "PREPARED") { - throw new Error("Sync failed."); - } - - await clearDevices(client); - - return client; -}; - -const start = async () => { - const loginClient = sdk.createClient({ baseUrl: credentials.baseUrl }); - - const res = await loginClient.login("m.login.password", { - user: credentials.userId, - password: credentials.password - }); - - loginClient.stopClient(); - - return await startWithAccessToken(res.access_token, res.device_id); -}; - -const verify = async (userId: string, deviceId: string) => { - await client.setDeviceKnown(userId, deviceId); - await client.setDeviceVerified(userId, deviceId); -}; - -const verifyAll = async (room: Room) => { - const members = await room.getEncryptionTargetMembers(); - const verificationPromises: Promise[] = []; - - for (const member of members) { - const devices = client.getStoredDevicesForUser(member.userId); - - for (const device of devices) { - - if (device.isUnverified()) { - verificationPromises.push( verify(member.userId, device.deviceId) ); - } - } - } - - await Promise.all(verificationPromises); -}; - - -const setRoomList = (client: MatrixClient) => { - roomList = client.getRooms(); - roomList.sort((a, b) => { - const aEvents = a.getLiveTimeline().getEvents(); - const bEvents = b.getLiveTimeline().getEvents(); - - const aMsg = aEvents[aEvents.length - 1]; - - if (aMsg == null) { - return -1; - } - - const bMsg = bEvents[bEvents.length - 1]; - - if (bMsg == null) { - return 1; - } - - if (aMsg.getTs() === bMsg.getTs()) { - return 0; - } - - return aMsg.getTs() > bMsg.getTs() ? 1 : -1; - }); -}; - -const fixWidth = (str: string, len: number) => - str.length > len ? `${str.substring(0, len - 1)}\u2026` : str.padEnd(len); - -const printRoomList = () => { - console.log("\nRoom List:"); - - for (let i = 0; i < roomList.length; i++) { - const events = roomList[i].getLiveTimeline().getEvents(); - const msg = events[events.length - 1]; - const dateStr = msg ? new Date(msg.getTs()).toISOString().replace(/T/, " ").replace(/\..+/, "") : "---"; - - const roomName = fixWidth(roomList[i].name, 25); - const memberCount = roomList[i].getJoinedMembers().length; - - console.log(`[${i}] ${roomName} (${memberCount} members) ${dateStr}`); - } -}; - -const printMessages = () => { - if (!viewingRoom) { - printRoomList(); - return; - } - - const events = viewingRoom.getLiveTimeline().getEvents(); - - for (const event of events) { - if (event.getType() !== sdk.EventType.RoomMessage) { - continue; - } - - console.log(event.getContent().body); - } -}; - -const client = await start(); +const client = await start(credentials, { forgetDevices: true }); client.on(sdk.ClientEvent.Room, () => { - setRoomList(client); + roomList = getRoomList(client); if (!viewingRoom) { - printRoomList(); + printRoomList(roomList); } rl.prompt(); @@ -196,12 +39,16 @@ rl.on("line", async (line: string) => { await client.joinRoom(roomList[index].roomId); } - await verifyAll(roomList[index]); + await verifyRoom(client, roomList[index]); viewingRoom = roomList[index]; await client.roomInitialSync(roomList[index].roomId, 20); - printMessages(); + if (viewingRoom) { + printMessages(viewingRoom); + } else { + printRoomList(roomList); + } rl.prompt(); return; @@ -222,7 +69,9 @@ rl.on("line", async (line: string) => { }); client.on(sdk.RoomEvent.Timeline, async(event, room) => { - if (!["m.room.message", "m.room.encrypted"].includes(event.getType())) { + const type = event.getType() as EventType; + + if (![sdk.EventType.RoomMessage, sdk.EventType.RoomMessageEncrypted].includes(type)) { return; } @@ -239,6 +88,6 @@ client.on(sdk.RoomEvent.Timeline, async(event, room) => { }); -setRoomList(client); -printRoomList(); +roomList = getRoomList(client); +printRoomList(roomList); rl.prompt(); From 462b614561a8c114278009fed01cc51a0c61903d Mon Sep 17 00:00:00 2001 From: saul Date: Mon, 8 May 2023 16:42:35 +1200 Subject: [PATCH 15/48] Add command for displaying members. --- examples/crypto-node/src/index.ts | 57 ++++++++++++++++--------------- examples/crypto-node/src/io.ts | 21 ++++++++++++ 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index 71131854f0f..ae3bc76eec5 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -1,5 +1,5 @@ import credentials from "./credentials.js"; -import { rl, printRoomList, printMessages } from "./io.js"; +import { rl, printRoomList, printMessages, printMemberList } from "./io.js"; import { start, verifyRoom, getRoomList } from "./matrix.js"; import sdk from "./matrix-importer.js"; import type { Room, EventType } from "../../../lib/index.js"; @@ -19,6 +19,25 @@ client.on(sdk.ClientEvent.Room, () => { rl.prompt(); }); +client.on(sdk.RoomEvent.Timeline, async(event, room) => { + const type = event.getType() as EventType; + + if (![sdk.EventType.RoomMessage, sdk.EventType.RoomMessageEncrypted].includes(type)) { + return; + } + + if (room != null && room.roomId !== viewingRoom?.roomId) { + return; + } + + await client.decryptEventIfNeeded(event); + + process.stdout.clearLine(-1); + process.stdout.cursorTo(0); + console.log(event.getContent().body); + rl.prompt(); +}); + rl.on("line", async (line: string) => { if (line.trim().length === 0) { rl.prompt(); @@ -54,12 +73,16 @@ rl.on("line", async (line: string) => { return; } } else { - const message = { - msgtype: sdk.MsgType.Text, - body: line - }; - - await client.sendMessage(viewingRoom.roomId, message); + if (line.indexOf("/members") === 0) { + printMemberList(viewingRoom); + } else { + const message = { + msgtype: sdk.MsgType.Text, + body: line + }; + + await client.sendMessage(viewingRoom.roomId, message); + } rl.prompt(); return; } @@ -68,26 +91,6 @@ rl.on("line", async (line: string) => { rl.prompt(); }); -client.on(sdk.RoomEvent.Timeline, async(event, room) => { - const type = event.getType() as EventType; - - if (![sdk.EventType.RoomMessage, sdk.EventType.RoomMessageEncrypted].includes(type)) { - return; - } - - if (room != null && room.roomId !== viewingRoom?.roomId) { - return; - } - - await client.decryptEventIfNeeded(event); - - process.stdout.clearLine(-1); - process.stdout.cursorTo(0); - console.log(event.getContent().body); - rl.prompt(); -}); - - roomList = getRoomList(client); printRoomList(roomList); rl.prompt(); diff --git a/examples/crypto-node/src/io.ts b/examples/crypto-node/src/io.ts index 294585f421e..b61556560ed 100644 --- a/examples/crypto-node/src/io.ts +++ b/examples/crypto-node/src/io.ts @@ -63,3 +63,24 @@ export const printMessages = (room: Room): void => { console.log(event.getContent().body); } }; + +/** + * Print a list of members in the room. + */ +export const printMemberList = (room: Room): void => { + const members = room.getMembers(); + + members.sort((a, b) => a.name === b.name ? 0 : a.name > b.name ? -1 : 1); + + console.log(`Membership list for room "${room.name}"`); + + for (const member of members) { + if (member.membership == null) { + continue; + } + + const membership = fixWidth(member.membership, 10); + + console.log(`${membership} :: ${member.name} (${member.userId})`); + } +} From e093a6c540c1461b17ec014d2aa6a1d5a0edc7eb Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 10:18:16 +1200 Subject: [PATCH 16/48] Refactor prompts. --- examples/crypto-node/src/index.ts | 84 +++++++++++++++---------------- examples/crypto-node/src/io.ts | 17 +++++++ 2 files changed, 58 insertions(+), 43 deletions(-) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index ae3bc76eec5..3760b5a350c 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -1,5 +1,5 @@ import credentials from "./credentials.js"; -import { rl, printRoomList, printMessages, printMemberList } from "./io.js"; +import { rl, prompt, printRoomList, printMessages, printMemberList } from "./io.js"; import { start, verifyRoom, getRoomList } from "./matrix.js"; import sdk from "./matrix-importer.js"; import type { Room, EventType } from "../../../lib/index.js"; @@ -16,7 +16,7 @@ client.on(sdk.ClientEvent.Room, () => { printRoomList(roomList); } - rl.prompt(); + prompt(); }); client.on(sdk.RoomEvent.Timeline, async(event, room) => { @@ -32,65 +32,63 @@ client.on(sdk.RoomEvent.Timeline, async(event, room) => { await client.decryptEventIfNeeded(event); - process.stdout.clearLine(-1); - process.stdout.cursorTo(0); - console.log(event.getContent().body); - rl.prompt(); + prompt(event.getContent().body); }); rl.on("line", async (line: string) => { if (line.trim().length === 0) { - rl.prompt(); + prompt(); return; } - if (viewingRoom == null) { - if (line.indexOf("/join ") === 0) { - const index = line.split(" ")[1]; + if (viewingRoom == null && line.indexOf("/join ") === 0) { + const index = line.split(" ")[1]; - if (roomList[index] == null) { - console.log("invalid room"); - rl.prompt(); - return; - } - - if (roomList[index].getMember(client.getUserId()).membership === sdk.JoinRule.Invite) { - await client.joinRoom(roomList[index].roomId); - } + if (roomList[index] == null) { + prompt("invalid room"); + return; + } - await verifyRoom(client, roomList[index]); + if (roomList[index].getMember(client.getUserId()).membership === sdk.JoinRule.Invite) { + await client.joinRoom(roomList[index].roomId); + } - viewingRoom = roomList[index]; - await client.roomInitialSync(roomList[index].roomId, 20); + await verifyRoom(client, roomList[index]); - if (viewingRoom) { - printMessages(viewingRoom); - } else { - printRoomList(roomList); - } + viewingRoom = roomList[index]; + await client.roomInitialSync(roomList[index].roomId, 20); - rl.prompt(); - return; - } - } else { - if (line.indexOf("/members") === 0) { - printMemberList(viewingRoom); + if (viewingRoom) { + printMessages(viewingRoom); } else { - const message = { - msgtype: sdk.MsgType.Text, - body: line - }; - - await client.sendMessage(viewingRoom.roomId, message); + printRoomList(roomList); } - rl.prompt(); + + prompt(); + return; + } + + if (viewingRoom != null && line.indexOf("/members") === 0) { + printMemberList(viewingRoom); + prompt(); + return; + } + + if (viewingRoom != null) { + const message = { + msgtype: sdk.MsgType.Text, + body: line + }; + + await client.sendMessage(viewingRoom.roomId, message); + + prompt(); return; } - console.log("invalid command"); - rl.prompt(); + prompt("invalid command"); }); roomList = getRoomList(client); printRoomList(roomList); -rl.prompt(); +prompt(); diff --git a/examples/crypto-node/src/io.ts b/examples/crypto-node/src/io.ts index b61556560ed..bb8f1fb7921 100644 --- a/examples/crypto-node/src/io.ts +++ b/examples/crypto-node/src/io.ts @@ -84,3 +84,20 @@ export const printMemberList = (room: Room): void => { console.log(`${membership} :: ${member.name} (${member.userId})`); } } + +/** + * Prompt the user with an optional string preserving input text. + */ +export const prompt = (text?: string): void => { + const cursor = rl.getCursorPos(); + + clearLine(); + + if (text != null) { + console.log(text); + } + + process.stdout.cursorTo(cursor.cols); + + rl.prompt(true); +}; From 8701aef730c0372f58b6d1e32d14f5c98d0619be Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 10:28:26 +1200 Subject: [PATCH 17/48] Remove cli-color types. --- examples/crypto-node/package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/crypto-node/package.json b/examples/crypto-node/package.json index 24ac9cd1438..cbf0ba3c088 100644 --- a/examples/crypto-node/package.json +++ b/examples/crypto-node/package.json @@ -12,8 +12,5 @@ "dependencies": { "@matrix-org/olm": "^3.2.15", "matrix-js-sdk": "file:../.." - }, - "devDependencies": { - "@types/cli-color": "^2.0.2" } } From c38b6e2ee6e90af32408bcd50c404e3d41fd2b77 Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 11:33:26 +1200 Subject: [PATCH 18/48] Print room info and add ability to leave rooms. --- examples/crypto-node/src/index.ts | 15 ++++++++++++- examples/crypto-node/src/io.ts | 37 +++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index 3760b5a350c..9bfa79d74bb 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -1,5 +1,5 @@ import credentials from "./credentials.js"; -import { rl, prompt, printRoomList, printMessages, printMemberList } from "./io.js"; +import { rl, prompt, printRoomList, printMessages, printMemberList, printRoomInfo } from "./io.js"; import { start, verifyRoom, getRoomList } from "./matrix.js"; import sdk from "./matrix-importer.js"; import type { Room, EventType } from "../../../lib/index.js"; @@ -74,6 +74,19 @@ rl.on("line", async (line: string) => { return; } + if (viewingRoom != null && line.indexOf("/roominfo") === 0) { + printRoomInfo(viewingRoom); + prompt(); + return; + } + + if (viewingRoom != null && line.indexOf("/exit") === 0) { + viewingRoom = null; + printRoomList(roomList); + prompt(); + return; + } + if (viewingRoom != null) { const message = { msgtype: sdk.MsgType.Text, diff --git a/examples/crypto-node/src/io.ts b/examples/crypto-node/src/io.ts index bb8f1fb7921..ca2011df2c2 100644 --- a/examples/crypto-node/src/io.ts +++ b/examples/crypto-node/src/io.ts @@ -1,5 +1,5 @@ import readline from "readline"; -import { EventType, Room } from "../../../lib/index.js" +import { Direction, EventType, Room } from "../../../lib/index.js" /** * Setup the line reader. @@ -83,7 +83,40 @@ export const printMemberList = (room: Room): void => { console.log(`${membership} :: ${member.name} (${member.userId})`); } -} +}; + +/** + * Print additional information about a room. + */ +export const printRoomInfo = (room: Room): void => { + const state = room.getLiveTimeline().getState(Direction.Forward); + const eTypeHeader = fixWidth("Event Type(state_key)", 26); + const sendHeader = fixWidth("Sender", 26); + const contentHeader = fixWidth("Content", 26); + + console.log(`${eTypeHeader}|${sendHeader}|${contentHeader}`); + + if (state == null) { + return; + } + + for (const [key, events] of state.events) { + + if (key === EventType.RoomMember) { + continue; + } + + for (const [stateKey, event] of events) { + const postfix = stateKey == null ? "" : `(${stateKey})`; + const typeAndKey = `${key}${postfix}`; + const typeStr = fixWidth(typeAndKey, eTypeHeader.length); + const sendStr = fixWidth(event.getSender() ?? "", sendHeader.length); + const contentStr = fixWidth(JSON.stringify(event.getContent()), 26); + + console.log(`${typeStr}|${sendStr}|${contentStr}`); + } + } +}; /** * Prompt the user with an optional string preserving input text. From f98112dcc05a2860b893908fd60f2fa867a3f10f Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 11:50:30 +1200 Subject: [PATCH 19/48] Add ability to invite users to a room. --- examples/crypto-node/src/index.ts | 14 ++++++++++++++ examples/crypto-node/src/io.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index 9bfa79d74bb..4f9bfcb4bf3 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -68,6 +68,20 @@ rl.on("line", async (line: string) => { return; } + if (viewingRoom != null && line.indexOf("/invite ") === 0) { + const userId = line.split(" ")[1].trim(); + + try { + await client.invite(viewingRoom.roomId, userId); + + prompt(); + } catch (error) { + prompt(`/invite Error: ${error}`); + } + + return; + } + if (viewingRoom != null && line.indexOf("/members") === 0) { printMemberList(viewingRoom); prompt(); diff --git a/examples/crypto-node/src/io.ts b/examples/crypto-node/src/io.ts index ca2011df2c2..0bddf54efd3 100644 --- a/examples/crypto-node/src/io.ts +++ b/examples/crypto-node/src/io.ts @@ -107,7 +107,7 @@ export const printRoomInfo = (room: Room): void => { } for (const [stateKey, event] of events) { - const postfix = stateKey == null ? "" : `(${stateKey})`; + const postfix = stateKey.length < 1 ? "" : `(${stateKey})`; const typeAndKey = `${key}${postfix}`; const typeStr = fixWidth(typeAndKey, eTypeHeader.length); const sendStr = fixWidth(event.getSender() ?? "", sendHeader.length); From fd7a393ee604893d264af9ea21c48a4f1daef0b3 Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 12:01:59 +1200 Subject: [PATCH 20/48] Move clear devices to separate command. --- examples/crypto-node/src/index.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index 4f9bfcb4bf3..3b549eb83e8 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -1,13 +1,13 @@ import credentials from "./credentials.js"; import { rl, prompt, printRoomList, printMessages, printMemberList, printRoomInfo } from "./io.js"; -import { start, verifyRoom, getRoomList } from "./matrix.js"; +import { start, verifyRoom, getRoomList, clearDevices } from "./matrix.js"; import sdk from "./matrix-importer.js"; import type { Room, EventType } from "../../../lib/index.js"; let roomList: Room[] = []; let viewingRoom: Room | null = null; -const client = await start(credentials, { forgetDevices: true }); +const client = await start(credentials); client.on(sdk.ClientEvent.Room, () => { roomList = getRoomList(client); @@ -41,6 +41,12 @@ rl.on("line", async (line: string) => { return; } + if (line.indexOf("/cleardevices") === 0) { + await clearDevices(client); + prompt(); + return; + } + if (viewingRoom == null && line.indexOf("/join ") === 0) { const index = line.split(" ")[1]; From d6f1c57d11456047a9c664ca7e97c165fea43cd9 Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 12:40:52 +1200 Subject: [PATCH 21/48] Refactor how commands are handled. --- examples/crypto-node/src/index.ts | 121 ++++++++++++++---------------- examples/crypto-node/src/io.ts | 66 +++++++++++----- 2 files changed, 106 insertions(+), 81 deletions(-) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index 3b549eb83e8..b4daa0666e9 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -1,5 +1,5 @@ import credentials from "./credentials.js"; -import { rl, prompt, printRoomList, printMessages, printMemberList, printRoomInfo } from "./io.js"; +import { prompt, printRoomList, printMessages, printMemberList, printRoomInfo, addCommand } from "./io.js"; import { start, verifyRoom, getRoomList, clearDevices } from "./matrix.js"; import sdk from "./matrix-importer.js"; import type { Room, EventType } from "../../../lib/index.js"; @@ -35,91 +35,86 @@ client.on(sdk.RoomEvent.Timeline, async(event, room) => { prompt(event.getContent().body); }); -rl.on("line", async (line: string) => { - if (line.trim().length === 0) { - prompt(); - return; - } +addCommand("/cleardevices", async () => { + await clearDevices(client); +}); - if (line.indexOf("/cleardevices") === 0) { - await clearDevices(client); - prompt(); - return; +addCommand("/join", async (index) => { + if (viewingRoom != null) { + return "You must first exit your current room."; } - if (viewingRoom == null && line.indexOf("/join ") === 0) { - const index = line.split(" ")[1]; + viewingRoom = roomList[index]; - if (roomList[index] == null) { - prompt("invalid room"); - return; - } + if (viewingRoom == null) { + return "Invalid Room."; + } - if (roomList[index].getMember(client.getUserId()).membership === sdk.JoinRule.Invite) { - await client.joinRoom(roomList[index].roomId); - } + if (viewingRoom.getMember(client.getUserId() ?? "")?.membership === sdk.JoinRule.Invite) { + await client.joinRoom(viewingRoom.roomId); + } - await verifyRoom(client, roomList[index]); + await verifyRoom(client, viewingRoom); + await client.roomInitialSync(viewingRoom.roomId, 20); - viewingRoom = roomList[index]; - await client.roomInitialSync(roomList[index].roomId, 20); + printMessages(viewingRoom); +}); - if (viewingRoom) { - printMessages(viewingRoom); - } else { - printRoomList(roomList); - } +addCommand("/exit", () => { + viewingRoom = null; + printRoomList(roomList); +}); - prompt(); - return; +addCommand("/invite", async (userId) => { + if (viewingRoom == null) { + return "You must first join a room."; } - if (viewingRoom != null && line.indexOf("/invite ") === 0) { - const userId = line.split(" ")[1].trim(); + try { + await client.invite(viewingRoom.roomId, userId); + } catch (error) { + return `/invite Error: ${error}`; + } +}); - try { - await client.invite(viewingRoom.roomId, userId); +addCommand("/members", async () => { + if (viewingRoom == null) { + return "You must first join a room."; + } - prompt(); - } catch (error) { - prompt(`/invite Error: ${error}`); - } + printMemberList(viewingRoom); +}); - return; +addCommand("/roominfo", async () => { + if (viewingRoom == null) { + return "You must first join a room."; } - if (viewingRoom != null && line.indexOf("/members") === 0) { - printMemberList(viewingRoom); - prompt(); - return; - } + printMemberList(viewingRoom); +}); - if (viewingRoom != null && line.indexOf("/roominfo") === 0) { - printRoomInfo(viewingRoom); - prompt(); - return; +addCommand("/roominfo", async () => { + if (viewingRoom == null) { + return "You must first join a room."; } - if (viewingRoom != null && line.indexOf("/exit") === 0) { - viewingRoom = null; - printRoomList(roomList); - prompt(); - return; - } + printRoomInfo(viewingRoom); +}); - if (viewingRoom != null) { - const message = { - msgtype: sdk.MsgType.Text, - body: line - }; +addCommand("/send", async (...tokens) => { + if (viewingRoom == null) { + return "You must first join a room."; + } - await client.sendMessage(viewingRoom.roomId, message); + console.log(tokens); + console.log(tokens.join(" ")); - prompt(); - return; - } + const message = { + msgtype: sdk.MsgType.Text, + body: tokens.join(" ") + }; - prompt("invalid command"); + await client.sendMessage(viewingRoom.roomId, message); }); roomList = getRoomList(client); diff --git a/examples/crypto-node/src/io.ts b/examples/crypto-node/src/io.ts index 0bddf54efd3..745765e3e66 100644 --- a/examples/crypto-node/src/io.ts +++ b/examples/crypto-node/src/io.ts @@ -1,10 +1,12 @@ import readline from "readline"; import { Direction, EventType, Room } from "../../../lib/index.js" +export type Command = (...args: string[]) => Promise | string | void + /** * Setup the line reader. */ -export const rl = readline.createInterface({ +const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); @@ -19,6 +21,51 @@ export const clearLine = (): void => { process.stdout.cursorTo(0); }; +const commands = new Map(); + +rl.on("line", async line => { + for (const [command, method] of commands.entries()) { + if (line.indexOf(command) === 0) { + const args = line.split(" "); + + args.shift(); + + const result = await method(...args); + + // Result can be void so we need to use this nullish coalescing operator + // to convert it to undefined. + prompt(result ?? undefined); + return; + } + } + + prompt("Invalid command."); +}); + +/** + * Prompt the user with an optional string preserving input text. + */ +export const prompt = (text?: string): void => { + const cursor = rl.getCursorPos(); + + clearLine(); + + if (text != null) { + console.log(text); + } + + process.stdout.cursorTo(cursor.cols); + + rl.prompt(true); +}; + +/** + * Add a command to execute when the user sends input. + */ +export const addCommand = (command: string, method: Command): void => { + commands.set(command, method); +}; + /** * Fix a string to a specific width. */ @@ -117,20 +164,3 @@ export const printRoomInfo = (room: Room): void => { } } }; - -/** - * Prompt the user with an optional string preserving input text. - */ -export const prompt = (text?: string): void => { - const cursor = rl.getCursorPos(); - - clearLine(); - - if (text != null) { - console.log(text); - } - - process.stdout.cursorTo(cursor.cols); - - rl.prompt(true); -}; From 8e2231f1ae96c73cf05ac9b000e9bb5127e1568c Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 12:42:58 +1200 Subject: [PATCH 22/48] Add quit command. --- examples/crypto-node/src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index b4daa0666e9..95f8f9db31b 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -35,6 +35,10 @@ client.on(sdk.RoomEvent.Timeline, async(event, room) => { prompt(event.getContent().body); }); +addCommand("/quit", () => { + process.exit(); +}); + addCommand("/cleardevices", async () => { await clearDevices(client); }); From eb86d57c0ebf2ceb187b0e5b9ca84a1355e5866c Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 12:54:55 +1200 Subject: [PATCH 23/48] Add auto completions. --- examples/crypto-node/src/io.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/examples/crypto-node/src/io.ts b/examples/crypto-node/src/io.ts index 745765e3e66..498fdab7f7a 100644 --- a/examples/crypto-node/src/io.ts +++ b/examples/crypto-node/src/io.ts @@ -3,12 +3,25 @@ import { Direction, EventType, Room } from "../../../lib/index.js" export type Command = (...args: string[]) => Promise | string | void +/** + * A map for holding all the added commands and methods. + */ +const commands = new Map(); + /** * Setup the line reader. */ const rl = readline.createInterface({ input: process.stdin, - output: process.stdout + output: process.stdout, + completer: (line: string) => { + const hits = [...commands.keys()].filter(function (c) { + return c.indexOf(line) == 0; + }); + + // show all completions if none found + return [hits.length ? hits : [...commands.keys()], line]; + } }); rl.setPrompt("$ "); @@ -21,8 +34,6 @@ export const clearLine = (): void => { process.stdout.cursorTo(0); }; -const commands = new Map(); - rl.on("line", async line => { for (const [command, method] of commands.entries()) { if (line.indexOf(command) === 0) { From a1e92a560d047dac771869793ccc5c5363e31fc7 Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 13:15:07 +1200 Subject: [PATCH 24/48] Add help command. --- examples/crypto-node/src/index.ts | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index 95f8f9db31b..69575fb1a8a 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -1,5 +1,13 @@ import credentials from "./credentials.js"; -import { prompt, printRoomList, printMessages, printMemberList, printRoomInfo, addCommand } from "./io.js"; +import { + prompt, + fixWidth, + printRoomList, + printMessages, + printMemberList, + printRoomInfo, + addCommand +} from "./io.js"; import { start, verifyRoom, getRoomList, clearDevices } from "./matrix.js"; import sdk from "./matrix-importer.js"; import type { Room, EventType } from "../../../lib/index.js"; @@ -35,6 +43,27 @@ client.on(sdk.RoomEvent.Timeline, async(event, room) => { prompt(event.getContent().body); }); +addCommand("/help", () => { + const displayCommand = (command: string, description: string) => { + console.log(` ${fixWidth(command, 20)} : ${description}`); + }; + + console.log("Global commands:"); + displayCommand("/help", "Show this help."); + displayCommand("/quit", "Quit the program."); + displayCommand("/cleardevices", "Clear all other devices from this account."); + + console.log("Room list index commands:"); + displayCommand("/join ", "Join a room, e.g. '/join 5'"); + + console.log("Room commands:"); + displayCommand("/exit", "Return to the room list index."); + displayCommand("/send ", "Send a message to the room, e.g. '/send Hello World.'"); + displayCommand("/members", "Show the room member list."); + displayCommand("/invite @foo:bar", "Invite @foo:bar to the room."); + displayCommand("/roominfo", "Display room info e.g. name, topic."); +}); + addCommand("/quit", () => { process.exit(); }); From 49bcb45f31abb7461006ab6574624dcbbe07bfb4 Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 13:35:37 +1200 Subject: [PATCH 25/48] Improve message display. --- examples/crypto-node/src/index.ts | 4 +++- examples/crypto-node/src/io.ts | 33 +++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index 69575fb1a8a..2ecd0b81a71 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -3,6 +3,7 @@ import { prompt, fixWidth, printRoomList, + printMessage, printMessages, printMemberList, printRoomInfo, @@ -40,7 +41,8 @@ client.on(sdk.RoomEvent.Timeline, async(event, room) => { await client.decryptEventIfNeeded(event); - prompt(event.getContent().body); + printMessage(event); + prompt(); }); addCommand("/help", () => { diff --git a/examples/crypto-node/src/io.ts b/examples/crypto-node/src/io.ts index 498fdab7f7a..b8016490c57 100644 --- a/examples/crypto-node/src/io.ts +++ b/examples/crypto-node/src/io.ts @@ -1,5 +1,5 @@ import readline from "readline"; -import { Direction, EventType, Room } from "../../../lib/index.js" +import { Direction, EventType, Room, MatrixEvent } from "../../../lib/index.js" export type Command = (...args: string[]) => Promise | string | void @@ -118,7 +118,7 @@ export const printMessages = (room: Room): void => { continue; } - console.log(event.getContent().body); + printMessage(event); } }; @@ -175,3 +175,32 @@ export const printRoomInfo = (room: Room): void => { } } }; + +/** + * Print a message with nice formatting. + */ +export const printMessage = (event: MatrixEvent) => { + const name = fixWidth(event.sender ? event.sender.name : event.getSender() ?? "", 30); + const time = tsToDateString(event.getTs()); + + let content: string; + + if (event.getType() === EventType.RoomMessage) { + content = event.getContent().body; + } else if (event.isState()) { + const stateKey = event.getStateKey(); + const postfix = stateKey == null || stateKey.length < 1 ? "" : ` (${stateKey})`; + const stateName = `${event.getType()}${postfix}`; + + content = `[State: ${stateName} updated to: ${JSON.stringify(event.getContent())}]`; + } else { + // random message event + content = `[Message: ${event.getType()} Content: ${JSON.stringify(event.getContent())}]`; + } + + console.log(`[${time}] ${name}`); + + for (const line of content.split("\n")) { + console.log(` ${line}`); + } +} From 782bc69ddb7d36615edfe65d16d561d08ae26376 Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 13:47:49 +1200 Subject: [PATCH 26/48] Document the index file. --- examples/crypto-node/src/index.ts | 96 +++++++++++++++++++++++++----- examples/crypto-node/src/matrix.ts | 4 +- 2 files changed, 83 insertions(+), 17 deletions(-) diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index 2ecd0b81a71..1a220c57ed0 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -1,4 +1,16 @@ +/** + * This file glues the matrix helper methods in './matrix.ts' with the IO helper + * methods in './io.ts' together to create a simple CLI. + */ + +/** + * Import the user's credentials. + */ import credentials from "./credentials.js"; + +/** + * Import out IO helper methods. + */ import { prompt, fixWidth, @@ -9,16 +21,33 @@ import { printRoomInfo, addCommand } from "./io.js"; + +/** + * Import our matrix helper methods. + */ import { start, verifyRoom, getRoomList, clearDevices } from "./matrix.js"; -import sdk from "./matrix-importer.js"; -import type { Room, EventType } from "../../../lib/index.js"; +/** + * Import the types and enums from matrix-js-sdk. + */ +import { ClientEvent, RoomEvent, EventType, JoinRule, MsgType } from "../../../lib/index.js" +import type { Room } from "../../../lib/index.js"; + +/** + * Global state for keeping track of rooms. + */ let roomList: Room[] = []; let viewingRoom: Room | null = null; +/** + * Create our matrix client. + */ const client = await start(credentials); -client.on(sdk.ClientEvent.Room, () => { +/** + * When a room is added or removed update the room list. + */ +client.on(ClientEvent.Room, () => { roomList = getRoomList(client); if (!viewingRoom) { @@ -28,10 +57,13 @@ client.on(sdk.ClientEvent.Room, () => { prompt(); }); -client.on(sdk.RoomEvent.Timeline, async(event, room) => { +/** + * When we receive a message, check if we are in that room and if so display it. + */ +client.on(RoomEvent.Timeline, async(event, room) => { const type = event.getType() as EventType; - if (![sdk.EventType.RoomMessage, sdk.EventType.RoomMessageEncrypted].includes(type)) { + if (![EventType.RoomMessage, EventType.RoomMessageEncrypted].includes(type)) { return; } @@ -45,6 +77,13 @@ client.on(sdk.RoomEvent.Timeline, async(event, room) => { prompt(); }); +/** + * Below is all of the possible commands and definitions. + */ + +/** + * Basic help command, displays the possible commands. + */ addCommand("/help", () => { const displayCommand = (command: string, description: string) => { console.log(` ${fixWidth(command, 20)} : ${description}`); @@ -66,14 +105,23 @@ addCommand("/help", () => { displayCommand("/roominfo", "Display room info e.g. name, topic."); }); +/** + * Quit command for quitting the program. + */ addCommand("/quit", () => { process.exit(); }); +/** + * Clear devices command for removing all other devices from the users account. + */ addCommand("/cleardevices", async () => { await clearDevices(client); }); +/** + * Join room command for joining a room from the room index. + */ addCommand("/join", async (index) => { if (viewingRoom != null) { return "You must first exit your current room."; @@ -85,7 +133,7 @@ addCommand("/join", async (index) => { return "Invalid Room."; } - if (viewingRoom.getMember(client.getUserId() ?? "")?.membership === sdk.JoinRule.Invite) { + if (viewingRoom.getMember(client.getUserId() ?? "")?.membership === JoinRule.Invite) { await client.joinRoom(viewingRoom.roomId); } @@ -95,11 +143,17 @@ addCommand("/join", async (index) => { printMessages(viewingRoom); }); +/** + * Exit command for exiting a joined room. + */ addCommand("/exit", () => { viewingRoom = null; printRoomList(roomList); }); +/** + * Invite command for inviting a user to the current room. + */ addCommand("/invite", async (userId) => { if (viewingRoom == null) { return "You must first join a room."; @@ -112,6 +166,9 @@ addCommand("/invite", async (userId) => { } }); +/** + * Members command, displays the list of members in the current room. + */ addCommand("/members", async () => { if (viewingRoom == null) { return "You must first join a room."; @@ -120,14 +177,9 @@ addCommand("/members", async () => { printMemberList(viewingRoom); }); -addCommand("/roominfo", async () => { - if (viewingRoom == null) { - return "You must first join a room."; - } - - printMemberList(viewingRoom); -}); - +/** + * Members command, displays the information about the current room. + */ addCommand("/roominfo", async () => { if (viewingRoom == null) { return "You must first join a room."; @@ -136,6 +188,9 @@ addCommand("/roominfo", async () => { printRoomInfo(viewingRoom); }); +/** + * Send command for allowing the user to send messages in the current room. + */ addCommand("/send", async (...tokens) => { if (viewingRoom == null) { return "You must first join a room."; @@ -145,13 +200,24 @@ addCommand("/send", async (...tokens) => { console.log(tokens.join(" ")); const message = { - msgtype: sdk.MsgType.Text, + msgtype: MsgType.Text, body: tokens.join(" ") }; await client.sendMessage(viewingRoom.roomId, message); }); +/** + * Initialize the room list. + */ roomList = getRoomList(client); + +/** + * Print the list of rooms. + */ printRoomList(roomList); + +/** + * Request the first input from the user. + */ prompt(); diff --git a/examples/crypto-node/src/matrix.ts b/examples/crypto-node/src/matrix.ts index d08d27abd85..6522647a790 100644 --- a/examples/crypto-node/src/matrix.ts +++ b/examples/crypto-node/src/matrix.ts @@ -4,7 +4,7 @@ */ import sdk from "./matrix-importer.js"; -import type { MatrixClient, Room } from "../../../lib/index.js"; +import type { ICreateClientOpts, MatrixClient, Room } from "../../../lib/index.js"; /** * This interface provides the details needed to perform a password login. @@ -28,7 +28,7 @@ export interface TokenLogin { /** * Create a matrix client using a token login. */ -export const startWithToken = async (tokenLogin: TokenLogin | sdk.ICreateClientOpts): Promise => { +export const startWithToken = async (tokenLogin: TokenLogin | ICreateClientOpts): Promise => { const client = sdk.createClient(tokenLogin); // We must initialize the crypto before starting the client. From aa9d57c24a58337c8e8ab33dd178010e47bc3060 Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 14:32:31 +1200 Subject: [PATCH 27/48] Rework how credentials are loaded. --- examples/crypto-node/.gitignore | 2 +- examples/crypto-node/src/index.ts | 13 +++++++++---- examples/crypto-node/src/io.ts | 24 ++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/examples/crypto-node/.gitignore b/examples/crypto-node/.gitignore index 065e5330b54..83f6e39715d 100644 --- a/examples/crypto-node/.gitignore +++ b/examples/crypto-node/.gitignore @@ -1 +1 @@ -src/credentials.ts +credentials.json diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index 1a220c57ed0..fa540196ca2 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -3,15 +3,14 @@ * methods in './io.ts' together to create a simple CLI. */ -/** - * Import the user's credentials. - */ -import credentials from "./credentials.js"; +import Path from "path"; +import { fileURLToPath } from 'url'; /** * Import out IO helper methods. */ import { + readCredentials, prompt, fixWidth, printRoomList, @@ -39,6 +38,12 @@ import type { Room } from "../../../lib/index.js"; let roomList: Room[] = []; let viewingRoom: Room | null = null; +/** +* Import the user's credentials. +*/ +const dirname = Path.dirname(fileURLToPath(import.meta.url)); +const credentials = await readCredentials(Path.join(dirname, "../credentials.json")); + /** * Create our matrix client. */ diff --git a/examples/crypto-node/src/io.ts b/examples/crypto-node/src/io.ts index b8016490c57..fdceaab769c 100644 --- a/examples/crypto-node/src/io.ts +++ b/examples/crypto-node/src/io.ts @@ -1,5 +1,7 @@ import readline from "readline"; import { Direction, EventType, Room, MatrixEvent } from "../../../lib/index.js" +import fs from "fs/promises"; +import type { PasswordLogin } from "./matrix.js"; export type Command = (...args: string[]) => Promise | string | void @@ -204,3 +206,25 @@ export const printMessage = (event: MatrixEvent) => { console.log(` ${line}`); } } + +/** + * Read login credentials from a JSON file. + */ +export const readCredentials = async (path: string): Promise => { + const text = await fs.readFile(path, { encoding: "utf8" }); + const json = JSON.parse(text); + + if (json.userId == null) { + throw new Error("userId field is required"); + } + + if (json.password == null) { + throw new Error("password field is required"); + } + + if (json.baseUrl == null) { + json.baseUrl = "https://matrix.org"; + } + + return json; +} From f7727229712a70ecb1c1d683203ba07289d5991a Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 14:45:45 +1200 Subject: [PATCH 28/48] Add credentials template. --- examples/crypto-node/credentials.template.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 examples/crypto-node/credentials.template.json diff --git a/examples/crypto-node/credentials.template.json b/examples/crypto-node/credentials.template.json new file mode 100644 index 00000000000..9bdb931ca76 --- /dev/null +++ b/examples/crypto-node/credentials.template.json @@ -0,0 +1,5 @@ +{ + "userId": "@my_user:matrix.org", + "password": "my_password", + "baseUrl": "https://matrix.org" +} From be68e7b422091619f0600da6d5e01ad4d5a8ab67 Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 16:04:21 +1200 Subject: [PATCH 29/48] Fix matrix lib path and handle building with tsc. --- examples/crypto-node/.gitignore | 1 + examples/crypto-node/package.json | 33 ++++++++++++--------- examples/crypto-node/src/index.ts | 4 +-- examples/crypto-node/src/io.ts | 2 +- examples/crypto-node/src/matrix-importer.ts | 2 +- examples/crypto-node/src/matrix.ts | 2 +- examples/crypto-node/tsconfig.json | 6 ++-- 7 files changed, 29 insertions(+), 21 deletions(-) diff --git a/examples/crypto-node/.gitignore b/examples/crypto-node/.gitignore index 83f6e39715d..13fa6ec0219 100644 --- a/examples/crypto-node/.gitignore +++ b/examples/crypto-node/.gitignore @@ -1 +1,2 @@ credentials.json +dist diff --git a/examples/crypto-node/package.json b/examples/crypto-node/package.json index cbf0ba3c088..8f0d12107ab 100644 --- a/examples/crypto-node/package.json +++ b/examples/crypto-node/package.json @@ -1,16 +1,21 @@ { - "name": "example-app", - "type": "module", - "version": "0.0.0", - "description": "", - "main": "src/index.js", - "scripts": { - "preinstall": "npm install ../.." - }, - "author": "", - "license": "Apache 2.0", - "dependencies": { - "@matrix-org/olm": "^3.2.15", - "matrix-js-sdk": "file:../.." - } + "name": "example-app", + "type": "module", + "version": "0.0.0", + "description": "", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "preinstall": "npm install ../..", + "build": "tsc -b" + }, + "author": "", + "license": "Apache 2.0", + "dependencies": { + "@matrix-org/olm": "^3.2.15", + "matrix-js-sdk": "file:../.." + }, + "devDependencies": { + "typescript": "^5.0.4" + } } diff --git a/examples/crypto-node/src/index.ts b/examples/crypto-node/src/index.ts index fa540196ca2..fef43bbc600 100644 --- a/examples/crypto-node/src/index.ts +++ b/examples/crypto-node/src/index.ts @@ -29,8 +29,8 @@ import { start, verifyRoom, getRoomList, clearDevices } from "./matrix.js"; /** * Import the types and enums from matrix-js-sdk. */ -import { ClientEvent, RoomEvent, EventType, JoinRule, MsgType } from "../../../lib/index.js" -import type { Room } from "../../../lib/index.js"; +import { ClientEvent, RoomEvent, EventType, JoinRule, MsgType } from "matrix-js-sdk" +import type { Room } from "matrix-js-sdk"; /** * Global state for keeping track of rooms. diff --git a/examples/crypto-node/src/io.ts b/examples/crypto-node/src/io.ts index fdceaab769c..5cb112299ba 100644 --- a/examples/crypto-node/src/io.ts +++ b/examples/crypto-node/src/io.ts @@ -1,5 +1,5 @@ import readline from "readline"; -import { Direction, EventType, Room, MatrixEvent } from "../../../lib/index.js" +import { Direction, EventType, Room, MatrixEvent } from "matrix-js-sdk" import fs from "fs/promises"; import type { PasswordLogin } from "./matrix.js"; diff --git a/examples/crypto-node/src/matrix-importer.ts b/examples/crypto-node/src/matrix-importer.ts index de04ffcd237..df3d0609d20 100644 --- a/examples/crypto-node/src/matrix-importer.ts +++ b/examples/crypto-node/src/matrix-importer.ts @@ -45,6 +45,6 @@ logger.setLevel(5); /** * Now we can import and export the matrix sdk. */ -import * as sdk from "../../../lib/index.js"; +import * as sdk from "matrix-js-sdk"; export default sdk; diff --git a/examples/crypto-node/src/matrix.ts b/examples/crypto-node/src/matrix.ts index 6522647a790..9181e0b1344 100644 --- a/examples/crypto-node/src/matrix.ts +++ b/examples/crypto-node/src/matrix.ts @@ -4,7 +4,7 @@ */ import sdk from "./matrix-importer.js"; -import type { ICreateClientOpts, MatrixClient, Room } from "../../../lib/index.js"; +import type { ICreateClientOpts, MatrixClient, Room } from "matrix-js-sdk"; /** * This interface provides the details needed to perform a password login. diff --git a/examples/crypto-node/tsconfig.json b/examples/crypto-node/tsconfig.json index 0fefd5e2840..e581061436a 100644 --- a/examples/crypto-node/tsconfig.json +++ b/examples/crypto-node/tsconfig.json @@ -10,12 +10,14 @@ "strictNullChecks": true, "noEmit": true, "declaration": true, - "strict": true + "strict": true, + "outDir": "./dist" }, "ts-node": { "esm": true }, - "include": ["./src/**/*.ts"] + "include": ["./src/**/*.ts"], + "exclude": ["node_modules"] } From 016b041f086b966ae2137b3a2d3998ab01ea0932 Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 16:09:25 +1200 Subject: [PATCH 30/48] Change filter to arrow function. --- examples/crypto-node/src/io.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/crypto-node/src/io.ts b/examples/crypto-node/src/io.ts index 5cb112299ba..d08baddbed7 100644 --- a/examples/crypto-node/src/io.ts +++ b/examples/crypto-node/src/io.ts @@ -17,7 +17,7 @@ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, completer: (line: string) => { - const hits = [...commands.keys()].filter(function (c) { + const hits = [...commands.keys()].filter(c => { return c.indexOf(line) == 0; }); From a98ade5f355e3460678fb1ea9e7c1467f6f1388d Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 16:22:47 +1200 Subject: [PATCH 31/48] Change build dir to lib. --- examples/crypto-node/.gitignore | 2 +- examples/crypto-node/tsconfig.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/crypto-node/.gitignore b/examples/crypto-node/.gitignore index 13fa6ec0219..1d12754aaa5 100644 --- a/examples/crypto-node/.gitignore +++ b/examples/crypto-node/.gitignore @@ -1,2 +1,2 @@ credentials.json -dist +lib diff --git a/examples/crypto-node/tsconfig.json b/examples/crypto-node/tsconfig.json index e581061436a..3ee7a73bf97 100644 --- a/examples/crypto-node/tsconfig.json +++ b/examples/crypto-node/tsconfig.json @@ -8,10 +8,9 @@ "noImplicitAny": false, "noImplicitThis": true, "strictNullChecks": true, - "noEmit": true, "declaration": true, "strict": true, - "outDir": "./dist" + "outDir": "./lib" }, "ts-node": { From 3ab577485ea0dbd28f7a536b3b7afcdbade7f7a8 Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 16:27:07 +1200 Subject: [PATCH 32/48] Add readme. --- examples/crypto-node/README.md | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 examples/crypto-node/README.md diff --git a/examples/crypto-node/README.md b/examples/crypto-node/README.md new file mode 100644 index 00000000000..7e3075018c4 --- /dev/null +++ b/examples/crypto-node/README.md @@ -0,0 +1,43 @@ +This is a functional terminal app which allows you to see the room list for a user, join rooms, send messages and view room membership lists. + + +## Install + +To try it out, you will need to create a credentials file: `crednetials.json` in the root of this example folder and configure it for your `homeserver`, `access_token` and `user_id` like so: + +```json +{ + "userId": "@my_user:matrix.org", + "password": "my_password", + "baseUrl": "https://matrix.org" +} +``` + +You may also copy `credentials.template.json` to `credentials.json` and just edit the fields. + +You then can install dependencies and build the example. +``` + $ npm install + $ npm run build +``` + +## Usage +You can run the exmaple by running the following command: + +``` +$ node lib/index.js +``` + +Once it starts up you can list commands by typing: + +``` +/help +``` + +If you have trouble with encryption errors cause by old devices you can delete them all by running: + +``` +/cleardevices +``` + +This will delete all the devices on the account (except for the current one) so be careful if you have devices you do not wish to lose. From 1f1b1cfe63fae7457a6ba4ca726d4d196d3e8f9e Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 16:27:57 +1200 Subject: [PATCH 33/48] Add header to first section. --- examples/crypto-node/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/crypto-node/README.md b/examples/crypto-node/README.md index 7e3075018c4..14f8f6907dd 100644 --- a/examples/crypto-node/README.md +++ b/examples/crypto-node/README.md @@ -1,5 +1,6 @@ -This is a functional terminal app which allows you to see the room list for a user, join rooms, send messages and view room membership lists. +## About +This is a functional terminal app which allows you to see the room list for a user, join rooms, send messages and view room membership lists with E2EE enabled. ## Install From d0d76b4b5a4cd3642c11d8e59ecce996dac3adcb Mon Sep 17 00:00:00 2001 From: saul Date: Tue, 9 May 2023 16:45:39 +1200 Subject: [PATCH 34/48] Revert "Update types to match spec (#3330)" This reverts commit 6b7c47e0dfd09167a991691e0a6b70bbe9c40f7b. --- src/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index f21743bdb25..cb8df7ae47f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -635,7 +635,7 @@ interface IJoinRequestBody { interface ITagMetadata { [key: string]: any; - order?: number; + order: number; } interface IMessagesResponse { @@ -4179,7 +4179,7 @@ export class MatrixClient extends TypedEventEmitter { + public setRoomTag(roomId: string, tagName: string, metadata: ITagMetadata): Promise<{}> { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { $userId: this.credentials.userId!, $roomId: roomId, From 4480159c019d88ce64f2076994ca9f0030fa124d Mon Sep 17 00:00:00 2001 From: Saul Date: Wed, 10 May 2023 08:23:45 +1200 Subject: [PATCH 35/48] Fix readme typo. Co-authored-by: David Baker --- examples/crypto-node/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/crypto-node/README.md b/examples/crypto-node/README.md index 14f8f6907dd..dd3e076b0aa 100644 --- a/examples/crypto-node/README.md +++ b/examples/crypto-node/README.md @@ -4,7 +4,7 @@ This is a functional terminal app which allows you to see the room list for a us ## Install -To try it out, you will need to create a credentials file: `crednetials.json` in the root of this example folder and configure it for your `homeserver`, `access_token` and `user_id` like so: +To try it out, you will need to create a credentials file: `credentials.json` in the root of this example folder and configure it for your `homeserver`, `access_token` and `user_id` like so: ```json { From 6db024800a11000d2430d657c0e762fc5278dada Mon Sep 17 00:00:00 2001 From: saul Date: Wed, 10 May 2023 08:34:12 +1200 Subject: [PATCH 36/48] Remove forget devices options from start. --- examples/crypto-node/src/matrix.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/examples/crypto-node/src/matrix.ts b/examples/crypto-node/src/matrix.ts index 9181e0b1344..39acb129d63 100644 --- a/examples/crypto-node/src/matrix.ts +++ b/examples/crypto-node/src/matrix.ts @@ -89,18 +89,13 @@ export const clearDevices = async (client: MatrixClient) => { /** * Start the client with a password login. */ -export const start = async (passwordLogin: PasswordLogin, options?: { forgetDevices?: boolean }): Promise => { +export const start = async (passwordLogin: PasswordLogin): Promise => { // Get the token login details. const tokenLogin = await getTokenLogin(passwordLogin); // Start the client with the token. const client = await startWithToken(tokenLogin); - // Clear other devices - this can help resolve olm session issues. - if (options?.forgetDevices) { - await clearDevices(client); - } - return client; } From c69f6af8c2703211c343e4c0664a65847a4c720e Mon Sep 17 00:00:00 2001 From: saul Date: Wed, 10 May 2023 12:09:46 +1200 Subject: [PATCH 37/48] Expand the readme with limitations and structure. --- examples/crypto-node/README.md | 55 +++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/examples/crypto-node/README.md b/examples/crypto-node/README.md index dd3e076b0aa..a25665fa605 100644 --- a/examples/crypto-node/README.md +++ b/examples/crypto-node/README.md @@ -35,10 +35,63 @@ Once it starts up you can list commands by typing: /help ``` -If you have trouble with encryption errors cause by old devices you can delete them all by running: +If you have trouble with encryption errors cause by old devices with broken olm sessions you can delete them all by running: ``` /cleardevices ``` This will delete all the devices on the account (except for the current one) so be careful if you have devices you do not wish to lose. + +## Limitations + +This example does not provide any way of verifying your sessions, so on some clients, users in the room will get a warning that someone is using an unverified session. + +This example does not provide any persistent storage so encryption keys for the device it creates are not persisted. This means every time the client starts it generates a new unverified device which is inaccessable when the program exits. + +If you want to persist data you will need to overwrite the default memory stores with stores that save data with a IndexedDB implementation: + +```javascript +import sqlite3 from "sqlite3"; +import indexeddbjs from "indexeddb-js"; + +const engine = new sqlite3.Database("./sqlite"); +const { indexedDB } = indexeddbjs.makeScope("sqlite3", engine); + +sdk.createClient({ + baseUrl: "https://matrix.org", + userId: "@my_user:matrix.org", + accessToken: "my_access_token", + deviceId: "my_device_id", + store: new sdk.IndexedDBStore({ indexedDB }), + cryptoStore: new sdk.IndexedDBCryptoStore(indexedDB, "crypto") +}); +``` + +Alternatively you could create your own store implementation using whatever backend storage you want. + +## Structure + +The structure of this example has been split into separate files that deal with specific logic. + +If you want to know how to import the Matrix SDK, have a look at `matrix-importer.ts`. If you want to know how to use the Matrix SDK, take a look at `matrix.ts`. If you want to know how to read the state, the `io.ts` file has a few related methods for things like printing rooms or messages. Finally the `index.ts` file glues a lot of these methods together to turn it into a small Matrix messaging client. + +### matrix-importer.ts + +This file is responsible for setting up the globals needed to enable E2EE on Matrix and importing the Matrix SDK correctly. This file then exports the Matrix SDK for ease of use. + +### matrix.ts + +This file provides a few methods to assist with certain actions through the Matrix SDK, such as logging in, verifying devices, clearing devices and getting rooms. + +* `getTokenLogin` - This method logs in via password to obtain an access token and device ID. +* `startWithToken` - This method uses an access token to log into the user's account, starts the client and initializes crypto. +* `clearDevices` - This method deletes the devices (other than the current one) from the user's account. + +### io.ts + +This file is responsible for handling the input and output to the console and reading credentials from a file. + +### index.ts + +This file handles the application setup and requests input from the user. This file essentially glues the methods from `matrix.ts` and `io.ts` together to turn it into a console messaging application. From 012dc1b83e9a305e93cd38a972f442398c8e87bf Mon Sep 17 00:00:00 2001 From: saul Date: Wed, 10 May 2023 12:36:38 +1200 Subject: [PATCH 38/48] Add notes about logging in. --- examples/crypto-node/README.md | 4 +++- examples/crypto-node/src/matrix.ts | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/crypto-node/README.md b/examples/crypto-node/README.md index a25665fa605..d240ca7ed9d 100644 --- a/examples/crypto-node/README.md +++ b/examples/crypto-node/README.md @@ -35,7 +35,7 @@ Once it starts up you can list commands by typing: /help ``` -If you have trouble with encryption errors cause by old devices with broken olm sessions you can delete them all by running: +If you have trouble with encryption errors caused by devices with broken olm sessions you can delete them all by running: ``` /cleardevices @@ -47,6 +47,8 @@ This will delete all the devices on the account (except for the current one) so This example does not provide any way of verifying your sessions, so on some clients, users in the room will get a warning that someone is using an unverified session. +This example does not store your access token meaning you log in everytime and generate a new one, if you have access to persistant storage you should save your device ID and access token so that you can reuse the old session. + This example does not provide any persistent storage so encryption keys for the device it creates are not persisted. This means every time the client starts it generates a new unverified device which is inaccessable when the program exits. If you want to persist data you will need to overwrite the default memory stores with stores that save data with a IndexedDB implementation: diff --git a/examples/crypto-node/src/matrix.ts b/examples/crypto-node/src/matrix.ts index 39acb129d63..c0105ff5870 100644 --- a/examples/crypto-node/src/matrix.ts +++ b/examples/crypto-node/src/matrix.ts @@ -29,6 +29,8 @@ export interface TokenLogin { * Create a matrix client using a token login. */ export const startWithToken = async (tokenLogin: TokenLogin | ICreateClientOpts): Promise => { + // If tokenLogin does not include store or cryptoStore parameters the client + // will use the default in-memory ones. const client = sdk.createClient(tokenLogin); // We must initialize the crypto before starting the client. @@ -94,6 +96,8 @@ export const start = async (passwordLogin: PasswordLogin): Promise const tokenLogin = await getTokenLogin(passwordLogin); // Start the client with the token. + // Here we are not adding a store or a cryptoStore to the tokenLogin so the + // client will default to the in-memory one. const client = await startWithToken(tokenLogin); return client; From ed8969b465d5bea706032e388642298ee93675bc Mon Sep 17 00:00:00 2001 From: saul Date: Wed, 14 Jun 2023 11:29:00 +1200 Subject: [PATCH 39/48] Fix old fetch call. --- examples/crypto-node/src/matrix-importer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/crypto-node/src/matrix-importer.ts b/examples/crypto-node/src/matrix-importer.ts index df3d0609d20..e80d82ee94b 100644 --- a/examples/crypto-node/src/matrix-importer.ts +++ b/examples/crypto-node/src/matrix-importer.ts @@ -32,7 +32,7 @@ global.fetch = async (input: RequestInfo | URL | string, init?: RequestInit): Pr } // Since this is not fetching the wasm we can just use the old implementation. - return await oldFetch.apply(this, [input, init]); + return await oldFetch(input, init); }; /** From c96c9e84c70e9d152e9c7557727ab79b5497a7b2 Mon Sep 17 00:00:00 2001 From: saul Date: Wed, 14 Jun 2023 11:40:41 +1200 Subject: [PATCH 40/48] Use localstorage store by default. --- examples/crypto-node/package.json | 4 +++- examples/crypto-node/src/matrix.ts | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/examples/crypto-node/package.json b/examples/crypto-node/package.json index 8f0d12107ab..44f42e2bdfc 100644 --- a/examples/crypto-node/package.json +++ b/examples/crypto-node/package.json @@ -13,9 +13,11 @@ "license": "Apache 2.0", "dependencies": { "@matrix-org/olm": "^3.2.15", - "matrix-js-sdk": "file:../.." + "matrix-js-sdk": "file:../..", + "node-localstorage": "^2.2.1" }, "devDependencies": { + "@types/node-localstorage": "^1.3.0", "typescript": "^5.0.4" } } diff --git a/examples/crypto-node/src/matrix.ts b/examples/crypto-node/src/matrix.ts index c0105ff5870..1b5acc9ab95 100644 --- a/examples/crypto-node/src/matrix.ts +++ b/examples/crypto-node/src/matrix.ts @@ -3,9 +3,19 @@ * sdk. */ + import { LocalStorage } from 'node-localstorage'; + import { LocalStorageCryptoStore } from "../node_modules/matrix-js-sdk/lib/crypto/store/localStorage-crypto-store.js"; + import { MemoryStore } from "../node_modules/matrix-js-sdk/lib/store/memory.js"; + + import sdk from "./matrix-importer.js"; import type { ICreateClientOpts, MatrixClient, Room } from "matrix-js-sdk"; +// Setup the local stores. +const localStorage = new LocalStorage("./localstorage"); +const cryptoStore = new LocalStorageCryptoStore(localStorage); +const store = new MemoryStore({ localStorage }); + /** * This interface provides the details needed to perform a password login. */ @@ -31,7 +41,11 @@ export interface TokenLogin { export const startWithToken = async (tokenLogin: TokenLogin | ICreateClientOpts): Promise => { // If tokenLogin does not include store or cryptoStore parameters the client // will use the default in-memory ones. - const client = sdk.createClient(tokenLogin); + const client = sdk.createClient({ + ...tokenLogin, + store, + cryptoStore + }); // We must initialize the crypto before starting the client. await client.initCrypto(); From 91400a8b70c7155957f9d7ee9c6479f62d04ce51 Mon Sep 17 00:00:00 2001 From: saul Date: Wed, 14 Jun 2023 11:41:09 +1200 Subject: [PATCH 41/48] Update device verification method to new API. --- examples/crypto-node/src/matrix.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/crypto-node/src/matrix.ts b/examples/crypto-node/src/matrix.ts index 1b5acc9ab95..e6a68c57431 100644 --- a/examples/crypto-node/src/matrix.ts +++ b/examples/crypto-node/src/matrix.ts @@ -132,13 +132,18 @@ export const verifyRoom = async (client: MatrixClient, room: Room): Promise[] = []; - for (const member of members) { - const devices = client.getStoredDevicesForUser(member.userId); + const crypto = client.getCrypto(); - for (const device of devices) { + if (crypto == null) { + return; + } + + const deviceMap = await crypto.getUserDeviceInfo(members.map(m => m.userId)); - if (device.isUnverified()) { - verificationPromises.push( verifyDevice(client, member.userId, device.deviceId) ); + for (const [member, devices] of deviceMap.entries()) { + for (const device of devices.values()) { + if (!device.verified) { + verificationPromises.push( verifyDevice(client, member, device.deviceId) ); } } } From 2d3ea4f3b6b22583262234abf4d62d8c1baca8e7 Mon Sep 17 00:00:00 2001 From: saul Date: Wed, 14 Jun 2023 11:46:05 +1200 Subject: [PATCH 42/48] Change logger path. --- examples/crypto-node/src/matrix-importer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/crypto-node/src/matrix-importer.ts b/examples/crypto-node/src/matrix-importer.ts index e80d82ee94b..af0fc790427 100644 --- a/examples/crypto-node/src/matrix-importer.ts +++ b/examples/crypto-node/src/matrix-importer.ts @@ -38,7 +38,7 @@ global.fetch = async (input: RequestInfo | URL | string, init?: RequestInit): Pr /** * We will increase the logger severity to reduce clutter. */ -import { logger } from "../../../lib/logger.js"; +import { logger } from "../node_modules/matrix-js-sdk/lib/logger.js"; logger.setLevel(5); From 149f322713024f8cb5561207d49d852343903418 Mon Sep 17 00:00:00 2001 From: saul Date: Wed, 14 Jun 2023 11:59:00 +1200 Subject: [PATCH 43/48] Ignore slight store signature mismatch. --- examples/crypto-node/src/matrix.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/crypto-node/src/matrix.ts b/examples/crypto-node/src/matrix.ts index e6a68c57431..1996471bf71 100644 --- a/examples/crypto-node/src/matrix.ts +++ b/examples/crypto-node/src/matrix.ts @@ -40,9 +40,11 @@ export interface TokenLogin { */ export const startWithToken = async (tokenLogin: TokenLogin | ICreateClientOpts): Promise => { // If tokenLogin does not include store or cryptoStore parameters the client - // will use the default in-memory ones. + // will use the default in-memory ones. The default in-memory ones can have + // issues when it comes to E2EE. const client = sdk.createClient({ ...tokenLogin, + // @ts-ignore TS2322 Ignore slight store signature mismatch. store, cryptoStore }); From 58cfbd14cadc0adb489a91a4ff8687614d4606e3 Mon Sep 17 00:00:00 2001 From: saul Date: Wed, 14 Jun 2023 11:59:59 +1200 Subject: [PATCH 44/48] Add localstorage to gitignore. --- examples/crypto-node/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/crypto-node/.gitignore b/examples/crypto-node/.gitignore index 1d12754aaa5..8ff8cfe5193 100644 --- a/examples/crypto-node/.gitignore +++ b/examples/crypto-node/.gitignore @@ -1,2 +1,3 @@ credentials.json lib +localstorage From fd06f09e033c34e9833c84db9cdc44e5e6251177 Mon Sep 17 00:00:00 2001 From: saul Date: Wed, 14 Jun 2023 12:04:46 +1200 Subject: [PATCH 45/48] Fix empty content error. --- examples/crypto-node/src/io.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/crypto-node/src/io.ts b/examples/crypto-node/src/io.ts index d08baddbed7..9c909d3da3f 100644 --- a/examples/crypto-node/src/io.ts +++ b/examples/crypto-node/src/io.ts @@ -202,6 +202,8 @@ export const printMessage = (event: MatrixEvent) => { console.log(`[${time}] ${name}`); + content = content ?? ""; + for (const line of content.split("\n")) { console.log(` ${line}`); } From 75b4f972ffe5fb1c803dd6924d33ce54cb3fed7b Mon Sep 17 00:00:00 2001 From: saul Date: Wed, 14 Jun 2023 12:09:30 +1200 Subject: [PATCH 46/48] Explain the borken olm sessions and update limitations. --- examples/crypto-node/README.md | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/examples/crypto-node/README.md b/examples/crypto-node/README.md index d240ca7ed9d..a1d5eef27ec 100644 --- a/examples/crypto-node/README.md +++ b/examples/crypto-node/README.md @@ -35,7 +35,7 @@ Once it starts up you can list commands by typing: /help ``` -If you have trouble with encryption errors caused by devices with broken olm sessions you can delete them all by running: +If you have trouble with encryption errors caused by devices with broken olm sessions (Usually occurring from use of the in-memory crypto store.) you can delete them all by running: ``` /cleardevices @@ -47,30 +47,7 @@ This will delete all the devices on the account (except for the current one) so This example does not provide any way of verifying your sessions, so on some clients, users in the room will get a warning that someone is using an unverified session. -This example does not store your access token meaning you log in everytime and generate a new one, if you have access to persistant storage you should save your device ID and access token so that you can reuse the old session. - -This example does not provide any persistent storage so encryption keys for the device it creates are not persisted. This means every time the client starts it generates a new unverified device which is inaccessable when the program exits. - -If you want to persist data you will need to overwrite the default memory stores with stores that save data with a IndexedDB implementation: - -```javascript -import sqlite3 from "sqlite3"; -import indexeddbjs from "indexeddb-js"; - -const engine = new sqlite3.Database("./sqlite"); -const { indexedDB } = indexeddbjs.makeScope("sqlite3", engine); - -sdk.createClient({ - baseUrl: "https://matrix.org", - userId: "@my_user:matrix.org", - accessToken: "my_access_token", - deviceId: "my_device_id", - store: new sdk.IndexedDBStore({ indexedDB }), - cryptoStore: new sdk.IndexedDBCryptoStore(indexedDB, "crypto") -}); -``` - -Alternatively you could create your own store implementation using whatever backend storage you want. +This example relies on the `node-localstorage` package to provide persistance which is more or less required for E2EE and at the time of writing there are no working alternative packages. ## Structure From 3f05fd006bcd5e384fc14abf9ce6fe08ed899501 Mon Sep 17 00:00:00 2001 From: saul Date: Wed, 14 Jun 2023 12:15:35 +1200 Subject: [PATCH 47/48] Save the access token and device ID. --- examples/crypto-node/src/matrix.ts | 34 ++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/examples/crypto-node/src/matrix.ts b/examples/crypto-node/src/matrix.ts index 1996471bf71..1b3d32ebeff 100644 --- a/examples/crypto-node/src/matrix.ts +++ b/examples/crypto-node/src/matrix.ts @@ -39,9 +39,9 @@ export interface TokenLogin { * Create a matrix client using a token login. */ export const startWithToken = async (tokenLogin: TokenLogin | ICreateClientOpts): Promise => { - // If tokenLogin does not include store or cryptoStore parameters the client - // will use the default in-memory ones. The default in-memory ones can have - // issues when it comes to E2EE. + // If sdk.createClient does not include store or cryptoStore parameters the + // client will use the default in-memory ones. The default in-memory ones can + // have issues when it comes to E2EE. const client = sdk.createClient({ ...tokenLogin, // @ts-ignore TS2322 Ignore slight store signature mismatch. @@ -108,16 +108,36 @@ export const clearDevices = async (client: MatrixClient) => { * Start the client with a password login. */ export const start = async (passwordLogin: PasswordLogin): Promise => { + // Attempt to get the access token and device ID from the storage. + let accessToken = localStorage.getItem(`token/${passwordLogin.userId}`); + let deviceId = localStorage.getItem(`device/${passwordLogin.userId}`); + // Get the token login details. - const tokenLogin = await getTokenLogin(passwordLogin); + let tokenLogin: TokenLogin; + + if (accessToken == null || deviceId == null) { + // Storage doesn't have the access token or device ID, use password to + // generate a new one. + tokenLogin = await getTokenLogin(passwordLogin); + + // Save the generated access token and device ID for another session. + localStorage.setItem(`token/${passwordLogin.userId}`, tokenLogin.accessToken); + localStorage.setItem(`device/${passwordLogin.userId}`, tokenLogin.deviceId); + } else { + // We have the access token and device ID, we can skip password login. + tokenLogin = { + baseUrl: passwordLogin.baseUrl, + userId: passwordLogin.userId, + accessToken, + deviceId + }; + } // Start the client with the token. - // Here we are not adding a store or a cryptoStore to the tokenLogin so the - // client will default to the in-memory one. const client = await startWithToken(tokenLogin); return client; -} +}; /** * Mark a device associated with a user as verified. From 3fc1b2ea197188bbcc127c35ea06925f264d035a Mon Sep 17 00:00:00 2001 From: saul Date: Wed, 14 Jun 2023 12:22:08 +1200 Subject: [PATCH 48/48] Remove '/' from localstorage item prefix. --- examples/crypto-node/src/matrix.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/crypto-node/src/matrix.ts b/examples/crypto-node/src/matrix.ts index 1b3d32ebeff..f58c4ceb29f 100644 --- a/examples/crypto-node/src/matrix.ts +++ b/examples/crypto-node/src/matrix.ts @@ -109,8 +109,8 @@ export const clearDevices = async (client: MatrixClient) => { */ export const start = async (passwordLogin: PasswordLogin): Promise => { // Attempt to get the access token and device ID from the storage. - let accessToken = localStorage.getItem(`token/${passwordLogin.userId}`); - let deviceId = localStorage.getItem(`device/${passwordLogin.userId}`); + let accessToken = localStorage.getItem(`token-${passwordLogin.userId}`); + let deviceId = localStorage.getItem(`device-${passwordLogin.userId}`); // Get the token login details. let tokenLogin: TokenLogin; @@ -121,8 +121,8 @@ export const start = async (passwordLogin: PasswordLogin): Promise tokenLogin = await getTokenLogin(passwordLogin); // Save the generated access token and device ID for another session. - localStorage.setItem(`token/${passwordLogin.userId}`, tokenLogin.accessToken); - localStorage.setItem(`device/${passwordLogin.userId}`, tokenLogin.deviceId); + localStorage.setItem(`token-${passwordLogin.userId}`, tokenLogin.accessToken); + localStorage.setItem(`device-${passwordLogin.userId}`, tokenLogin.deviceId); } else { // We have the access token and device ID, we can skip password login. tokenLogin = {