diff --git a/README.md b/README.md index 325ed1111..59f798e61 100644 --- a/README.md +++ b/README.md @@ -21,16 +21,6 @@ endpoints from before Matrix 1.1, for example. # Quickstart -## In a browser - -### Note, the browserify build has been removed. Please use a bundler like webpack or vite instead. - -## In Node.js - -Ensure you have the latest LTS version of Node.js installed. -This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills. -If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn` to the MatrixClient constructor options. - Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install) if you do not have it already. @@ -47,8 +37,6 @@ client.publicRooms(function (err, data) { See below for how to include libolm to enable end-to-end-encryption. Please check [the Node.js terminal app](examples/node) for a more complex example. -You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported. - To start the client: ```javascript @@ -106,7 +94,7 @@ Object.keys(client.store.rooms).forEach((roomId) => { This SDK provides a full object model around the Matrix Client-Server API and emits events for incoming data and state changes. Aside from wrapping the HTTP API, it: -- Handles syncing (via `/initialSync` and `/events`) +- Handles syncing (via `/sync`) - Handles the generation of "friendly" room and member names. - Handles historical `RoomMember` information (e.g. display names). - Manages room member state across multiple events (e.g. it handles typing, power @@ -127,20 +115,20 @@ events for incoming data and state changes. Aside from wrapping the HTTP API, it - Handles room initial sync on accepting invites. - Handles WebRTC calling. -Later versions of the SDK will: - -- Expose a `RoomSummary` which would be suitable for a recents page. -- Provide different pluggable storage layers (e.g. local storage, database-backed) - # Usage -## Conventions +## Supported platforms -### Emitted events +`matrix-js-sdk` can be used in either Node.js applications (ensure you have the latest LTS version of Node.js installed), +or in browser applications, via a bundler such as Webpack or Vite. -The SDK will emit events using an `EventEmitter`. It also -emits object models (e.g. `Rooms`, `RoomMembers`) when they -are updated. +You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported. + +## Emitted events + +The SDK raises notifications to the application using +[`EventEmitter`s](https://nodejs.org/api/events.html#class-eventemitter). The `MatrixClient` itself +implements `EventEmitter`, as do many of the high-level abstractions such as `Room` and `RoomMember`. ```javascript // Listen for low-level MatrixEvents @@ -161,45 +149,21 @@ client.on(RoomMemberEvent.Typing, function (event, member) { client.startClient(); ``` -### Promises and Callbacks +## Entry points -Most of the methods in the SDK are asynchronous: they do not directly return a -result, but instead return a [Promise](http://documentup.com/kriskowal/q/) -which will be fulfilled in the future. +As well as the primary entry point (`matrix-js-sdk`), there are several other entry points which may be useful: -The typical usage is something like: - -```javascript - matrixClient.someMethod(arg1, arg2).then(function(result) { - ... - }); -``` - -Alternatively, if you have a Node.js-style `callback(err, result)` function, -you can pass the result of the promise into it with something like: - -```javascript -matrixClient.someMethod(arg1, arg2).nodeify(callback); -``` - -The main thing to note is that it is problematic to discard the result of a -promise-returning function, as that will cause exceptions to go unobserved. - -Methods which return a promise show this in their documentation. - -Many methods in the SDK support _both_ Node.js-style callbacks _and_ Promises, -via an optional `callback` argument. The callback support is now deprecated: -new methods do not include a `callback` argument, and in the future it may be -removed from existing methods. - -## Low level types - -There are some low level TypeScript types exported via the `matrix-js-sdk/lib/types` entrypoint to not bloat the main entrypoint. +| Entry point | Description | +| ------------------------------ | --------------------------------------------------------------------------------------------------- | +| `matrix-js-sdk` | Primary entry point. High-level functionality, and lots of historical clutter in need of a cleanup. | +| `matrix-js-sdk/lib/crypto-api` | Cryptography functionality. | +| `matrix-js-sdk/lib/types` | Low-level types, reflecting data structures defined in the Matrix spec. | +| `matrix-js-sdk/lib/testing` | Test utilities, which may be useful in test code but should not be used in production code. | ## Examples This section provides some useful code snippets which demonstrate the -core functionality of the SDK. These examples assume the SDK is setup like this: +core functionality of the SDK. These examples assume the SDK is set up like this: ```javascript import * as sdk from "matrix-js-sdk"; @@ -306,6 +270,9 @@ Then visit `http://localhost:8005` to see the API docs. # End-to-end encryption support +**This section is outdated.** Use of `libolm` is deprecated and we are replacing it with support +from the matrix-rust-sdk (https://github.com/element-hq/element-web/issues/21972). + The SDK supports end-to-end encryption via the Olm and Megolm protocols, using [libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the application to make libolm available, via the `Olm` global. diff --git a/spec/unit/testing.spec.ts b/spec/unit/testing.spec.ts new file mode 100644 index 000000000..a18147d86 --- /dev/null +++ b/spec/unit/testing.spec.ts @@ -0,0 +1,88 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { mkDecryptionFailureMatrixEvent, mkEncryptedMatrixEvent, mkMatrixEvent } from "../../src/testing"; +import { EventType } from "../../src"; + +describe("testing", () => { + describe("mkMatrixEvent", () => { + it("makes an event", () => { + const event = mkMatrixEvent({ + content: { body: "blah" }, + sender: "@alice:test", + type: EventType.RoomMessage, + roomId: "!test:room", + }); + + expect(event.getContent()).toEqual({ body: "blah" }); + expect(event.sender?.userId).toEqual("@alice:test"); + expect(event.isState()).toBe(false); + }); + + it("makes a state event", () => { + const event = mkMatrixEvent({ + content: { body: "blah" }, + sender: "@alice:test", + type: EventType.RoomTopic, + roomId: "!test:room", + stateKey: "", + }); + + expect(event.getContent()).toEqual({ body: "blah" }); + expect(event.sender?.userId).toEqual("@alice:test"); + expect(event.isState()).toBe(true); + expect(event.getStateKey()).toEqual(""); + }); + }); + + describe("mkEncryptedMatrixEvent", () => { + it("makes an event", async () => { + const event = await mkEncryptedMatrixEvent({ + plainContent: { body: "blah" }, + sender: "@alice:test", + plainType: EventType.RoomMessage, + roomId: "!test:room", + }); + + expect(event.sender?.userId).toEqual("@alice:test"); + expect(event.isEncrypted()).toBe(true); + expect(event.isDecryptionFailure()).toBe(false); + expect(event.getContent()).toEqual({ body: "blah" }); + expect(event.getType()).toEqual("m.room.message"); + }); + }); + + describe("mkDecryptionFailureMatrixEvent", () => { + it("makes an event", async () => { + const event = await mkDecryptionFailureMatrixEvent({ + sender: "@alice:test", + roomId: "!test:room", + code: "UNKNOWN", + msg: "blah", + }); + + expect(event.sender?.userId).toEqual("@alice:test"); + expect(event.isEncrypted()).toBe(true); + expect(event.isDecryptionFailure()).toBe(true); + expect(event.getContent()).toEqual({ + body: "** Unable to decrypt: DecryptionError: blah **", + msgtype: "m.bad.encrypted", + }); + expect(event.getType()).toEqual("m.room.message"); + expect(event.isState()).toBe(false); + }); + }); +}); diff --git a/src/testing.ts b/src/testing.ts new file mode 100644 index 000000000..2b11b0362 --- /dev/null +++ b/src/testing.ts @@ -0,0 +1,165 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * This file is a secondary entrypoint for the js-sdk library, exposing utilities which might be useful for writing tests. + * + * In general it should not be included in runtime applications. + * + * @packageDocumentation + */ + +import { IContent, IEvent, IUnsigned, MatrixEvent } from "./models/event"; +import { RoomMember } from "./models/room-member"; +import { EventType } from "./@types/event"; +import { IEventDecryptionResult } from "./@types/crypto"; +import { DecryptionError } from "./crypto/algorithms"; + +/** + * Create a {@link MatrixEvent}. + * + * @param opts - Values for the event. + */ +export function mkMatrixEvent(opts: { + /** Room ID of the event. */ + roomId: string; + + /** The sender of the event. */ + sender: string; + + /** The type of the event. */ + type: EventType | string; + + /** Optional `state_key` for the event. If unspecified, a non-state event is created. */ + stateKey?: string; + + /** Optional `origin_server_ts` for the event. If unspecified, the timestamp will be set to 0. */ + ts?: number; + + /** Optional `event_id` for the event. If provided will be used as event ID; else an ID is generated. */ + eventId?: string; + + /** Content of the event. */ + content: IContent; + + /** Optional `unsigned` data for the event. */ + unsigned?: IUnsigned; +}): MatrixEvent { + const event: Partial = { + type: opts.type, + room_id: opts.roomId, + sender: opts.sender, + content: opts.content, + event_id: opts.eventId ?? "$" + Math.random() + "-" + Math.random(), + origin_server_ts: opts.ts ?? 0, + unsigned: opts.unsigned, + }; + if (opts.stateKey !== undefined) { + event.state_key = opts.stateKey; + } + + const mxEvent = new MatrixEvent(event); + mxEvent.sender = { + userId: opts.sender, + membership: "join", + name: opts.sender, + rawDisplayName: opts.sender, + roomId: opts.sender, + getAvatarUrl: () => {}, + getMxcAvatarUrl: () => {}, + } as unknown as RoomMember; + return mxEvent; +} + +/** + * Create a `MatrixEvent` representing a successfully-decrypted `m.room.encrypted` event. + * + * @param opts - Values for the event. + */ +export async function mkEncryptedMatrixEvent(opts: { + /** Room ID of the event. */ + roomId: string; + + /** The sender of the event. */ + sender: string; + + /** The type the event will have, once it has been decrypted. */ + plainType: EventType | string; + + /** The content the event will have, once it has been decrypted. */ + plainContent: IContent; +}): Promise { + // we construct an event which has been decrypted by stubbing out CryptoBackend.decryptEvent and then + // calling MatrixEvent.attemptDecryption. + + const mxEvent = mkMatrixEvent({ + type: EventType.RoomMessageEncrypted, + roomId: opts.roomId, + sender: opts.sender, + content: { algorithm: "m.megolm.v1.aes-sha2" }, + }); + + const decryptionResult: IEventDecryptionResult = { + claimedEd25519Key: "", + clearEvent: { + type: opts.plainType, + content: opts.plainContent, + }, + forwardingCurve25519KeyChain: [], + senderCurve25519Key: "", + untrusted: false, + }; + + const mockCrypto = { + decryptEvent: async (_ev): Promise => decryptionResult, + } as Parameters[0]; + await mxEvent.attemptDecryption(mockCrypto); + return mxEvent; +} + +/** + * Create a `MatrixEvent` representing a `m.room.encrypted` event which could not be decrypted. + * + * @param opts - Values for the event. + */ +export async function mkDecryptionFailureMatrixEvent(opts: { + /** Room ID of the event. */ + roomId: string; + + /** The sender of the event. */ + sender: string; + + /** The reason code for the failure */ + code: string; + + /** A textual reason for the failure */ + msg: string; +}): Promise { + const mxEvent = mkMatrixEvent({ + type: EventType.RoomMessageEncrypted, + roomId: opts.roomId, + sender: opts.sender, + content: { algorithm: "m.megolm.v1.aes-sha2" }, + }); + + const mockCrypto = { + decryptEvent: async (_ev): Promise => { + throw new DecryptionError(opts.code, opts.msg); + }, + } as Parameters[0]; + await mxEvent.attemptDecryption(mockCrypto); + return mxEvent; +} diff --git a/typedoc.json b/typedoc.json index 017c3a4d6..69426dc7a 100644 --- a/typedoc.json +++ b/typedoc.json @@ -2,7 +2,7 @@ "$schema": "https://typedoc.org/schema.json", "plugin": ["typedoc-plugin-mdn-links", "typedoc-plugin-missing-exports", "typedoc-plugin-coverage"], "coverageLabel": "TypeDoc", - "entryPoints": ["src/matrix.ts", "src/types.ts"], + "entryPoints": ["src/matrix.ts", "src/types.ts", "src/testing.ts"], "excludeExternals": true, "out": "_docs" }