Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add NodeJS E2EE TS example. #3346

Closed
wants to merge 49 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
6176b59
Prepare another example.
saul-jb May 4, 2023
53bf29b
Fix tsconfig, add missing packages and get login working.
saul-jb May 5, 2023
80ae44b
Reduce logging and display rooms.
saul-jb May 5, 2023
f592b3e
Update olm lib.
saul-jb May 5, 2023
0da301b
Join rooms and display messages.
saul-jb May 5, 2023
6b7c47e
Update types to match spec (#3330)
t3chguy May 5, 2023
52536ec
Verify devices in rooms.
saul-jb May 8, 2023
45f1a17
Clear old devices.
saul-jb May 8, 2023
c198139
Only display incoming messages when in the room.
saul-jb May 8, 2023
98f4566
Refactor matrix setup into separate file.
saul-jb May 8, 2023
c9d502a
Refactor matrix helper methods into separate file.
saul-jb May 8, 2023
99fbb84
Refactor the IO to a separate file.
saul-jb May 8, 2023
680086d
Add method for getting the list of rooms.
saul-jb May 8, 2023
6e1e825
Cleanup main logic using refactors.
saul-jb May 8, 2023
462b614
Add command for displaying members.
saul-jb May 8, 2023
e093a6c
Refactor prompts.
saul-jb May 8, 2023
8701aef
Remove cli-color types.
saul-jb May 8, 2023
c38b6e2
Print room info and add ability to leave rooms.
saul-jb May 8, 2023
f98112d
Add ability to invite users to a room.
saul-jb May 8, 2023
fd7a393
Move clear devices to separate command.
saul-jb May 9, 2023
d6f1c57
Refactor how commands are handled.
saul-jb May 9, 2023
8e2231f
Add quit command.
saul-jb May 9, 2023
eb86d57
Add auto completions.
saul-jb May 9, 2023
a1e92a5
Add help command.
saul-jb May 9, 2023
49bcb45
Improve message display.
saul-jb May 9, 2023
782bc69
Document the index file.
saul-jb May 9, 2023
aa9d57c
Rework how credentials are loaded.
saul-jb May 9, 2023
f772722
Add credentials template.
saul-jb May 9, 2023
be68e7b
Fix matrix lib path and handle building with tsc.
saul-jb May 9, 2023
016b041
Change filter to arrow function.
saul-jb May 9, 2023
a98ade5
Change build dir to lib.
saul-jb May 9, 2023
3ab5774
Add readme.
saul-jb May 9, 2023
1f1b1cf
Add header to first section.
saul-jb May 9, 2023
d0d76b4
Revert "Update types to match spec (#3330)"
saul-jb May 9, 2023
4480159
Fix readme typo.
saul-jb May 9, 2023
6db0248
Remove forget devices options from start.
saul-jb May 9, 2023
c69f6af
Expand the readme with limitations and structure.
saul-jb May 10, 2023
012dc1b
Add notes about logging in.
saul-jb May 10, 2023
ed8969b
Fix old fetch call.
saul-jb Jun 13, 2023
372c92b
Merge branch 'master' into feat/crypto-node-ts-example
saul-jb Jun 13, 2023
c96c9e8
Use localstorage store by default.
saul-jb Jun 13, 2023
91400a8
Update device verification method to new API.
saul-jb Jun 13, 2023
2d3ea4f
Change logger path.
saul-jb Jun 13, 2023
149f322
Ignore slight store signature mismatch.
saul-jb Jun 13, 2023
58cfbd1
Add localstorage to gitignore.
saul-jb Jun 13, 2023
fd06f09
Fix empty content error.
saul-jb Jun 14, 2023
75b4f97
Explain the borken olm sessions and update limitations.
saul-jb Jun 14, 2023
3f05fd0
Save the access token and device ID.
saul-jb Jun 14, 2023
3fc1b2e
Remove '/' from localstorage item prefix.
saul-jb Jun 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/crypto-node/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
credentials.json
lib
localstorage
1 change: 1 addition & 0 deletions examples/crypto-node/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@matrix-org:registry=https://gitlab.matrix.org/api/v4/packages/npm/
76 changes: 76 additions & 0 deletions examples/crypto-node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
## 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

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
{
"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 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
```

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 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

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.
5 changes: 5 additions & 0 deletions examples/crypto-node/credentials.template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"userId": "@my_user:matrix.org",
"password": "my_password",
"baseUrl": "https://matrix.org"
}
23 changes: 23 additions & 0 deletions examples/crypto-node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"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:../..",
"node-localstorage": "^2.2.1"
},
"devDependencies": {
"@types/node-localstorage": "^1.3.0",
"typescript": "^5.0.4"
}
}
228 changes: 228 additions & 0 deletions examples/crypto-node/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/**
* 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 Path from "path";
import { fileURLToPath } from 'url';

/**
* Import out IO helper methods.
*/
import {
readCredentials,
prompt,
fixWidth,
printRoomList,
printMessage,
printMessages,
printMemberList,
printRoomInfo,
addCommand
} from "./io.js";

/**
* Import our matrix helper methods.
*/
import { start, verifyRoom, getRoomList, clearDevices } from "./matrix.js";

/**
* Import the types and enums from matrix-js-sdk.
*/
import { ClientEvent, RoomEvent, EventType, JoinRule, MsgType } from "matrix-js-sdk"
import type { Room } from "matrix-js-sdk";

/**
* Global state for keeping track of rooms.
*/
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.
*/
const client = await start(credentials);

/**
* When a room is added or removed update the room list.
*/
client.on(ClientEvent.Room, () => {
roomList = getRoomList(client);

if (!viewingRoom) {
printRoomList(roomList);
}

prompt();
});

/**
* 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 (![EventType.RoomMessage, EventType.RoomMessageEncrypted].includes(type)) {
return;
}

if (room != null && room.roomId !== viewingRoom?.roomId) {
return;
}

await client.decryptEventIfNeeded(event);

printMessage(event);
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}`);
};

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 <index>", "Join a room, e.g. '/join 5'");

console.log("Room commands:");
displayCommand("/exit", "Return to the room list index.");
displayCommand("/send <message>", "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.");
});

/**
* 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.";
}

viewingRoom = roomList[index];

if (viewingRoom == null) {
return "Invalid Room.";
}

if (viewingRoom.getMember(client.getUserId() ?? "")?.membership === JoinRule.Invite) {
await client.joinRoom(viewingRoom.roomId);
}

await verifyRoom(client, viewingRoom);
await client.roomInitialSync(viewingRoom.roomId, 20);

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.";
}

try {
await client.invite(viewingRoom.roomId, userId);
} catch (error) {
return `/invite Error: ${error}`;
}
});

/**
* Members command, displays the list of members in the current room.
*/
addCommand("/members", 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.";
}

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.";
}

console.log(tokens);
console.log(tokens.join(" "));

const message = {
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();
Loading