Compare commits
49 Commits
v19.6.0
...
v20.1.0-rc.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 07476a0ae0 | |||
| 6348704bec | |||
| f84a33910c | |||
| 0ccf7c50f2 | |||
| ead33003b7 | |||
| 7d5360a00f | |||
| 4b283015ba | |||
| 887e15aac5 | |||
| a83d80f502 | |||
| 6166a8f7fd | |||
| 3efc18cfde | |||
| 5520aa3e2a | |||
| 5afe373446 | |||
| 9bb5afe5c0 | |||
| f349663329 | |||
| f398e3564d | |||
| 91171afddd | |||
| b54c9d689a | |||
| 4e69d7c9ac | |||
| bf9f595984 | |||
| f410e71bfa | |||
| 83fca5b57d | |||
| 90052670e7 | |||
| db49a1a623 | |||
| 45330c6418 | |||
| 4ba083e6af | |||
| 0403e4bedc | |||
| 14aa7846a5 | |||
| 2d067ad957 | |||
| 418aa3ff6a | |||
| a587d7c360 | |||
| 45348a354e | |||
| fa3339fc84 | |||
| b332c6c4b9 | |||
| 209a101be7 | |||
| efbf5479d1 | |||
| dacef048be | |||
| caadc6f95b | |||
| 39bc7e2bb3 | |||
| 040f012350 | |||
| 8b2b677f92 | |||
| 2b1fab928b | |||
| 516f52c5a4 | |||
| 8599a98b47 | |||
| 7e24cb6cae | |||
| 38aa8d18c0 | |||
| 2967ee6309 | |||
| 2e10b6065c | |||
| 69057ee035 |
+21
-1
@@ -1,14 +1,22 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
"matrix-org",
|
||||
"import",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/babel",
|
||||
"plugin:import/typescript",
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: true,
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
// NOTE: These rules are frozen and new rules should not be added here.
|
||||
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
|
||||
rules: {
|
||||
@@ -35,7 +43,19 @@ module.exports = {
|
||||
"no-console": "error",
|
||||
|
||||
// restrict EventEmitters to force callers to use TypedEventEmitter
|
||||
"no-restricted-imports": ["error", "events"],
|
||||
"no-restricted-imports": ["error", {
|
||||
name: "events",
|
||||
message: "Please use TypedEventEmitter instead"
|
||||
}],
|
||||
|
||||
"import/no-restricted-paths": ["error", {
|
||||
"zones": [{
|
||||
"target": "./src/",
|
||||
"from": "./src/index.ts",
|
||||
"message": "The package index is dynamic between src and lib depending on " +
|
||||
"whether release or development, target the specific module or matrix.ts instead",
|
||||
}],
|
||||
}],
|
||||
},
|
||||
overrides: [{
|
||||
files: [
|
||||
|
||||
@@ -36,5 +36,6 @@ jobs:
|
||||
package=$(cat package.json | jq -er .name)
|
||||
npm dist-tag add "$package@$release" latest
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
# JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc
|
||||
INPUT_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
release: ${{ steps.npm-publish.outputs.version }}
|
||||
|
||||
@@ -23,6 +23,16 @@ jobs:
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
|
||||
- name: Switch js-sdk to release mode
|
||||
run: |
|
||||
scripts/switch_package_to_release.js
|
||||
yarn install
|
||||
yarn run build:compile
|
||||
yarn run build:types
|
||||
|
||||
- name: Typecheck (release mode)
|
||||
run: "yarn run lint:types"
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -1,3 +1,45 @@
|
||||
Changes in [20.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0-rc.1) (2022-10-04)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Add local notification settings capability ([\#2700](https://github.com/matrix-org/matrix-js-sdk/pull/2700)).
|
||||
* Implementation of MSC3882 login token request ([\#2687](https://github.com/matrix-org/matrix-js-sdk/pull/2687)). Contributed by @hughns.
|
||||
* Typings for MSC2965 OIDC provider discovery ([\#2424](https://github.com/matrix-org/matrix-js-sdk/pull/2424)). Contributed by @hughns.
|
||||
* Support to remotely toggle push notifications ([\#2686](https://github.com/matrix-org/matrix-js-sdk/pull/2686)).
|
||||
* Read receipts for threads ([\#2635](https://github.com/matrix-org/matrix-js-sdk/pull/2635)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Unexpected ignored self key request when it's not shared history ([\#2724](https://github.com/matrix-org/matrix-js-sdk/pull/2724)). Contributed by @mcalinghee.
|
||||
* Fix IDB initial migration handling causing spurious lazy loading upgrade loops ([\#2718](https://github.com/matrix-org/matrix-js-sdk/pull/2718)). Fixes vector-im/element-web#23377.
|
||||
* Fix backpagination at end logic being spec non-conforming ([\#2680](https://github.com/matrix-org/matrix-js-sdk/pull/2680)). Fixes vector-im/element-web#22784.
|
||||
|
||||
Changes in [20.0.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.2) (2022-09-30)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix issue in sync when crypto is not supported by client ([\#2715](https://github.com/matrix-org/matrix-js-sdk/pull/2715)). Contributed by @stas-demydiuk.
|
||||
|
||||
Changes in [20.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.1) (2022-09-28)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix missing return when receiving an invitation without shared history ([\#2710](https://github.com/matrix-org/matrix-js-sdk/pull/2710)).
|
||||
|
||||
Changes in [20.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.0) (2022-09-28)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Bump IDB crypto store version ([\#2705](https://github.com/matrix-org/matrix-js-sdk/pull/2705)).
|
||||
|
||||
Changes in [19.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.7.0) (2022-09-28)
|
||||
==================================================================================================
|
||||
|
||||
## 🔒 Security
|
||||
* Fix for [CVE-2022-39249](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39249)
|
||||
* Fix for [CVE-2022-39250](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39250)
|
||||
* Fix for [CVE-2022-39251](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39251)
|
||||
* Fix for [CVE-2022-39236](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39236)
|
||||
|
||||
Changes in [19.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.6.0) (2022-09-27)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "19.6.0",
|
||||
"version": "20.1.0-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=12.9.0"
|
||||
@@ -92,9 +92,10 @@
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "8.23.0",
|
||||
"eslint": "8.23.1",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-matrix-org": "^0.6.0",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
|
||||
+1
-13
@@ -135,7 +135,6 @@ yarn install --ignore-scripts --pure-lockfile
|
||||
# ignore leading v on release
|
||||
release="${1#v}"
|
||||
tag="v${release}"
|
||||
rel_branch="release-$tag"
|
||||
|
||||
prerelease=0
|
||||
# We check if this build is a prerelease by looking to
|
||||
@@ -150,18 +149,7 @@ else
|
||||
read -p "Making a FINAL RELEASE, press enter to continue " REPLY
|
||||
fi
|
||||
|
||||
# We might already be on the release branch, in which case, yay
|
||||
# If we're on any branch starting with 'release', or the staging branch
|
||||
# we don't create a separate release branch (this allows us to use the same
|
||||
# release branch for releases and release candidates).
|
||||
curbranch=$(git symbolic-ref --short HEAD)
|
||||
if [[ "$curbranch" != release* && "$curbranch" != "staging" ]]; then
|
||||
echo "Creating release branch"
|
||||
git checkout -b "$rel_branch"
|
||||
else
|
||||
echo "Using current branch ($curbranch) for release"
|
||||
rel_branch=$curbranch
|
||||
fi
|
||||
rel_branch=$(git symbolic-ref --short HEAD)
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
echo "Generating changelog"
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fsProm = require('fs/promises');
|
||||
|
||||
const PKGJSON = 'package.json';
|
||||
|
||||
async function main() {
|
||||
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8'));
|
||||
for (const field of ['main', 'typings']) {
|
||||
if (pkgJson["matrix_lib_"+field] !== undefined) {
|
||||
pkgJson[field] = pkgJson["matrix_lib_"+field];
|
||||
}
|
||||
}
|
||||
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -132,7 +132,7 @@ async function aliDownloadsKeys(): Promise<void> {
|
||||
// check that the localStorage is updated as we expect (not sure this is
|
||||
// an integration test, but meh)
|
||||
await Promise.all([p1(), p2()]);
|
||||
await aliTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
await aliTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
// @ts-ignore - protected
|
||||
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const devices = data.devices[bobUserId];
|
||||
@@ -494,6 +494,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
@@ -504,6 +505,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
@@ -567,6 +569,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
@@ -584,6 +587,9 @@ describe("MatrixClient crypto", () => {
|
||||
await firstSync(bobTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
bobTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {},
|
||||
);
|
||||
await bobRecvMessage();
|
||||
await bobEnablesEncryption();
|
||||
const ciphertext = await bobSendsReplyMessage();
|
||||
|
||||
@@ -833,6 +833,45 @@ describe("MatrixClient event timelines", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should stop paginating when it encounters no `end` token", () => {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
.respond(200, () => ({
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
}));
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.check(function(req) {
|
||||
const params = req.queryParams;
|
||||
expect(params.dir).toEqual("b");
|
||||
expect(params.from).toEqual("start_token0");
|
||||
expect(params.limit).toEqual("30");
|
||||
}).respond(200, () => ({
|
||||
start: "start_token0",
|
||||
chunk: [],
|
||||
}));
|
||||
|
||||
return Promise.all([
|
||||
(async () => {
|
||||
const tl = await client.getEventTimeline(timelineSet, EVENTS[0].event_id);
|
||||
const success = await client.paginateEventTimeline(tl, { backwards: true });
|
||||
expect(success).toBeFalsy();
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS)).toEqual(null);
|
||||
expect(tl.getPaginationToken(EventTimeline.FORWARDS)).toEqual("end_token0");
|
||||
})(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should allow you to paginate forwards", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
@@ -889,6 +889,7 @@ describe("MatrixClient", function() {
|
||||
};
|
||||
|
||||
const prom = client.getPushers();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushers").respond(200, response);
|
||||
await httpBackend.flush();
|
||||
expect(await prom).toStrictEqual(response);
|
||||
@@ -1074,6 +1075,29 @@ describe("MatrixClient", function() {
|
||||
expect(await prom).toStrictEqual(response);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestLoginToken", () => {
|
||||
it("should hit the expected API endpoint with UIA", async () => {
|
||||
const response = {};
|
||||
const uiaData = { foo: "baa" };
|
||||
const prom = client.requestLoginToken(uiaData);
|
||||
httpBackend
|
||||
.when("POST", "/unstable/org.matrix.msc3882/login/token", { auth: uiaData })
|
||||
.respond(200, response);
|
||||
await httpBackend.flush();
|
||||
expect(await prom).toStrictEqual(response);
|
||||
});
|
||||
|
||||
it("should hit the expected API endpoint without UIA", async () => {
|
||||
const response = {};
|
||||
const prom = client.requestLoginToken();
|
||||
httpBackend
|
||||
.when("POST", "/unstable/org.matrix.msc3882/login/token", {})
|
||||
.respond(200, response);
|
||||
await httpBackend.flush();
|
||||
expect(await prom).toStrictEqual(response);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function withThreadId(event, newThreadId) {
|
||||
|
||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import { Optional } from "matrix-events-sdk/lib/types";
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
@@ -25,6 +27,8 @@ import {
|
||||
RoomMemberEvent,
|
||||
UNSTABLE_MSC2716_MARKER,
|
||||
MatrixClient,
|
||||
ClientEvent,
|
||||
IndexedDBCryptoStore,
|
||||
} from "../../src";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
@@ -87,6 +91,8 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
|
||||
await client.initCrypto();
|
||||
|
||||
const roomId = "!cycles:example.org";
|
||||
|
||||
// First sync: an invite
|
||||
@@ -219,6 +225,44 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
return httpBackend!.flushAllExpected();
|
||||
});
|
||||
|
||||
it("should emit ClientEvent.Room when invited while crypto is disabled", async () => {
|
||||
const roomId = "!invite:example.org";
|
||||
|
||||
// First sync: an invite
|
||||
const inviteSyncRoomSection = {
|
||||
invite: {
|
||||
[roomId]: {
|
||||
invite_state: {
|
||||
events: [{
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "invite",
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: inviteSyncRoomSection,
|
||||
});
|
||||
|
||||
// First fire: an initial invite
|
||||
let fires = 0;
|
||||
client!.once(ClientEvent.Room, (room) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
});
|
||||
|
||||
// noinspection ES6MissingAwait
|
||||
client!.startClient();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(fires).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("initial sync", () => {
|
||||
@@ -1426,3 +1470,70 @@ describe("MatrixClient syncing", () => {
|
||||
return utils.syncPromise(client, numSyncs);
|
||||
}
|
||||
});
|
||||
|
||||
describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
const syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {},
|
||||
presence: {},
|
||||
};
|
||||
|
||||
it("should emit ClientEvent.Room when invited while using indexeddb crypto store", async () => {
|
||||
const idbTestClient = new TestClient(
|
||||
selfUserId,
|
||||
"DEVICE",
|
||||
selfAccessToken,
|
||||
undefined,
|
||||
{ cryptoStore: new IndexedDBCryptoStore(global.indexedDB, "tests") },
|
||||
);
|
||||
const idbHttpBackend = idbTestClient.httpBackend;
|
||||
const idbClient = idbTestClient.client;
|
||||
idbHttpBackend.when("GET", "/versions").respond(200, {});
|
||||
idbHttpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
|
||||
await idbClient.initCrypto();
|
||||
|
||||
const roomId = "!invite:example.org";
|
||||
|
||||
// First sync: an invite
|
||||
const inviteSyncRoomSection = {
|
||||
invite: {
|
||||
[roomId]: {
|
||||
invite_state: {
|
||||
events: [{
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "invite",
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
idbHttpBackend.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: inviteSyncRoomSection,
|
||||
});
|
||||
|
||||
// First fire: an initial invite
|
||||
let fires = 0;
|
||||
idbClient.once(ClientEvent.Room, (room) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
});
|
||||
|
||||
// noinspection ES6MissingAwait
|
||||
idbClient.startClient();
|
||||
await idbHttpBackend.flushAllExpected();
|
||||
|
||||
expect(fires).toBe(1);
|
||||
|
||||
idbHttpBackend.verifyNoOutstandingExpectation();
|
||||
idbClient.stopClient();
|
||||
idbHttpBackend.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,8 +29,11 @@ import {
|
||||
IDownloadKeyResult,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
IndexedDBCryptoStore,
|
||||
Room,
|
||||
} from "../../src/matrix";
|
||||
import { IDeviceKeys } from "../../src/crypto/dehydration";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -280,10 +283,13 @@ describe("megolm", () => {
|
||||
|
||||
it("Alice receives a megolm message", async () => {
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
@@ -326,10 +332,13 @@ describe("megolm", () => {
|
||||
it("Alice receives a megolm message before the session keys", async () => {
|
||||
// https://github.com/vector-im/element-web/issues/2273
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event, but don't send it yet
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
@@ -383,10 +392,13 @@ describe("megolm", () => {
|
||||
|
||||
it("Alice gets a second room_key message", async () => {
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted1 = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
@@ -468,6 +480,9 @@ describe("megolm", () => {
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
|
||||
@@ -541,13 +556,16 @@ describe("megolm", () => {
|
||||
|
||||
logger.log('Forcing alice to download our device keys');
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 2),
|
||||
]);
|
||||
|
||||
logger.log('Telling alice to block our device');
|
||||
@@ -592,6 +610,9 @@ describe("megolm", () => {
|
||||
|
||||
logger.log("Fetching bob's devices and marking known");
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
@@ -786,6 +807,10 @@ describe("megolm", () => {
|
||||
logger.log('Forcing alice to download our device keys');
|
||||
const downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
// so will this.
|
||||
const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test')
|
||||
.then(() => {
|
||||
@@ -805,9 +830,12 @@ describe("megolm", () => {
|
||||
it("Alice exports megolm keys and imports them to a new device", async () => {
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} }, failures: {} });
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
// establish an olm session with alice
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
@@ -855,6 +883,8 @@ describe("megolm", () => {
|
||||
await aliceTestClient.client.importRoomKeys(exported);
|
||||
await aliceTestClient.start();
|
||||
|
||||
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
@@ -927,10 +957,13 @@ describe("megolm", () => {
|
||||
|
||||
it("Alice can decrypt a message with falsey content", async () => {
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
@@ -985,10 +1018,13 @@ describe("megolm", () => {
|
||||
"should successfully decrypt bundled redaction events that don't include a room_id in their /sync data",
|
||||
async () => {
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
@@ -1045,4 +1081,283 @@ describe("megolm", () => {
|
||||
expect(redactionEvent.content.reason).toEqual("redaction test");
|
||||
},
|
||||
);
|
||||
|
||||
it("Alice receives shared history before being invited to a room by the sharer", async () => {
|
||||
const beccaTestClient = new TestClient(
|
||||
"@becca:localhost", "foobar", "bazquux",
|
||||
);
|
||||
await beccaTestClient.client.initCrypto();
|
||||
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
await beccaTestClient.start();
|
||||
|
||||
const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {});
|
||||
beccaTestClient.client.store.storeRoom(beccaRoom);
|
||||
await beccaTestClient.client.setRoomEncryption(ROOM_ID, { "algorithm": "m.megolm.v1.aes-sha2" });
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@becca:localhost",
|
||||
room_id: ROOM_ID,
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "test message",
|
||||
},
|
||||
});
|
||||
|
||||
await beccaTestClient.client.crypto.encryptEvent(event, beccaRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private properties
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
|
||||
const device = new DeviceInfo(beccaTestClient.client.deviceId);
|
||||
aliceTestClient.client.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => beccaTestClient.client.getUserId();
|
||||
|
||||
// Create an olm session for Becca and Alice's devices
|
||||
const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload();
|
||||
const aliceOtkId = Object.keys(aliceOtks)[0];
|
||||
const aliceOtk = aliceOtks[aliceOtkId];
|
||||
const p2pSession = new global.Olm.Session();
|
||||
await beccaTestClient.client.crypto.cryptoStore.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
beccaTestClient.client.crypto.cryptoStore.getAccount(txn, (pickledAccount: string) => {
|
||||
const account = new global.Olm.Account();
|
||||
try {
|
||||
account.unpickle(beccaTestClient.client.crypto.olmDevice.pickleKey, pickledAccount);
|
||||
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
|
||||
} finally {
|
||||
account.free();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const content = event.getWireContent();
|
||||
const groupSessionKey = await beccaTestClient.client.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
ROOM_ID,
|
||||
content.sender_key,
|
||||
content.session_id,
|
||||
);
|
||||
const encryptedForwardedKey = encryptOlmEvent({
|
||||
sender: "@becca:localhost",
|
||||
senderKey: beccaTestClient.getDeviceKey(),
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
plaincontent: {
|
||||
"algorithm": 'm.megolm.v1.aes-sha2',
|
||||
"room_id": ROOM_ID,
|
||||
"sender_key": content.sender_key,
|
||||
"sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key,
|
||||
"session_id": content.session_id,
|
||||
"session_key": groupSessionKey.key,
|
||||
"chain_index": groupSessionKey.chain_index,
|
||||
"forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain,
|
||||
"org.matrix.msc3061.shared_history": true,
|
||||
},
|
||||
plaintype: 'm.forwarded_room_key',
|
||||
});
|
||||
|
||||
// Alice receives shared history
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 1,
|
||||
to_device: { events: [encryptedForwardedKey] },
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
// Alice is invited to the room by Becca
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 2,
|
||||
rooms: { invite: { [ROOM_ID]: { invite_state: { events: [
|
||||
{
|
||||
sender: '@becca:localhost',
|
||||
type: 'm.room.encryption',
|
||||
state_key: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
},
|
||||
{
|
||||
sender: '@becca:localhost',
|
||||
type: 'm.room.member',
|
||||
state_key: '@alice:localhost',
|
||||
content: {
|
||||
membership: 'invite',
|
||||
},
|
||||
},
|
||||
] } } } },
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
// Alice has joined the room
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(
|
||||
200, getSyncResponse(["@alice:localhost", "@becca:localhost"]),
|
||||
);
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 4,
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: { timeline: { events: [event.event] } },
|
||||
},
|
||||
},
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const roomEvent = room.getLiveTimeline().getEvents()[0];
|
||||
expect(roomEvent.isEncrypted()).toBe(true);
|
||||
const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
|
||||
expect(decryptedEvent.getContent().body).toEqual('test message');
|
||||
|
||||
await beccaTestClient.stop();
|
||||
});
|
||||
|
||||
it("Alice receives shared history before being invited to a room by someone else", async () => {
|
||||
const beccaTestClient = new TestClient(
|
||||
"@becca:localhost", "foobar", "bazquux",
|
||||
);
|
||||
await beccaTestClient.client.initCrypto();
|
||||
|
||||
await aliceTestClient.start();
|
||||
await beccaTestClient.start();
|
||||
|
||||
const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {});
|
||||
beccaTestClient.client.store.storeRoom(beccaRoom);
|
||||
await beccaTestClient.client.setRoomEncryption(ROOM_ID, { "algorithm": "m.megolm.v1.aes-sha2" });
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@becca:localhost",
|
||||
room_id: ROOM_ID,
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "test message",
|
||||
},
|
||||
});
|
||||
|
||||
await beccaTestClient.client.crypto.encryptEvent(event, beccaRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private properties
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
|
||||
const device = new DeviceInfo(beccaTestClient.client.deviceId);
|
||||
aliceTestClient.client.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
// Create an olm session for Becca and Alice's devices
|
||||
const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload();
|
||||
const aliceOtkId = Object.keys(aliceOtks)[0];
|
||||
const aliceOtk = aliceOtks[aliceOtkId];
|
||||
const p2pSession = new global.Olm.Session();
|
||||
await beccaTestClient.client.crypto.cryptoStore.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
beccaTestClient.client.crypto.cryptoStore.getAccount(txn, (pickledAccount: string) => {
|
||||
const account = new global.Olm.Account();
|
||||
try {
|
||||
account.unpickle(beccaTestClient.client.crypto.olmDevice.pickleKey, pickledAccount);
|
||||
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
|
||||
} finally {
|
||||
account.free();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const content = event.getWireContent();
|
||||
const groupSessionKey = await beccaTestClient.client.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
ROOM_ID,
|
||||
content.sender_key,
|
||||
content.session_id,
|
||||
);
|
||||
const encryptedForwardedKey = encryptOlmEvent({
|
||||
sender: "@becca:localhost",
|
||||
senderKey: beccaTestClient.getDeviceKey(),
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
plaincontent: {
|
||||
"algorithm": 'm.megolm.v1.aes-sha2',
|
||||
"room_id": ROOM_ID,
|
||||
"sender_key": content.sender_key,
|
||||
"sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key,
|
||||
"session_id": content.session_id,
|
||||
"session_key": groupSessionKey.key,
|
||||
"chain_index": groupSessionKey.chain_index,
|
||||
"forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain,
|
||||
"org.matrix.msc3061.shared_history": true,
|
||||
},
|
||||
plaintype: 'm.forwarded_room_key',
|
||||
});
|
||||
|
||||
// Alice receives forwarded history from Becca
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 1,
|
||||
to_device: { events: [encryptedForwardedKey] },
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
// Alice is invited to the room by Charlie
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 2,
|
||||
rooms: { invite: { [ROOM_ID]: { invite_state: { events: [
|
||||
{
|
||||
sender: '@becca:localhost',
|
||||
type: 'm.room.encryption',
|
||||
state_key: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
},
|
||||
{
|
||||
sender: '@charlie:localhost',
|
||||
type: 'm.room.member',
|
||||
state_key: '@alice:localhost',
|
||||
content: {
|
||||
membership: 'invite',
|
||||
},
|
||||
},
|
||||
] } } } },
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
// Alice has joined the room
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(
|
||||
200, getSyncResponse(["@alice:localhost", "@becca:localhost", "@charlie:localhost"]),
|
||||
);
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 4,
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: { timeline: { events: [event.event] } },
|
||||
},
|
||||
},
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
// Decryption should fail, because Alice hasn't received any keys she can trust
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const roomEvent = room.getLiveTimeline().getEvents()[0];
|
||||
expect(roomEvent.isEncrypted()).toBe(true);
|
||||
const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
|
||||
expect(decryptedEvent.isDecryptionFailure()).toBe(true);
|
||||
|
||||
await beccaTestClient.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import '../olm-loader';
|
||||
|
||||
import { logger } from '../../src/logger';
|
||||
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { ClientEvent, EventType, MatrixClient, MsgType } from "../../src";
|
||||
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
|
||||
@@ -371,3 +371,14 @@ export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent>
|
||||
}
|
||||
|
||||
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
|
||||
|
||||
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
|
||||
app_display_name: "app",
|
||||
app_id: "123",
|
||||
data: {},
|
||||
device_display_name: "name",
|
||||
kind: "http",
|
||||
lang: "en",
|
||||
pushkey: "pushpush",
|
||||
...extra,
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
makeBeaconContent,
|
||||
makeBeaconInfoContent,
|
||||
makeTopicContent,
|
||||
parseBeaconContent,
|
||||
parseTopicContent,
|
||||
} from "../../src/content-helpers";
|
||||
|
||||
@@ -127,6 +128,66 @@ describe('Beacon content helpers', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseBeaconContent()", () => {
|
||||
it("should not explode when parsing an invalid beacon", () => {
|
||||
// deliberate cast to simulate wire content being invalid
|
||||
const result = parseBeaconContent({} as any);
|
||||
expect(result).toEqual({
|
||||
description: undefined,
|
||||
uri: undefined,
|
||||
timestamp: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse unstable values", () => {
|
||||
const uri = "urigoeshere";
|
||||
const description = "descriptiongoeshere";
|
||||
const timestamp = 1234;
|
||||
const result = parseBeaconContent({
|
||||
"org.matrix.msc3488.location": {
|
||||
uri,
|
||||
description,
|
||||
},
|
||||
"org.matrix.msc3488.ts": timestamp,
|
||||
|
||||
// relationship not used - just here to satisfy types
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: "$unused",
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({
|
||||
description,
|
||||
uri,
|
||||
timestamp,
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse stable values", () => {
|
||||
const uri = "urigoeshere";
|
||||
const description = "descriptiongoeshere";
|
||||
const timestamp = 1234;
|
||||
const result = parseBeaconContent({
|
||||
"m.location": {
|
||||
uri,
|
||||
description,
|
||||
},
|
||||
"m.ts": timestamp,
|
||||
|
||||
// relationship not used - just here to satisfy types
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: "$unused",
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({
|
||||
description,
|
||||
uri,
|
||||
timestamp,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Topic content helpers', () => {
|
||||
|
||||
+498
-12
@@ -15,6 +15,8 @@ import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import { logger } from '../../src/logger';
|
||||
import { MemoryStore } from "../../src";
|
||||
import { RoomKeyRequestState } from '../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
import { RoomMember } from '../../src/models/room-member';
|
||||
import { IStore } from '../../src/store';
|
||||
|
||||
const Olm = global.Olm;
|
||||
@@ -40,20 +42,52 @@ async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent>
|
||||
type: "m.forwarded_room_key",
|
||||
sender: client.getUserId(),
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: eventContent.sender_key,
|
||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||
session_id: eventContent.session_id,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
forwarding_curve25519_key_chain:
|
||||
key.forwarding_curve_key_chain,
|
||||
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||
"room_id": roomId,
|
||||
"sender_key": eventContent.sender_key,
|
||||
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
|
||||
"session_id": eventContent.session_id,
|
||||
"session_key": key.key,
|
||||
"chain_index": key.chain_index,
|
||||
"forwarding_curve25519_key_chain": key.forwarding_curve_key_chain,
|
||||
"org.matrix.msc3061.shared_history": true,
|
||||
},
|
||||
});
|
||||
// make onRoomKeyEvent think this was an encrypted event
|
||||
// @ts-ignore private property
|
||||
ksEvent.senderCurve25519Key = "akey";
|
||||
ksEvent.getWireType = () => "m.room.encrypted";
|
||||
ksEvent.getWireContent = () => {
|
||||
return {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
};
|
||||
};
|
||||
return ksEvent;
|
||||
}
|
||||
|
||||
function roomKeyEventForEvent(client: MatrixClient, event: MatrixEvent): MatrixEvent {
|
||||
const roomId = event.getRoomId();
|
||||
const eventContent = event.getWireContent();
|
||||
const key = client.crypto.olmDevice.getOutboundGroupSessionKey(eventContent.session_id);
|
||||
const ksEvent = new MatrixEvent({
|
||||
type: "m.room_key",
|
||||
sender: client.getUserId(),
|
||||
content: {
|
||||
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||
"room_id": roomId,
|
||||
"session_id": eventContent.session_id,
|
||||
"session_key": key.key,
|
||||
},
|
||||
});
|
||||
// make onRoomKeyEvent think this was an encrypted event
|
||||
// @ts-ignore private property
|
||||
ksEvent.senderCurve25519Key = event.getSenderKey();
|
||||
ksEvent.getWireType = () => "m.room.encrypted";
|
||||
ksEvent.getWireContent = () => {
|
||||
return {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
};
|
||||
};
|
||||
return ksEvent;
|
||||
}
|
||||
|
||||
@@ -95,7 +129,7 @@ describe("Crypto", function() {
|
||||
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
|
||||
event.getForwardingCurve25519KeyChain = () => ["not empty"];
|
||||
event.isKeySourceUntrusted = () => false;
|
||||
event.isKeySourceUntrusted = () => true;
|
||||
event.getClaimedEd25519Key =
|
||||
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
|
||||
@@ -233,6 +267,7 @@ describe("Crypto", function() {
|
||||
describe('Key requests', function() {
|
||||
let aliceClient: MatrixClient;
|
||||
let bobClient: MatrixClient;
|
||||
let claraClient: MatrixClient;
|
||||
|
||||
beforeEach(async function() {
|
||||
aliceClient = (new TestClient(
|
||||
@@ -241,22 +276,35 @@ describe("Crypto", function() {
|
||||
bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
claraClient = (new TestClient(
|
||||
"@clara:example.com", "claradevice",
|
||||
)).client;
|
||||
await aliceClient.initCrypto();
|
||||
await bobClient.initCrypto();
|
||||
await claraClient.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
claraClient.stopClient();
|
||||
});
|
||||
|
||||
it("does not cancel keyshare requests if some messages are not decrypted", async function() {
|
||||
it("does not cancel keyshare requests until all messages are decrypted with trusted keys", async function() {
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
// Make Bob invited by Alice so Bob will accept Alice's forwarded keys
|
||||
bobRoom.currentState.setStateEvents([new MatrixEvent({
|
||||
type: "m.room.member",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
content: { membership: "invite" },
|
||||
state_key: "@bob:example.com",
|
||||
})]);
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
@@ -302,6 +350,9 @@ describe("Crypto", function() {
|
||||
}
|
||||
}));
|
||||
|
||||
const device = new DeviceInfo(aliceClient.deviceId);
|
||||
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
@@ -314,6 +365,8 @@ describe("Crypto", function() {
|
||||
// the first message can't be decrypted yet, but the second one
|
||||
// can
|
||||
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
|
||||
bobClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await decryptEventsPromise;
|
||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||
@@ -340,8 +393,24 @@ describe("Crypto", function() {
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await decryptEventPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
expect(events[0].isKeySourceUntrusted()).toBeTruthy();
|
||||
await sleep(1);
|
||||
// the room key request should be gone since we've now decrypted everything
|
||||
// the room key request should still be there, since we've
|
||||
// decrypted everything with an untrusted key
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeDefined();
|
||||
|
||||
// Now share a trusted room key event so Bob will re-decrypt the messages.
|
||||
// Bob will backfill trust when they receive a trusted session with a higher
|
||||
// index that connects to an untrusted session with a lower index.
|
||||
const roomKeyEvent = roomKeyEventForEvent(aliceClient, events[1]);
|
||||
const trustedDecryptEventPromise = awaitEvent(events[0], "Event.decrypted");
|
||||
await bobDecryptor.onRoomKeyEvent(roomKeyEvent);
|
||||
await trustedDecryptEventPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
expect(events[0].isKeySourceUntrusted()).toBeFalsy();
|
||||
await sleep(1);
|
||||
// now the room key request should be gone, since there's
|
||||
// no better key to wait for
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -383,6 +452,9 @@ describe("Crypto", function() {
|
||||
// decryption keys yet
|
||||
}
|
||||
|
||||
const device = new DeviceInfo(aliceClient.deviceId);
|
||||
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
@@ -462,6 +534,420 @@ describe("Crypto", function() {
|
||||
expect(aliceSendToDevice).toBeCalledTimes(3);
|
||||
expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
|
||||
});
|
||||
|
||||
it("should accept forwarded keys which it requested", async function() {
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
const events = [
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "1",
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$2",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "2",
|
||||
},
|
||||
}),
|
||||
];
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private properties
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const device = new DeviceInfo(aliceClient.deviceId);
|
||||
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const cryptoStore = bobClient.crypto.cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: senderKey,
|
||||
session_id: sessionId,
|
||||
};
|
||||
const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody);
|
||||
expect(outgoingReq).toBeDefined();
|
||||
await cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
outgoingReq.requestId, RoomKeyRequestState.Unsent,
|
||||
{ state: RoomKeyRequestState.Sent },
|
||||
);
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
const decryptEventsPromise = Promise.all(events.map((ev) => {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}));
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
);
|
||||
expect(key).not.toBeNull();
|
||||
await decryptEventsPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
});
|
||||
|
||||
it("should accept forwarded keys from the user who invited it to the room", async function() {
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
const claraRoom = new Room(roomId, claraClient, "@clara:example.com", {});
|
||||
// Make Bob invited by Clara
|
||||
bobRoom.currentState.setStateEvents([new MatrixEvent({
|
||||
type: "m.room.member",
|
||||
sender: "@clara:example.com",
|
||||
room_id: roomId,
|
||||
content: { membership: "invite" },
|
||||
state_key: "@bob:example.com",
|
||||
})]);
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
claraClient.store.storeRoom(claraRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await claraClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
const events = [
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "1",
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$2",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "2",
|
||||
},
|
||||
}),
|
||||
];
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private properties
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const device = new DeviceInfo(claraClient.deviceId);
|
||||
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@clara:example.com";
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
const decryptEventsPromise = Promise.all(events.map((ev) => {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}));
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
ksEvent.event.sender = claraClient.getUserId(),
|
||||
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId());
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
);
|
||||
expect(key).not.toBeNull();
|
||||
await decryptEventsPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
});
|
||||
|
||||
it("should accept forwarded keys from one of its own user's other devices", async function() {
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
const events = [
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "1",
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$2",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "2",
|
||||
},
|
||||
}),
|
||||
];
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private properties
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const device = new DeviceInfo(claraClient.deviceId);
|
||||
device.verified = DeviceInfo.DeviceVerification.VERIFIED;
|
||||
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:example.com";
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
const decryptEventsPromise = Promise.all(events.map((ev) => {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}));
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
ksEvent.event.sender = bobClient.getUserId(),
|
||||
ksEvent.sender = new RoomMember(roomId, bobClient.getUserId());
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
);
|
||||
expect(key).not.toBeNull();
|
||||
await decryptEventsPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
});
|
||||
|
||||
it("should not accept unexpected forwarded keys for a room it's in", async function() {
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
const claraRoom = new Room(roomId, claraClient, "@clara:example.com", {});
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
claraClient.store.storeRoom(claraRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await claraClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
const events = [
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "1",
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$2",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "2",
|
||||
},
|
||||
}),
|
||||
];
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private properties
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const device = new DeviceInfo(claraClient.deviceId);
|
||||
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
ksEvent.event.sender = claraClient.getUserId(),
|
||||
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId());
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
);
|
||||
expect(key).toBeNull();
|
||||
});
|
||||
|
||||
it("should park forwarded keys for a room it's not in", async function() {
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
const events = [
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "1",
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$2",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "2",
|
||||
},
|
||||
}),
|
||||
];
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
// @ts-ignore private properties
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
}));
|
||||
|
||||
const device = new DeviceInfo(aliceClient.deviceId);
|
||||
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
const content = events[0].getWireContent();
|
||||
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const bobKey = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
content.sender_key,
|
||||
content.session_id,
|
||||
);
|
||||
expect(bobKey).toBeNull();
|
||||
|
||||
const aliceKey = await aliceClient.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
content.sender_key,
|
||||
content.session_id,
|
||||
);
|
||||
const parked = await bobClient.crypto.cryptoStore.takeParkedSharedHistory(roomId);
|
||||
expect(parked).toEqual([{
|
||||
senderId: aliceClient.getUserId(),
|
||||
senderKey: content.sender_key,
|
||||
sessionId: content.session_id,
|
||||
sessionKey: aliceKey.key,
|
||||
keysClaimed: { ed25519: aliceKey.sender_claimed_ed25519_key },
|
||||
forwardingCurve25519KeyChain: ["akey"],
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Secret storage', function() {
|
||||
|
||||
@@ -110,6 +110,12 @@ describe("MegolmDecryption", function() {
|
||||
senderCurve25519Key: "SENDER_CURVE25519",
|
||||
claimedEd25519Key: "SENDER_ED25519",
|
||||
};
|
||||
event.getWireType = () => "m.room.encrypted";
|
||||
event.getWireContent = () => {
|
||||
return {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
};
|
||||
};
|
||||
|
||||
const mockCrypto = {
|
||||
decryptEvent: function() {
|
||||
|
||||
@@ -214,6 +214,12 @@ describe("MegolmBackup", function() {
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
});
|
||||
event.getWireType = () => "m.room.encrypted";
|
||||
event.getWireContent = () => {
|
||||
return {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
};
|
||||
};
|
||||
const decryptedData = {
|
||||
clearEvent: {
|
||||
type: 'm.room_key',
|
||||
|
||||
@@ -26,6 +26,7 @@ import { logger } from '../../../src/logger';
|
||||
import * as utils from "../../../src/utils";
|
||||
import { ICreateClientOpts } from '../../../src/client';
|
||||
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
|
||||
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
@@ -250,13 +251,13 @@ describe("Secrets", function() {
|
||||
|
||||
osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
"VAX": {
|
||||
verified: 0,
|
||||
known: false,
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:VAX": vaxDevice.deviceEd25519Key,
|
||||
"curve25519:VAX": vaxDevice.deviceCurve25519Key,
|
||||
},
|
||||
verified: DeviceInfo.DeviceVerification.VERIFIED,
|
||||
},
|
||||
});
|
||||
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
@@ -280,10 +281,12 @@ describe("Secrets", function() {
|
||||
Object.values(otks)[0],
|
||||
);
|
||||
|
||||
const request = await secretStorage.request("foo", ["VAX"]);
|
||||
const secret = await request.promise;
|
||||
osborne2.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
osborne2.client.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const request = await secretStorage.request("foo", ["VAX"]);
|
||||
await request.promise; // return value not used
|
||||
|
||||
expect(secret).toBe("bar");
|
||||
osborne2.stop();
|
||||
vax.stop();
|
||||
clearTestClientTimeouts();
|
||||
|
||||
@@ -464,7 +464,7 @@ describe("SAS verification", function() {
|
||||
},
|
||||
);
|
||||
|
||||
alice.client.setDeviceVerified = jest.fn();
|
||||
alice.client.crypto.setDeviceVerification = jest.fn();
|
||||
alice.client.getDeviceEd25519Key = () => {
|
||||
return "alice+base64+ed25519+key";
|
||||
};
|
||||
@@ -482,7 +482,7 @@ describe("SAS verification", function() {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
bob.client.setDeviceVerified = jest.fn();
|
||||
bob.client.crypto.setDeviceVerification = jest.fn();
|
||||
bob.client.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
@@ -565,10 +565,24 @@ describe("SAS verification", function() {
|
||||
]);
|
||||
|
||||
// make sure Alice and Bob verified each other
|
||||
expect(alice.client.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId);
|
||||
expect(bob.client.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId);
|
||||
expect(alice.client.crypto.setDeviceVerification)
|
||||
.toHaveBeenCalledWith(
|
||||
bob.client.getUserId(),
|
||||
bob.client.deviceId,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
{ "ed25519:Dynabook": "bob+base64+ed25519+key" },
|
||||
);
|
||||
expect(bob.client.crypto.setDeviceVerification)
|
||||
.toHaveBeenCalledWith(
|
||||
alice.client.getUserId(),
|
||||
alice.client.deviceId,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
{ "ed25519:Osborne2": "alice+base64+ed25519+key" },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2022 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 { LocalNotificationSettings } from "../../src/@types/local_notifications";
|
||||
import { LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixClient } from "../../src/matrix";
|
||||
import { TestClient } from '../TestClient';
|
||||
|
||||
let client: MatrixClient;
|
||||
|
||||
describe("Local notification settings", () => {
|
||||
beforeEach(() => {
|
||||
client = (new TestClient(
|
||||
"@alice:matrix.org", "123", undefined, undefined, undefined,
|
||||
)).client;
|
||||
client.setAccountData = jest.fn();
|
||||
});
|
||||
|
||||
describe("Lets you set local notification settings", () => {
|
||||
it("stores settings in account data", () => {
|
||||
const deviceId = "device";
|
||||
const settings: LocalNotificationSettings = { is_silenced: true };
|
||||
client.setLocalNotificationSettings(deviceId, settings);
|
||||
|
||||
expect(client.setAccountData).toHaveBeenCalledWith(
|
||||
`${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`,
|
||||
settings,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -565,7 +565,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
rooms = {};
|
||||
rooms[tree.roomId] = parentRoom;
|
||||
(<any>tree).room = parentRoom; // override readonly
|
||||
client.getRoom = (r) => rooms[r];
|
||||
client.getRoom = (r) => rooms[r ?? ""];
|
||||
|
||||
clientSendStateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
|
||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { REFERENCE_RELATION } from "matrix-events-sdk";
|
||||
|
||||
import { MatrixEvent } from "../../../src";
|
||||
import { M_BEACON_INFO } from "../../../src/@types/beacon";
|
||||
import {
|
||||
@@ -431,6 +433,27 @@ describe('Beacon', () => {
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should ignore invalid beacon events", () => {
|
||||
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
|
||||
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||
|
||||
const ev = new MatrixEvent({
|
||||
type: M_BEACON_INFO.name,
|
||||
sender: userId,
|
||||
room_id: roomId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: beacon.beaconInfoId,
|
||||
},
|
||||
},
|
||||
});
|
||||
beacon.addLocations([ev]);
|
||||
|
||||
expect(beacon.latestLocationEvent).toBeFalsy();
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when beacon is live with a start timestamp is in the future', () => {
|
||||
it('ignores locations before the beacon start timestamp', () => {
|
||||
const startTimestamp = now + 60000;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
Copyright 2022 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 MockHttpBackend from 'matrix-mock-request';
|
||||
|
||||
import { IHttpOpts, MatrixClient, PUSHER_ENABLED } from "../../src/matrix";
|
||||
import { mkPusher } from '../test-utils/test-utils';
|
||||
|
||||
const realSetTimeout = setTimeout;
|
||||
function flushPromises() {
|
||||
return new Promise(r => {
|
||||
realSetTimeout(r, 1);
|
||||
});
|
||||
}
|
||||
|
||||
let client: MatrixClient;
|
||||
let httpBackend: MockHttpBackend;
|
||||
|
||||
describe("Pushers", () => {
|
||||
beforeEach(() => {
|
||||
httpBackend = new MockHttpBackend();
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
request: httpBackend.requestFn as unknown as IHttpOpts["request"],
|
||||
});
|
||||
});
|
||||
|
||||
describe("supports remotely toggling push notifications", () => {
|
||||
it("migration support when connecting to a legacy homeserver", async () => {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
unstable_features: {
|
||||
"org.matrix.msc3881": false,
|
||||
},
|
||||
});
|
||||
httpBackend.when("GET", "/pushers").respond(200, {
|
||||
pushers: [
|
||||
mkPusher(),
|
||||
mkPusher({ [PUSHER_ENABLED.name]: true }),
|
||||
mkPusher({ [PUSHER_ENABLED.name]: false }),
|
||||
],
|
||||
});
|
||||
|
||||
const promise = client.getPushers();
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
|
||||
const response = await promise;
|
||||
|
||||
expect(response.pushers[0][PUSHER_ENABLED.name]).toBe(true);
|
||||
expect(response.pushers[1][PUSHER_ENABLED.name]).toBe(true);
|
||||
expect(response.pushers[2][PUSHER_ENABLED.name]).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
Copyright 2022 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 MockHttpBackend from 'matrix-mock-request';
|
||||
|
||||
import { ReceiptType } from '../../src/@types/read_receipts';
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { IHttpOpts } from '../../src/http-api';
|
||||
import { EventType } from '../../src/matrix';
|
||||
import { MAIN_ROOM_TIMELINE } from '../../src/models/read-receipt';
|
||||
import { encodeUri } from '../../src/utils';
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
|
||||
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
|
||||
// other async methods which break the event loop, letting scheduled promise
|
||||
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
|
||||
// it manually (this is what sinon does under the hood). We do both in a loop
|
||||
// until the thing we expect happens: hopefully this is the least flakey way
|
||||
// and avoids assuming anything about the app's behaviour.
|
||||
const realSetTimeout = setTimeout;
|
||||
function flushPromises() {
|
||||
return new Promise(r => {
|
||||
realSetTimeout(r, 1);
|
||||
});
|
||||
}
|
||||
|
||||
let client: MatrixClient;
|
||||
let httpBackend: MockHttpBackend;
|
||||
|
||||
const THREAD_ID = "$thread_event_id";
|
||||
const ROOM_ID = "!123:matrix.org";
|
||||
|
||||
const threadEvent = utils.mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: "@bob:matrix.org",
|
||||
room: ROOM_ID,
|
||||
content: {
|
||||
"body": "Hello from a thread",
|
||||
"m.relates_to": {
|
||||
"event_id": THREAD_ID,
|
||||
"m.in_reply_to": {
|
||||
"event_id": THREAD_ID,
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const roomEvent = utils.mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: "@bob:matrix.org",
|
||||
room: ROOM_ID,
|
||||
content: {
|
||||
"body": "Hello from a room",
|
||||
},
|
||||
});
|
||||
|
||||
function mockServerSideSupport(client, hasServerSideSupport) {
|
||||
const doesServerSupportUnstableFeature = client.doesServerSupportUnstableFeature;
|
||||
client.doesServerSupportUnstableFeature = (unstableFeature) => {
|
||||
if (unstableFeature === "org.matrix.msc3771") {
|
||||
return Promise.resolve(hasServerSideSupport);
|
||||
} else {
|
||||
return doesServerSupportUnstableFeature(unstableFeature);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe("Read receipt", () => {
|
||||
beforeEach(() => {
|
||||
httpBackend = new MockHttpBackend();
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
request: httpBackend.requestFn as unknown as IHttpOpts["request"],
|
||||
});
|
||||
client.isGuest = () => false;
|
||||
});
|
||||
|
||||
describe("sendReceipt", () => {
|
||||
it("sends a thread read receipt", async () => {
|
||||
httpBackend.when(
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId(),
|
||||
}),
|
||||
).check((request) => {
|
||||
expect(request.data.thread_id).toEqual(THREAD_ID);
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, true);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("sends a room read receipt", async () => {
|
||||
httpBackend.when(
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: roomEvent.getId(),
|
||||
}),
|
||||
).check((request) => {
|
||||
expect(request.data.thread_id).toEqual(MAIN_ROOM_TIMELINE);
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, true);
|
||||
client.sendReceipt(roomEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("sends a room read receipt when there's no server support", async () => {
|
||||
httpBackend.when(
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId(),
|
||||
}),
|
||||
).check((request) => {
|
||||
expect(request.data.thread_id).toBeUndefined();
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, false);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
});
|
||||
});
|
||||
+30
-16
@@ -32,13 +32,14 @@ import {
|
||||
RoomEvent,
|
||||
} from "../../src";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import { IWrappedReceipt, Room } from "../../src/models/room";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { RoomState } from "../../src/models/room-state";
|
||||
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { Thread, ThreadEvent } from "../../src/models/thread";
|
||||
import { WrappedReceipt } from "../../src/models/read-receipt";
|
||||
|
||||
describe("Room", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -1430,6 +1431,19 @@ describe("Room", function() {
|
||||
expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasUserReadUpTo", function() {
|
||||
it("should acknowledge if an event has been read", function() {
|
||||
const ts = 13787898424;
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, ts),
|
||||
]));
|
||||
expect(room.hasUserReadEvent(userB, eventToAck.getId())).toEqual(true);
|
||||
});
|
||||
it("return false for an unknown event", function() {
|
||||
expect(room.hasUserReadEvent(userB, "unknown_event")).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("tags", function() {
|
||||
@@ -2439,8 +2453,8 @@ describe("Room", function() {
|
||||
const room = new Room(roomId, client, userA);
|
||||
|
||||
it("handles missing receipt type", () => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
||||
return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as IWrappedReceipt : null;
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||
return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as WrappedReceipt : null;
|
||||
};
|
||||
|
||||
expect(room.getEventReadUpTo(userA)).toEqual("eventId");
|
||||
@@ -2448,12 +2462,12 @@ describe("Room", function() {
|
||||
|
||||
describe("prefers newer receipt", () => {
|
||||
it("should compare correctly using timelines", () => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||
if (receiptType === ReceiptType.ReadPrivate) {
|
||||
return { eventId: "eventId1" } as IWrappedReceipt;
|
||||
return { eventId: "eventId1" } as WrappedReceipt;
|
||||
}
|
||||
if (receiptType === ReceiptType.Read) {
|
||||
return { eventId: "eventId2" } as IWrappedReceipt;
|
||||
return { eventId: "eventId2" } as WrappedReceipt;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -2473,12 +2487,12 @@ describe("Room", function() {
|
||||
room.getUnfilteredTimelineSet = () => ({
|
||||
compareEventOrdering: (_1, _2) => null,
|
||||
} as EventTimelineSet);
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||
if (receiptType === ReceiptType.ReadPrivate) {
|
||||
return { eventId: "eventId1", data: { ts: i === 1 ? 2 : 1 } } as IWrappedReceipt;
|
||||
return { eventId: "eventId1", data: { ts: i === 1 ? 2 : 1 } } as WrappedReceipt;
|
||||
}
|
||||
if (receiptType === ReceiptType.Read) {
|
||||
return { eventId: "eventId2", data: { ts: i === 2 ? 2 : 1 } } as IWrappedReceipt;
|
||||
return { eventId: "eventId2", data: { ts: i === 2 ? 2 : 1 } } as WrappedReceipt;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -2491,9 +2505,9 @@ describe("Room", function() {
|
||||
room.getUnfilteredTimelineSet = () => ({
|
||||
compareEventOrdering: (_1, _2) => null,
|
||||
} as EventTimelineSet);
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||
if (receiptType === ReceiptType.Read) {
|
||||
return { eventId: "eventId2", data: { ts: 1 } } as IWrappedReceipt;
|
||||
return { eventId: "eventId2", data: { ts: 1 } } as WrappedReceipt;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -2510,12 +2524,12 @@ describe("Room", function() {
|
||||
});
|
||||
|
||||
it("should give precedence to m.read.private", () => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||
if (receiptType === ReceiptType.ReadPrivate) {
|
||||
return { eventId: "eventId1" } as IWrappedReceipt;
|
||||
return { eventId: "eventId1" } as WrappedReceipt;
|
||||
}
|
||||
if (receiptType === ReceiptType.Read) {
|
||||
return { eventId: "eventId2" } as IWrappedReceipt;
|
||||
return { eventId: "eventId2" } as WrappedReceipt;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -2524,9 +2538,9 @@ describe("Room", function() {
|
||||
});
|
||||
|
||||
it("should give precedence to m.read", () => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||
if (receiptType === ReceiptType.Read) {
|
||||
return { eventId: "eventId3" } as IWrappedReceipt;
|
||||
return { eventId: "eventId3" } as WrappedReceipt;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'jest-localstorage-mock';
|
||||
import { IndexedDBStore, IStateEventWithRoomId, MemoryStore } from "../../../src";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
|
||||
import { defer } from "../../../src/utils";
|
||||
|
||||
describe("IndexedDBStore", () => {
|
||||
afterEach(() => {
|
||||
@@ -111,4 +112,57 @@ describe("IndexedDBStore", () => {
|
||||
await store.setPendingEvents(roomId, []);
|
||||
expect(localStorage.getItem("mx_pending_events_" + roomId)).toBeNull();
|
||||
});
|
||||
|
||||
it("should resolve isNewlyCreated to true if no database existed initially", async () => {
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB,
|
||||
dbName: "db1",
|
||||
localStorage,
|
||||
});
|
||||
await store.startup();
|
||||
|
||||
await expect(store.isNewlyCreated()).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
it("should resolve isNewlyCreated to false if database existed already", async () => {
|
||||
let store = new IndexedDBStore({
|
||||
indexedDB,
|
||||
dbName: "db2",
|
||||
localStorage,
|
||||
});
|
||||
await store.startup();
|
||||
|
||||
store = new IndexedDBStore({
|
||||
indexedDB,
|
||||
dbName: "db2",
|
||||
localStorage,
|
||||
});
|
||||
await store.startup();
|
||||
|
||||
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
|
||||
});
|
||||
|
||||
it("should resolve isNewlyCreated to false if database existed already but needs upgrade", async () => {
|
||||
const deferred = defer<Event>();
|
||||
// seed db3 to Version 1 so it forces a migration
|
||||
const req = indexedDB.open("matrix-js-sdk:db3", 1);
|
||||
req.onupgradeneeded = () => {
|
||||
const db = req.result;
|
||||
db.createObjectStore("users", { keyPath: ["userId"] });
|
||||
db.createObjectStore("accountData", { keyPath: ["type"] });
|
||||
db.createObjectStore("sync", { keyPath: ["clobber"] });
|
||||
};
|
||||
req.onsuccess = deferred.resolve;
|
||||
await deferred.promise;
|
||||
req.result.close();
|
||||
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB,
|
||||
dbName: "db3",
|
||||
localStorage,
|
||||
});
|
||||
await store.startup();
|
||||
|
||||
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -156,9 +156,13 @@ export interface IPusher {
|
||||
lang: string;
|
||||
profile_tag?: string;
|
||||
pushkey: string;
|
||||
enabled?: boolean | null | undefined;
|
||||
"org.matrix.msc3881.enabled"?: boolean | null | undefined;
|
||||
device_id?: string | null;
|
||||
"org.matrix.msc3881.device_id"?: string | null;
|
||||
}
|
||||
|
||||
export interface IPusherRequest extends IPusher {
|
||||
export interface IPusherRequest extends Omit<IPusher, "device_id" | "org.matrix.msc3881.device_id"> {
|
||||
append?: boolean;
|
||||
}
|
||||
|
||||
|
||||
+28
-3
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
|
||||
// disable lint because these are wire responses
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
@@ -29,7 +31,7 @@ export interface IRefreshTokenResponse {
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
/**
|
||||
* Response to GET login flows as per https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3login
|
||||
* Response to GET login flows as per https://spec.matrix.org/v1.3/client-server-api/#get_matrixclientv3login
|
||||
*/
|
||||
export interface ILoginFlowsResponse {
|
||||
flows: LoginFlow[];
|
||||
@@ -45,13 +47,20 @@ export interface IPasswordFlow extends ILoginFlow {
|
||||
type: "m.login.password";
|
||||
}
|
||||
|
||||
export const DELEGATED_OIDC_COMPATIBILITY = new UnstableValue(
|
||||
"delegated_oidc_compatibility",
|
||||
"org.matrix.msc3824.delegated_oidc_compatibility",
|
||||
);
|
||||
|
||||
/**
|
||||
* Representation of SSO flow as per https://spec.matrix.org/latest/client-server-api/#client-login-via-sso
|
||||
* Representation of SSO flow as per https://spec.matrix.org/v1.3/client-server-api/#client-login-via-sso
|
||||
*/
|
||||
export interface ISSOFlow extends ILoginFlow {
|
||||
type: "m.login.sso" | "m.login.cas";
|
||||
// eslint-disable-next-line camelcase
|
||||
identity_providers?: IIdentityProvider[];
|
||||
[DELEGATED_OIDC_COMPATIBILITY.name]?: boolean;
|
||||
[DELEGATED_OIDC_COMPATIBILITY.altName]?: boolean;
|
||||
}
|
||||
|
||||
export enum IdentityProviderBrand {
|
||||
@@ -71,7 +80,7 @@ export interface IIdentityProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters to login request as per https://spec.matrix.org/latest/client-server-api/#login
|
||||
* Parameters to login request as per https://spec.matrix.org/v1.3/client-server-api/#login
|
||||
*/
|
||||
/* eslint-disable camelcase */
|
||||
export interface ILoginParams {
|
||||
@@ -90,3 +99,19 @@ export enum SSOAction {
|
||||
/** The user intends to register for a new account */
|
||||
REGISTER = "register",
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a successful [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882)
|
||||
* `m.login.token` issuance request.
|
||||
* Note that this is UNSTABLE and subject to breaking changes without notice.
|
||||
*/
|
||||
export interface LoginTokenPostResponse {
|
||||
/**
|
||||
* The token to use with `m.login.token` to authenticate.
|
||||
*/
|
||||
login_token: string;
|
||||
/**
|
||||
* Expiration in seconds.
|
||||
*/
|
||||
expires_in: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
Copyright 2022 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.
|
||||
*/
|
||||
|
||||
export type OlmGroupSessionExtraData = {
|
||||
untrusted?: boolean;
|
||||
sharedHistory?: boolean;
|
||||
};
|
||||
@@ -191,6 +191,33 @@ export const EVENT_VISIBILITY_CHANGE_TYPE = new UnstableValue(
|
||||
"m.visibility",
|
||||
"org.matrix.msc3531.visibility");
|
||||
|
||||
/**
|
||||
* https://github.com/matrix-org/matrix-doc/pull/3881
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export const PUSHER_ENABLED = new UnstableValue(
|
||||
"enabled",
|
||||
"org.matrix.msc3881.enabled");
|
||||
|
||||
/**
|
||||
* https://github.com/matrix-org/matrix-doc/pull/3881
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export const PUSHER_DEVICE_ID = new UnstableValue(
|
||||
"device_id",
|
||||
"org.matrix.msc3881.device_id");
|
||||
|
||||
/**
|
||||
* https://github.com/matrix-org/matrix-doc/pull/3890
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue(
|
||||
"m.local_notification_settings",
|
||||
"org.matrix.msc3890.local_notification_settings");
|
||||
|
||||
export interface IEncryptedFile {
|
||||
url: string;
|
||||
mimetype?: string;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2022 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.
|
||||
*/
|
||||
|
||||
export interface LocalNotificationSettings {
|
||||
is_silenced: boolean;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2022 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 { IAuthData } from "../interactive-auth";
|
||||
|
||||
/**
|
||||
* Helper type to represent HTTP request body for a UIA enabled endpoint
|
||||
*/
|
||||
export type UIARequest<T> = T & {
|
||||
auth?: IAuthData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper type to represent HTTP response body for a UIA enabled endpoint
|
||||
*/
|
||||
export type UIAResponse<T> = T | IAuthData;
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 - 2022 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.
|
||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Optional } from "matrix-events-sdk/lib/types";
|
||||
|
||||
/**
|
||||
* Represents a simple Matrix namespaced value. This will assume that if a stable prefix
|
||||
* is provided that the stable prefix should be used when representing the identifier.
|
||||
@@ -54,7 +56,7 @@ export class NamespacedValue<S extends string, U extends string> {
|
||||
|
||||
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
|
||||
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
|
||||
public findIn<T>(obj: any): T {
|
||||
public findIn<T>(obj: any): Optional<T> {
|
||||
let val: T;
|
||||
if (this.name) {
|
||||
val = obj?.[this.name];
|
||||
|
||||
+120
-25
@@ -158,7 +158,9 @@ import {
|
||||
} from "./@types/requests";
|
||||
import {
|
||||
EventType,
|
||||
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
|
||||
MsgType,
|
||||
PUSHER_ENABLED,
|
||||
RelationType,
|
||||
RoomCreateTypeField,
|
||||
RoomType,
|
||||
@@ -188,7 +190,7 @@ import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, Rule
|
||||
import { IThreepid } from "./@types/threepids";
|
||||
import { CryptoStore } from "./crypto/store/base";
|
||||
import { MediaHandler } from "./webrtc/mediaHandler";
|
||||
import { IRefreshTokenResponse, SSOAction } from "./@types/auth";
|
||||
import { LoginTokenPostResponse, ILoginFlowsResponse, IRefreshTokenResponse, SSOAction } from "./@types/auth";
|
||||
import { TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
import { ReceiptType } from "./@types/read_receipts";
|
||||
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
|
||||
@@ -198,7 +200,10 @@ import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
|
||||
import { UnstableValue } from "./NamespacedValue";
|
||||
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
|
||||
import { ToDeviceBatch } from "./models/ToDeviceMessage";
|
||||
import { MAIN_ROOM_TIMELINE } from "./models/read-receipt";
|
||||
import { IgnoredInvites } from "./models/invites-ignorer";
|
||||
import { UIARequest, UIAResponse } from "./@types/uia";
|
||||
import { LocalNotificationSettings } from "./@types/local_notifications";
|
||||
|
||||
export type Store = IStore;
|
||||
|
||||
@@ -523,10 +528,16 @@ interface IServerVersions {
|
||||
unstable_features: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export const M_AUTHENTICATION = new UnstableValue(
|
||||
"m.authentication",
|
||||
"org.matrix.msc2965.authentication",
|
||||
);
|
||||
|
||||
export interface IClientWellKnown {
|
||||
[key: string]: any;
|
||||
"m.homeserver"?: IWellKnownConfig;
|
||||
"m.identity_server"?: IWellKnownConfig;
|
||||
[M_AUTHENTICATION.name]?: IDelegatedAuthConfig; // MSC2965
|
||||
}
|
||||
|
||||
export interface IWellKnownConfig {
|
||||
@@ -538,6 +549,13 @@ export interface IWellKnownConfig {
|
||||
base_url?: string | null;
|
||||
}
|
||||
|
||||
export interface IDelegatedAuthConfig { // MSC2965
|
||||
/** The OIDC Provider/issuer the client should use */
|
||||
issuer: string;
|
||||
/** The optional URL of the web UI where the user can manage their account */
|
||||
account?: string;
|
||||
}
|
||||
|
||||
interface IKeyBackupPath {
|
||||
path: string;
|
||||
queryData?: {
|
||||
@@ -905,7 +923,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public urlPreviewCache: { [key: string]: Promise<IPreviewUrlResponse> } = {};
|
||||
public identityServer: IIdentityServerProvider;
|
||||
public http: MatrixHttpApi; // XXX: Intended private, used in code.
|
||||
public crypto: Crypto; // XXX: Intended private, used in code.
|
||||
public crypto?: Crypto; // XXX: Intended private, used in code.
|
||||
public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code.
|
||||
public callEventHandler: CallEventHandler; // XXX: Intended private, used in code.
|
||||
public supportsCallTransfer = false; // XXX: Intended private, used in code.
|
||||
@@ -3332,7 +3350,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param {string} roomId The room ID
|
||||
* @return {Room|null} The Room or null if it doesn't exist or there is no data store.
|
||||
*/
|
||||
public getRoom(roomId: string): Room | null {
|
||||
public getRoom(roomId: string | undefined): Room | null {
|
||||
if (!roomId) {
|
||||
return null;
|
||||
}
|
||||
return this.store.getRoom(roomId);
|
||||
}
|
||||
|
||||
@@ -3421,7 +3442,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param {string} eventType The event type
|
||||
* @return {?object} The contents of the given account data event
|
||||
*/
|
||||
public getAccountData(eventType: string): MatrixEvent {
|
||||
public getAccountData(eventType: string): MatrixEvent | undefined {
|
||||
return this.store.getAccountData(eventType);
|
||||
}
|
||||
|
||||
@@ -4590,7 +4611,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @return {Promise} Resolves: to an empty object {}
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
public sendReceipt(event: MatrixEvent, receiptType: ReceiptType, body: any, callback?: Callback): Promise<{}> {
|
||||
public async sendReceipt(
|
||||
event: MatrixEvent,
|
||||
receiptType: ReceiptType,
|
||||
body: any,
|
||||
callback?: Callback,
|
||||
): Promise<{}> {
|
||||
if (typeof (body) === 'function') {
|
||||
callback = body as any as Callback; // legacy
|
||||
body = {};
|
||||
@@ -4605,10 +4631,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
$receiptType: receiptType,
|
||||
$eventId: event.getId(),
|
||||
});
|
||||
|
||||
// TODO: Add a check for which spec version this will be released in
|
||||
if (await this.doesServerSupportUnstableFeature("org.matrix.msc3771")) {
|
||||
const isThread = !!event.threadRootId;
|
||||
body.thread_id = isThread
|
||||
? event.threadRootId
|
||||
: MAIN_ROOM_TIMELINE;
|
||||
}
|
||||
|
||||
const promise = this.http.authedRequest(callback, Method.Post, path, undefined, body || {});
|
||||
|
||||
const room = this.getRoom(event.getRoomId());
|
||||
if (room) {
|
||||
if (room && this.credentials.userId) {
|
||||
room.addLocalEchoReceipt(this.credentials.userId, event, receiptType);
|
||||
}
|
||||
return promise;
|
||||
@@ -4622,7 +4657,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @return {Promise} Resolves: to an empty object {}
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
public async sendReadReceipt(event: MatrixEvent, receiptType = ReceiptType.Read, callback?: Callback): Promise<{}> {
|
||||
public async sendReadReceipt(
|
||||
event: MatrixEvent | null,
|
||||
receiptType = ReceiptType.Read,
|
||||
callback?: Callback,
|
||||
): Promise<{} | undefined> {
|
||||
if (!event) return;
|
||||
const eventId = event.getId();
|
||||
const room = this.getRoom(event.getRoomId());
|
||||
if (room && room.hasPendingEvent(eventId)) {
|
||||
@@ -5287,6 +5327,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param {object} [options]
|
||||
* @param {boolean} options.preventReEmit don't re-emit events emitted on an event mapped by this mapper on the client
|
||||
* @param {boolean} options.decrypt decrypt event proactively
|
||||
* @param {boolean} options.toDevice the event is a to_device event
|
||||
* @return {Function}
|
||||
*/
|
||||
public getEventMapper(options?: MapperOpts): EventMapper {
|
||||
@@ -5608,13 +5649,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.processBeaconEvents(timelineSet.room, timelineEvents);
|
||||
this.processThreadEvents(room, threadedEvents, backwards);
|
||||
|
||||
const atEnd = res.end === undefined || res.end === res.start;
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||
// we can recover from gappy syncs.
|
||||
if (backwards && res.end == res.start) {
|
||||
if (backwards && atEnd) {
|
||||
eventTimeline.setPaginationToken(null, dir);
|
||||
}
|
||||
return res.end != res.start;
|
||||
return !atEnd;
|
||||
}).finally(() => {
|
||||
eventTimeline.paginationRequests[dir] = null;
|
||||
});
|
||||
@@ -7080,10 +7123,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
/**
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {Promise<ILoginFlowsResponse>} Resolves to the available login flows
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
public loginFlows(callback?: Callback): Promise<any> { // TODO: Types
|
||||
public loginFlows(callback?: Callback): Promise<ILoginFlowsResponse> {
|
||||
return this.http.request(callback, Method.Get, "/login");
|
||||
}
|
||||
|
||||
@@ -7252,6 +7295,27 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return this.http.authedRequest(undefined, Method.Post, '/account/deactivate', undefined, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request for an `m.login.token` to be issued as per
|
||||
* [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882).
|
||||
* The server may require User-Interactive auth.
|
||||
* Note that this is UNSTABLE and subject to breaking changes without notice.
|
||||
* @param {IAuthData} auth Optional. Auth data to supply for User-Interactive auth.
|
||||
* @return {Promise<UIAResponse<LoginTokenPostResponse>>} Resolves: On success, the token response
|
||||
* or UIA auth data.
|
||||
*/
|
||||
public requestLoginToken(auth?: IAuthData): Promise<UIAResponse<LoginTokenPostResponse>> {
|
||||
const body: UIARequest<{}> = { auth };
|
||||
return this.http.authedRequest(
|
||||
undefined, // no callback support
|
||||
Method.Post,
|
||||
"/org.matrix.msc3882/login/token",
|
||||
undefined, // no query params
|
||||
body,
|
||||
{ prefix: PREFIX_UNSTABLE },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fallback URL to use for unknown interactive-auth stages.
|
||||
*
|
||||
@@ -7346,7 +7410,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
$eventType: eventType,
|
||||
});
|
||||
return this.http.authedRequest(
|
||||
undefined, Method.Get, path, null, null, {
|
||||
undefined, Method.Get, path, undefined, undefined, {
|
||||
prefix: PREFIX_UNSTABLE,
|
||||
},
|
||||
);
|
||||
@@ -7543,7 +7607,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
[ReceiptType.Read]: rrEventId,
|
||||
};
|
||||
|
||||
if (await this.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) {
|
||||
if (
|
||||
(await this.doesServerSupportUnstableFeature("org.matrix.msc2285.stable"))
|
||||
|| (await this.isVersionSupported("v1.4"))
|
||||
) {
|
||||
content[ReceiptType.ReadPrivate] = rpEventId;
|
||||
}
|
||||
|
||||
@@ -7651,7 +7718,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public getLocalAliases(roomId: string): Promise<{ aliases: string[] }> {
|
||||
const path = utils.encodeUri("/rooms/$roomId/aliases", { $roomId: roomId });
|
||||
const prefix = PREFIX_V3;
|
||||
return this.http.authedRequest(undefined, Method.Get, path, null, null, { prefix });
|
||||
return this.http.authedRequest(undefined, Method.Get, path, undefined, undefined, { prefix });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7888,7 +7955,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
'bind': bind,
|
||||
};
|
||||
return this.http.authedRequest(
|
||||
callback, Method.Post, path, null, data,
|
||||
callback, Method.Post, path, undefined, data,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7907,7 +7974,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public async addThreePidOnly(data: IAddThreePidOnlyBody): Promise<{}> {
|
||||
const path = "/account/3pid/add";
|
||||
const prefix = await this.isVersionSupported("r0.6.0") ? PREFIX_R0 : PREFIX_UNSTABLE;
|
||||
return this.http.authedRequest(undefined, Method.Post, path, null, data, { prefix });
|
||||
return this.http.authedRequest(undefined, Method.Post, path, undefined, data, { prefix });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7929,7 +7996,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const prefix = await this.isVersionSupported("r0.6.0") ?
|
||||
PREFIX_R0 : PREFIX_UNSTABLE;
|
||||
return this.http.authedRequest(
|
||||
undefined, Method.Post, path, null, data, { prefix },
|
||||
undefined, Method.Post, path, undefined, data, { prefix },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7956,7 +8023,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
id_server: this.getIdentityServerUrl(true),
|
||||
};
|
||||
const prefix = await this.isVersionSupported("r0.6.0") ? PREFIX_R0 : PREFIX_UNSTABLE;
|
||||
return this.http.authedRequest(undefined, Method.Post, path, null, data, { prefix });
|
||||
return this.http.authedRequest(undefined, Method.Post, path, undefined, data, { prefix });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7973,7 +8040,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// eslint-disable-next-line camelcase
|
||||
): Promise<{ id_server_unbind_result: IdServerUnbindResult }> {
|
||||
const path = "/account/3pid/delete";
|
||||
return this.http.authedRequest(undefined, Method.Post, path, null, { medium, address });
|
||||
return this.http.authedRequest(undefined, Method.Post, path, undefined, { medium, address });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8019,7 +8086,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
};
|
||||
|
||||
return this.http.authedRequest<{}>(
|
||||
callback, Method.Post, path, null, data,
|
||||
callback, Method.Post, path, undefined, data,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8110,8 +8177,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @return {Promise} Resolves: Array of objects representing pushers
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
public getPushers(callback?: Callback): Promise<{ pushers: IPusher[] }> {
|
||||
return this.http.authedRequest(callback, Method.Get, "/pushers");
|
||||
public async getPushers(callback?: Callback): Promise<{ pushers: IPusher[] }> {
|
||||
const response = await this.http.authedRequest(callback, Method.Get, "/pushers");
|
||||
|
||||
// Migration path for clients that connect to a homeserver that does not support
|
||||
// MSC3881 yet, see https://github.com/matrix-org/matrix-spec-proposals/blob/kerry/remote-push-toggle/proposals/3881-remote-push-notification-toggling.md#migration
|
||||
if (!await this.doesServerSupportUnstableFeature("org.matrix.msc3881")) {
|
||||
response.pushers = response.pushers.map(pusher => {
|
||||
if (!pusher.hasOwnProperty(PUSHER_ENABLED.name)) {
|
||||
pusher[PUSHER_ENABLED.name] = true;
|
||||
}
|
||||
return pusher;
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8124,7 +8204,22 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*/
|
||||
public setPusher(pusher: IPusherRequest, callback?: Callback): Promise<{}> {
|
||||
const path = "/pushers/set";
|
||||
return this.http.authedRequest(callback, Method.Post, path, null, pusher);
|
||||
return this.http.authedRequest(callback, Method.Post, path, undefined, pusher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists local notification settings
|
||||
* @param {string} deviceId
|
||||
* @param {LocalNotificationSettings} notificationSettings
|
||||
* @return {Promise} Resolves: an empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
public setLocalNotificationSettings(
|
||||
deviceId: string,
|
||||
notificationSettings: LocalNotificationSettings,
|
||||
): Promise<{}> {
|
||||
const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
|
||||
return this.setAccountData(key, notificationSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8911,7 +9006,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
$eventId: eventId,
|
||||
});
|
||||
|
||||
return this.http.authedRequest(undefined, Method.Post, path, null, { score, reason });
|
||||
return this.http.authedRequest(undefined, Method.Post, path, undefined, { score, reason });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9073,7 +9168,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*/
|
||||
public async getRoomSummary(roomIdOrAlias: string, via?: string[]): Promise<IRoomSummary> {
|
||||
const path = utils.encodeUri("/rooms/$roomid/summary", { $roomid: roomIdOrAlias });
|
||||
return this.http.authedRequest(undefined, Method.Get, path, { via }, null, {
|
||||
return this.http.authedRequest(undefined, Method.Get, path, { via }, undefined, {
|
||||
qsStringifyOptions: { arrayFormat: 'repeat' },
|
||||
prefix: "/_matrix/client/unstable/im.nheko.summary",
|
||||
});
|
||||
|
||||
@@ -292,16 +292,17 @@ export const makeBeaconContent: MakeBeaconContent = (
|
||||
});
|
||||
|
||||
export type BeaconLocationState = MLocationContent & {
|
||||
timestamp: number;
|
||||
uri?: string; // override from MLocationContent to allow optionals
|
||||
timestamp?: number;
|
||||
};
|
||||
|
||||
export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => {
|
||||
const { description, uri } = M_LOCATION.findIn<MLocationContent>(content);
|
||||
const location = M_LOCATION.findIn<MLocationContent>(content);
|
||||
const timestamp = M_TIMESTAMP.findIn<number>(content);
|
||||
|
||||
return {
|
||||
description,
|
||||
uri,
|
||||
description: location?.description,
|
||||
uri: location?.uri,
|
||||
timestamp,
|
||||
};
|
||||
};
|
||||
|
||||
+51
-14
@@ -23,6 +23,7 @@ import * as algorithms from './algorithms';
|
||||
import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
|
||||
import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
|
||||
import { IMegolmSessionData } from "./index";
|
||||
import { OlmGroupSessionExtraData } from "../@types/crypto";
|
||||
|
||||
// The maximum size of an event is 65K, and we base64 the content, so this is a
|
||||
// reasonable approximation to the biggest plaintext we can encrypt.
|
||||
@@ -122,6 +123,7 @@ interface IInboundGroupSessionKey {
|
||||
forwarding_curve25519_key_chain: string[];
|
||||
sender_claimed_ed25519_key: string;
|
||||
shared_history: boolean;
|
||||
untrusted: boolean;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -1101,7 +1103,7 @@ export class OlmDevice {
|
||||
sessionKey: string,
|
||||
keysClaimed: Record<string, string>,
|
||||
exportFormat: boolean,
|
||||
extraSessionData: Record<string, any> = {},
|
||||
extraSessionData: OlmGroupSessionExtraData = {},
|
||||
): Promise<void> {
|
||||
await this.cryptoStore.doTxn(
|
||||
'readwrite', [
|
||||
@@ -1133,17 +1135,42 @@ export class OlmDevice {
|
||||
"Update for megolm session "
|
||||
+ senderKey + "/" + sessionId,
|
||||
);
|
||||
if (existingSession.first_known_index()
|
||||
<= session.first_known_index()
|
||||
&& !(existingSession.first_known_index() == session.first_known_index()
|
||||
&& !extraSessionData.untrusted
|
||||
&& existingSessionData.untrusted)) {
|
||||
// existing session has lower index (i.e. can
|
||||
// decrypt more), or they have the same index and
|
||||
// the new sessions trust does not win over the old
|
||||
// sessions trust, so keep it
|
||||
logger.log(`Keeping existing megolm session ${sessionId}`);
|
||||
return;
|
||||
if (existingSession.first_known_index() <= session.first_known_index()) {
|
||||
if (!existingSessionData.untrusted || extraSessionData.untrusted) {
|
||||
// existing session has less-than-or-equal index
|
||||
// (i.e. can decrypt at least as much), and the
|
||||
// new session's trust does not win over the old
|
||||
// session's trust, so keep it
|
||||
logger.log(`Keeping existing megolm session ${sessionId}`);
|
||||
return;
|
||||
}
|
||||
if (existingSession.first_known_index() < session.first_known_index()) {
|
||||
// We want to upgrade the existing session's trust,
|
||||
// but we can't just use the new session because we'll
|
||||
// lose the lower index. Check that the sessions connect
|
||||
// properly, and then manually set the existing session
|
||||
// as trusted.
|
||||
if (
|
||||
existingSession.export_session(session.first_known_index())
|
||||
=== session.export_session(session.first_known_index())
|
||||
) {
|
||||
logger.info(
|
||||
"Upgrading trust of existing megolm session " +
|
||||
sessionId + " based on newly-received trusted session",
|
||||
);
|
||||
existingSessionData.untrusted = false;
|
||||
this.cryptoStore.storeEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, existingSessionData, txn,
|
||||
);
|
||||
} else {
|
||||
logger.warn(
|
||||
"Newly-received megolm session " + sessionId +
|
||||
" does not match existing session! Keeping existing session",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// If the sessions have the same index, go ahead and store the new trusted one.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1427,13 +1454,23 @@ export class OlmDevice {
|
||||
const claimedKeys = sessionData.keysClaimed || {};
|
||||
const senderEd25519Key = claimedKeys.ed25519 || null;
|
||||
|
||||
const forwardingKeyChain = sessionData.forwardingCurve25519KeyChain || [];
|
||||
// older forwarded keys didn't set the "untrusted"
|
||||
// property, but can be identified by having a
|
||||
// non-empty forwarding key chain. These keys should
|
||||
// be marked as untrusted since we don't know that they
|
||||
// can be trusted
|
||||
const untrusted = "untrusted" in sessionData
|
||||
? sessionData.untrusted
|
||||
: forwardingKeyChain.length > 0;
|
||||
|
||||
result = {
|
||||
"chain_index": chainIndex,
|
||||
"key": exportedSession,
|
||||
"forwarding_curve25519_key_chain":
|
||||
sessionData.forwardingCurve25519KeyChain || [],
|
||||
"forwarding_curve25519_key_chain": forwardingKeyChain,
|
||||
"sender_claimed_ed25519_key": senderEd25519Key,
|
||||
"shared_history": sessionData.sharedHistory || false,
|
||||
"untrusted": untrusted,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
@@ -539,7 +539,23 @@ export class SecretStorage {
|
||||
// because someone could be trying to send us bogus data
|
||||
return;
|
||||
}
|
||||
|
||||
if (!olmlib.isOlmEncrypted(event)) {
|
||||
logger.error("secret event not properly encrypted");
|
||||
return;
|
||||
}
|
||||
|
||||
const content = event.getContent();
|
||||
|
||||
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
|
||||
olmlib.OLM_ALGORITHM,
|
||||
content.sender_key,
|
||||
);
|
||||
if (senderKeyUser !== event.getSender()) {
|
||||
logger.error("sending device does not belong to the user it claims to be from");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log("got secret share for request", content.request_id);
|
||||
const requestControl = this.requests.get(content.request_id);
|
||||
if (requestControl) {
|
||||
@@ -559,6 +575,14 @@ export class SecretStorage {
|
||||
logger.log("unsolicited secret share from device", deviceInfo.deviceId);
|
||||
return;
|
||||
}
|
||||
// unsure that the sender is trusted. In theory, this check is
|
||||
// unnecessary since we only accept secret shares from devices that
|
||||
// we requested from, but it doesn't hurt.
|
||||
const deviceTrust = this.baseApis.crypto.checkDeviceInfoTrust(event.getSender(), deviceInfo);
|
||||
if (!deviceTrust.isVerified()) {
|
||||
logger.log("secret share from unverified device");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log(
|
||||
`Successfully received secret ${requestControl.name} ` +
|
||||
|
||||
@@ -23,7 +23,7 @@ limitations under the License.
|
||||
import { MatrixClient } from "../../client";
|
||||
import { Room } from "../../models/room";
|
||||
import { OlmDevice } from "../OlmDevice";
|
||||
import { MatrixEvent, RoomMember } from "../..";
|
||||
import { MatrixEvent, RoomMember } from "../../matrix";
|
||||
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
|
||||
import { DeviceInfo } from "../deviceinfo";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
|
||||
+114
-17
@@ -35,8 +35,10 @@ import { Room } from '../../models/room';
|
||||
import { DeviceInfo } from "../deviceinfo";
|
||||
import { IOlmSessionResult } from "../olmlib";
|
||||
import { DeviceInfoMap } from "../DeviceList";
|
||||
import { MatrixEvent } from "../..";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
|
||||
import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager';
|
||||
import { OlmGroupSessionExtraData } from "../../@types/crypto";
|
||||
|
||||
// determine whether the key can be shared with invitees
|
||||
export function isRoomSharedHistory(room: Room): boolean {
|
||||
@@ -1189,8 +1191,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
* {@link module:crypto/algorithms/DecryptionAlgorithm}
|
||||
*/
|
||||
class MegolmDecryption extends DecryptionAlgorithm {
|
||||
// events which we couldn't decrypt due to unknown sessions / indexes: map from
|
||||
// senderKey|sessionId to Set of MatrixEvents
|
||||
// events which we couldn't decrypt due to unknown sessions /
|
||||
// indexes, or which we could only decrypt with untrusted keys:
|
||||
// map from senderKey|sessionId to Set of MatrixEvents
|
||||
private pendingEvents = new Map<string, Map<string, Set<MatrixEvent>>>();
|
||||
|
||||
// this gets stubbed out by the unit tests.
|
||||
@@ -1294,9 +1297,13 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
);
|
||||
}
|
||||
|
||||
// success. We can remove the event from the pending list, if that hasn't
|
||||
// already happened.
|
||||
this.removeEventFromPendingList(event);
|
||||
// Success. We can remove the event from the pending list, if
|
||||
// that hasn't already happened. However, if the event was
|
||||
// decrypted with an untrusted key, leave it on the pending
|
||||
// list so it will be retried if we find a trusted key later.
|
||||
if (!res.untrusted) {
|
||||
this.removeEventFromPendingList(event);
|
||||
}
|
||||
|
||||
const payload = JSON.parse(res.result);
|
||||
|
||||
@@ -1391,6 +1398,8 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
let exportFormat = false;
|
||||
let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
|
||||
|
||||
const extraSessionData: OlmGroupSessionExtraData = {};
|
||||
|
||||
if (!content.room_id ||
|
||||
!content.session_key ||
|
||||
!content.session_id ||
|
||||
@@ -1400,12 +1409,59 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!senderKey) {
|
||||
logger.error("key event has no sender key (not encrypted?)");
|
||||
if (!olmlib.isOlmEncrypted(event)) {
|
||||
logger.error("key event not properly encrypted");
|
||||
return;
|
||||
}
|
||||
|
||||
if (content["org.matrix.msc3061.shared_history"]) {
|
||||
extraSessionData.sharedHistory = true;
|
||||
}
|
||||
|
||||
if (event.getType() == "m.forwarded_room_key") {
|
||||
const deviceInfo = this.crypto.deviceList.getDeviceByIdentityKey(
|
||||
olmlib.OLM_ALGORITHM,
|
||||
senderKey,
|
||||
);
|
||||
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
|
||||
olmlib.OLM_ALGORITHM,
|
||||
senderKey,
|
||||
);
|
||||
if (senderKeyUser !== event.getSender()) {
|
||||
logger.error("sending device does not belong to the user it claims to be from");
|
||||
return;
|
||||
}
|
||||
const outgoingRequests = deviceInfo ? await this.crypto.cryptoStore.getOutgoingRoomKeyRequestsByTarget(
|
||||
event.getSender(), deviceInfo.deviceId, [RoomKeyRequestState.Sent],
|
||||
) : [];
|
||||
const weRequested = outgoingRequests.some((req) => (
|
||||
req.requestBody.room_id === content.room_id && req.requestBody.session_id === content.session_id
|
||||
));
|
||||
const room = this.baseApis.getRoom(content.room_id);
|
||||
const memberEvent = room?.getMember(this.userId)?.events.member;
|
||||
const fromInviter = memberEvent?.getSender() === event.getSender() ||
|
||||
(memberEvent?.getUnsigned()?.prev_sender === event.getSender() &&
|
||||
memberEvent?.getPrevContent()?.membership === "invite");
|
||||
const fromUs = event.getSender() === this.baseApis.getUserId();
|
||||
|
||||
if (!weRequested && !fromUs) {
|
||||
// If someone sends us an unsolicited key and they're
|
||||
// not one of our other devices and it's not shared
|
||||
// history, ignore it
|
||||
if (!extraSessionData.sharedHistory) {
|
||||
logger.log("forwarded key not shared history - ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
// If someone sends us an unsolicited key for a room
|
||||
// we're already in, and they're not one of our other
|
||||
// devices or the one who invited us, ignore it
|
||||
if (room && !fromInviter) {
|
||||
logger.log("forwarded key not from inviter or from us - ignoring");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
exportFormat = true;
|
||||
forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain) ?
|
||||
content.forwarding_curve25519_key_chain : [];
|
||||
@@ -1418,7 +1474,6 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
logger.error("forwarded_room_key event is missing sender_key field");
|
||||
return;
|
||||
}
|
||||
senderKey = content.sender_key;
|
||||
|
||||
const ed25519Key = content.sender_claimed_ed25519_key;
|
||||
if (!ed25519Key) {
|
||||
@@ -1431,11 +1486,45 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
keysClaimed = {
|
||||
ed25519: ed25519Key,
|
||||
};
|
||||
|
||||
// If this is a key for a room we're not in, don't load it
|
||||
// yet, just park it in case *this sender* invites us to
|
||||
// that room later
|
||||
if (!room) {
|
||||
const parkedData = {
|
||||
senderId: event.getSender(),
|
||||
senderKey: content.sender_key,
|
||||
sessionId: content.session_id,
|
||||
sessionKey: content.session_key,
|
||||
keysClaimed,
|
||||
forwardingCurve25519KeyChain: forwardingKeyChain,
|
||||
};
|
||||
await this.crypto.cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
['parked_shared_history'],
|
||||
(txn) => this.crypto.cryptoStore.addParkedSharedHistory(content.room_id, parkedData, txn),
|
||||
logger.withPrefix("[addParkedSharedHistory]"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const sendingDevice = this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey);
|
||||
const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender(), sendingDevice);
|
||||
|
||||
if (fromUs && !deviceTrust.isVerified()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// forwarded keys are always untrusted
|
||||
extraSessionData.untrusted = true;
|
||||
|
||||
// replace the sender key with the sender key of the session
|
||||
// creator for storage
|
||||
senderKey = content.sender_key;
|
||||
} else {
|
||||
keysClaimed = event.getKeysClaimed();
|
||||
}
|
||||
|
||||
const extraSessionData: any = {};
|
||||
if (content["org.matrix.msc3061.shared_history"]) {
|
||||
extraSessionData.sharedHistory = true;
|
||||
}
|
||||
@@ -1453,7 +1542,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
);
|
||||
|
||||
// have another go at decrypting events sent with this session.
|
||||
if (await this.retryDecryption(senderKey, content.session_id)) {
|
||||
if (await this.retryDecryption(senderKey, content.session_id, !extraSessionData.untrusted)) {
|
||||
// cancel any outstanding room key requests for this session.
|
||||
// Only do this if we managed to decrypt every message in the
|
||||
// session, because if we didn't, we leave the other key
|
||||
@@ -1668,7 +1757,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
session: IMegolmSessionData,
|
||||
opts: { untrusted?: boolean, source?: string } = {},
|
||||
): Promise<void> {
|
||||
const extraSessionData: any = {};
|
||||
const extraSessionData: OlmGroupSessionExtraData = {};
|
||||
if (opts.untrusted || session.untrusted) {
|
||||
extraSessionData.untrusted = true;
|
||||
}
|
||||
@@ -1696,7 +1785,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
});
|
||||
}
|
||||
// have another go at decrypting events sent with this session.
|
||||
this.retryDecryption(session.sender_key, session.session_id);
|
||||
this.retryDecryption(session.sender_key, session.session_id, !extraSessionData.untrusted);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1707,10 +1796,17 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
* @private
|
||||
* @param {String} senderKey
|
||||
* @param {String} sessionId
|
||||
* @param {Boolean} forceRedecryptIfUntrusted whether messages that were already
|
||||
* successfully decrypted using untrusted keys should be re-decrypted
|
||||
*
|
||||
* @return {Boolean} whether all messages were successfully decrypted
|
||||
* @return {Boolean} whether all messages were successfully
|
||||
* decrypted with trusted keys
|
||||
*/
|
||||
private async retryDecryption(senderKey: string, sessionId: string): Promise<boolean> {
|
||||
private async retryDecryption(
|
||||
senderKey: string,
|
||||
sessionId: string,
|
||||
forceRedecryptIfUntrusted?: boolean,
|
||||
): Promise<boolean> {
|
||||
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||
if (!senderPendingEvents) {
|
||||
return true;
|
||||
@@ -1725,13 +1821,14 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
|
||||
await Promise.all([...pending].map(async (ev) => {
|
||||
try {
|
||||
await ev.attemptDecryption(this.crypto, { isRetry: true });
|
||||
await ev.attemptDecryption(this.crypto, { isRetry: true, forceRedecryptIfUntrusted });
|
||||
} catch (e) {
|
||||
// don't die if something goes wrong
|
||||
}
|
||||
}));
|
||||
|
||||
// If decrypted successfully, they'll have been removed from pendingEvents
|
||||
// If decrypted successfully with trusted keys, they'll have
|
||||
// been removed from pendingEvents
|
||||
return !this.pendingEvents.get(senderKey)?.has(sessionId);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
registerAlgorithm,
|
||||
} from "./base";
|
||||
import { Room } from '../../models/room';
|
||||
import { MatrixEvent } from "../..";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
import { IEventDecryptionResult } from "../index";
|
||||
import { IInboundSession } from "../OlmDevice";
|
||||
|
||||
@@ -222,6 +222,26 @@ class OlmDecryption extends DecryptionAlgorithm {
|
||||
);
|
||||
}
|
||||
|
||||
// check that the device that encrypted the event belongs to the user
|
||||
// that the event claims it's from. We need to make sure that our
|
||||
// device list is up-to-date. If the device is unknown, we can only
|
||||
// assume that the device logged out. Some event handlers, such as
|
||||
// secret sharing, may be more strict and reject events that come from
|
||||
// unknown devices.
|
||||
await this.crypto.deviceList.downloadKeys([event.getSender()], false);
|
||||
const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(
|
||||
olmlib.OLM_ALGORITHM,
|
||||
deviceKey,
|
||||
);
|
||||
if (senderKeyUser !== event.getSender() && senderKeyUser !== undefined) {
|
||||
throw new DecryptionError(
|
||||
"OLM_BAD_SENDER",
|
||||
"Message claimed to be from " + event.getSender(), {
|
||||
real_sender: senderKeyUser,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// check that the original sender matches what the homeserver told us, to
|
||||
// avoid people masquerading as others.
|
||||
// (this check is also provided via the sender's embedded ed25519 key,
|
||||
|
||||
@@ -431,7 +431,6 @@ export class BackupManager {
|
||||
)
|
||||
);
|
||||
});
|
||||
ret.usable = ret.usable || ret.trusted_locally;
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
+17
-9
@@ -2105,6 +2105,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* @param {?boolean} known whether to mark that the user has been made aware of
|
||||
* the existence of this device. Null to leave unchanged
|
||||
*
|
||||
* @param {?Record<string, any>} keys The list of keys that was present
|
||||
* during the device verification. This will be double checked with the list
|
||||
* of keys the given device has currently.
|
||||
*
|
||||
* @return {Promise<module:crypto/deviceinfo>} updated DeviceInfo
|
||||
*/
|
||||
public async setDeviceVerification(
|
||||
@@ -2113,6 +2117,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
verified?: boolean,
|
||||
blocked?: boolean,
|
||||
known?: boolean,
|
||||
keys?: Record<string, string>,
|
||||
): Promise<DeviceInfo | CrossSigningInfo> {
|
||||
// get rid of any `undefined`s here so we can just check
|
||||
// for null rather than null or undefined
|
||||
@@ -2131,6 +2136,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
if (!verified) {
|
||||
throw new Error("Cannot set a cross-signing key as unverified");
|
||||
}
|
||||
const gotKeyId = keys ? Object.values(keys)[0] : null;
|
||||
if (keys && (Object.values(keys).length !== 1 || gotKeyId !== xsk.getId())) {
|
||||
throw new Error(`Key did not match expected value: expected ${xsk.getId()}, got ${gotKeyId}`);
|
||||
}
|
||||
|
||||
if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) {
|
||||
this.storeTrustedSelfKeys(xsk.keys);
|
||||
@@ -2191,6 +2200,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
let verificationStatus = dev.verified;
|
||||
|
||||
if (verified) {
|
||||
if (keys) {
|
||||
for (const [keyId, key] of Object.entries(keys)) {
|
||||
if (dev.keys[keyId] !== key) {
|
||||
throw new Error(`Key did not match expected value: expected ${key}, got ${dev.keys[keyId]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
verificationStatus = DeviceVerification.VERIFIED;
|
||||
} else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
|
||||
verificationStatus = DeviceVerification.UNVERIFIED;
|
||||
@@ -2400,13 +2416,6 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return null;
|
||||
}
|
||||
|
||||
const forwardingChain = event.getForwardingCurve25519KeyChain();
|
||||
if (forwardingChain.length > 0) {
|
||||
// we got the key this event from somewhere else
|
||||
// TODO: check if we can trust the forwarders.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (event.isKeySourceUntrusted()) {
|
||||
// we got the key for this event from a source that we consider untrusted
|
||||
return null;
|
||||
@@ -2478,8 +2487,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
ret.encrypted = true;
|
||||
|
||||
const forwardingChain = event.getForwardingCurve25519KeyChain();
|
||||
if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) {
|
||||
if (event.isKeySourceUntrusted()) {
|
||||
// we got the key this event from somewhere else
|
||||
// TODO: check if we can trust the forwarders.
|
||||
ret.authenticated = false;
|
||||
|
||||
@@ -30,6 +30,8 @@ import { logger } from '../logger';
|
||||
import { IOneTimeKey } from "./dehydration";
|
||||
import { IClaimOTKsResult, MatrixClient } from "../client";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { EventType } from "../@types/event";
|
||||
|
||||
enum Algorithm {
|
||||
Olm = "m.olm.v1.curve25519-aes-sha2",
|
||||
@@ -554,6 +556,22 @@ export function pkVerify(obj: IObject, pubKey: string, userId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that an event was encrypted using olm.
|
||||
*/
|
||||
export function isOlmEncrypted(event: MatrixEvent): boolean {
|
||||
if (!event.getSenderKey()) {
|
||||
logger.error("Event has no sender key (not encrypted?)");
|
||||
return false;
|
||||
}
|
||||
if (event.getWireType() !== EventType.RoomMessageEncrypted ||
|
||||
!(["m.olm.v1.curve25519-aes-sha2"].includes(event.getWireContent().algorithm))) {
|
||||
logger.error("Event was not encrypted using an appropriate algorithm");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a typed array of uint8 as base64.
|
||||
* @param {Uint8Array} uint8Array The data to encode.
|
||||
|
||||
@@ -25,6 +25,7 @@ import { ICrossSigningInfo } from "../CrossSigning";
|
||||
import { PrefixedLogger } from "../../logger";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
|
||||
/**
|
||||
* Internal module. Definitions for storage for the crypto module
|
||||
@@ -127,6 +128,8 @@ export interface CryptoStore {
|
||||
roomId: string,
|
||||
txn?: unknown,
|
||||
): Promise<[senderKey: string, sessionId: string][]>;
|
||||
addParkedSharedHistory(roomId: string, data: ParkedSharedHistory, txn?: unknown): void;
|
||||
takeParkedSharedHistory(roomId: string, txn?: unknown): Promise<ParkedSharedHistory[]>;
|
||||
|
||||
// Session key backups
|
||||
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
|
||||
@@ -203,3 +206,12 @@ export interface OutgoingRoomKeyRequest {
|
||||
requestBody: IRoomKeyRequestBody;
|
||||
state: RoomKeyRequestState;
|
||||
}
|
||||
|
||||
export interface ParkedSharedHistory {
|
||||
senderId: string;
|
||||
senderKey: string;
|
||||
sessionId: string;
|
||||
sessionKey: string;
|
||||
keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>; // XXX: Less type dependence on MatrixEvent
|
||||
forwardingCurve25519KeyChain: string[];
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
IWithheld,
|
||||
Mode,
|
||||
OutgoingRoomKeyRequest,
|
||||
ParkedSharedHistory,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
@@ -33,7 +34,6 @@ import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
|
||||
export const VERSION = 10;
|
||||
const PROFILE_TRANSACTIONS = false;
|
||||
|
||||
/**
|
||||
@@ -873,6 +873,50 @@ export class Backend implements CryptoStore {
|
||||
});
|
||||
}
|
||||
|
||||
public addParkedSharedHistory(
|
||||
roomId: string,
|
||||
parkedData: ParkedSharedHistory,
|
||||
txn?: IDBTransaction,
|
||||
): void {
|
||||
if (!txn) {
|
||||
txn = this.db.transaction(
|
||||
"parked_shared_history", "readwrite",
|
||||
);
|
||||
}
|
||||
const objectStore = txn.objectStore("parked_shared_history");
|
||||
const req = objectStore.get([roomId]);
|
||||
req.onsuccess = () => {
|
||||
const { parked } = req.result || { parked: [] };
|
||||
parked.push(parkedData);
|
||||
objectStore.put({ roomId, parked });
|
||||
};
|
||||
}
|
||||
|
||||
public takeParkedSharedHistory(
|
||||
roomId: string,
|
||||
txn?: IDBTransaction,
|
||||
): Promise<ParkedSharedHistory[]> {
|
||||
if (!txn) {
|
||||
txn = this.db.transaction(
|
||||
"parked_shared_history", "readwrite",
|
||||
);
|
||||
}
|
||||
const cursorReq = txn.objectStore("parked_shared_history").openCursor(roomId);
|
||||
return new Promise((resolve, reject) => {
|
||||
cursorReq.onsuccess = () => {
|
||||
const cursor = cursorReq.result;
|
||||
if (!cursor) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
const data = cursor.value;
|
||||
cursor.delete();
|
||||
resolve(data);
|
||||
};
|
||||
cursorReq.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
public doTxn<T>(
|
||||
mode: Mode,
|
||||
stores: string | string[],
|
||||
@@ -905,45 +949,34 @@ export class Backend implements CryptoStore {
|
||||
}
|
||||
}
|
||||
|
||||
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
|
||||
logger.log(
|
||||
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
|
||||
+ ` to ${VERSION}`,
|
||||
);
|
||||
if (oldVersion < 1) { // The database did not previously exist.
|
||||
createDatabase(db);
|
||||
}
|
||||
if (oldVersion < 2) {
|
||||
db.createObjectStore("account");
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
type DbMigration = (db: IDBDatabase) => void;
|
||||
const DB_MIGRATIONS: DbMigration[] = [
|
||||
(db) => { createDatabase(db); },
|
||||
(db) => { db.createObjectStore("account"); },
|
||||
(db) => {
|
||||
const sessionsStore = db.createObjectStore("sessions", {
|
||||
keyPath: ["deviceKey", "sessionId"],
|
||||
});
|
||||
sessionsStore.createIndex("deviceKey", "deviceKey");
|
||||
}
|
||||
if (oldVersion < 4) {
|
||||
},
|
||||
(db) => {
|
||||
db.createObjectStore("inbound_group_sessions", {
|
||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||
});
|
||||
}
|
||||
if (oldVersion < 5) {
|
||||
db.createObjectStore("device_data");
|
||||
}
|
||||
if (oldVersion < 6) {
|
||||
db.createObjectStore("rooms");
|
||||
}
|
||||
if (oldVersion < 7) {
|
||||
},
|
||||
(db) => { db.createObjectStore("device_data"); },
|
||||
(db) => { db.createObjectStore("rooms"); },
|
||||
(db) => {
|
||||
db.createObjectStore("sessions_needing_backup", {
|
||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||
});
|
||||
}
|
||||
if (oldVersion < 8) {
|
||||
},
|
||||
(db) => {
|
||||
db.createObjectStore("inbound_group_sessions_withheld", {
|
||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||
});
|
||||
}
|
||||
if (oldVersion < 9) {
|
||||
},
|
||||
(db) => {
|
||||
const problemsStore = db.createObjectStore("session_problems", {
|
||||
keyPath: ["deviceKey", "time"],
|
||||
});
|
||||
@@ -952,13 +985,29 @@ export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
|
||||
db.createObjectStore("notified_error_devices", {
|
||||
keyPath: ["userId", "deviceId"],
|
||||
});
|
||||
}
|
||||
if (oldVersion < 10) {
|
||||
},
|
||||
(db) => {
|
||||
db.createObjectStore("shared_history_inbound_group_sessions", {
|
||||
keyPath: ["roomId"],
|
||||
});
|
||||
}
|
||||
},
|
||||
(db) => {
|
||||
db.createObjectStore("parked_shared_history", {
|
||||
keyPath: ["roomId"],
|
||||
});
|
||||
},
|
||||
// Expand as needed.
|
||||
];
|
||||
export const VERSION = DB_MIGRATIONS.length;
|
||||
|
||||
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
|
||||
logger.log(
|
||||
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
|
||||
+ ` to ${VERSION}`,
|
||||
);
|
||||
DB_MIGRATIONS.forEach((migration, index) => {
|
||||
if (oldVersion <= index) migration(db);
|
||||
});
|
||||
}
|
||||
|
||||
function createDatabase(db: IDBDatabase): void {
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
IWithheld,
|
||||
Mode,
|
||||
OutgoingRoomKeyRequest,
|
||||
ParkedSharedHistory,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
@@ -55,6 +56,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
public static STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
|
||||
public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld';
|
||||
public static STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS = 'shared_history_inbound_group_sessions';
|
||||
public static STORE_PARKED_SHARED_HISTORY = 'parked_shared_history';
|
||||
public static STORE_DEVICE_DATA = 'device_data';
|
||||
public static STORE_ROOMS = 'rooms';
|
||||
public static STORE_BACKUP = 'sessions_needing_backup';
|
||||
@@ -669,6 +671,27 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Park a shared-history group session for a room we may be invited to later.
|
||||
*/
|
||||
public addParkedSharedHistory(
|
||||
roomId: string,
|
||||
parkedData: ParkedSharedHistory,
|
||||
txn?: IDBTransaction,
|
||||
): void {
|
||||
this.backend.addParkedSharedHistory(roomId, parkedData, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop out all shared-history group sessions for a room.
|
||||
*/
|
||||
public takeParkedSharedHistory(
|
||||
roomId: string,
|
||||
txn?: IDBTransaction,
|
||||
): Promise<ParkedSharedHistory[]> {
|
||||
return this.backend.takeParkedSharedHistory(roomId, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a transaction on the crypto store. Any store methods
|
||||
* that require a transaction (txn) object to be passed in may
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
IWithheld,
|
||||
Mode,
|
||||
OutgoingRoomKeyRequest,
|
||||
ParkedSharedHistory,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
@@ -58,6 +59,7 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
private rooms: { [roomId: string]: IRoomEncryption } = {};
|
||||
private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
|
||||
private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {};
|
||||
private parkedSharedHistory = new Map<string, ParkedSharedHistory[]>(); // keyed by room ID
|
||||
|
||||
/**
|
||||
* Ensure the database exists and is up-to-date.
|
||||
@@ -526,6 +528,18 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []);
|
||||
}
|
||||
|
||||
public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory): void {
|
||||
const parked = this.parkedSharedHistory.get(roomId) ?? [];
|
||||
parked.push(parkedData);
|
||||
this.parkedSharedHistory.set(roomId, parked);
|
||||
}
|
||||
|
||||
public takeParkedSharedHistory(roomId: string): Promise<ParkedSharedHistory[]> {
|
||||
const parked = this.parkedSharedHistory.get(roomId) ?? [];
|
||||
this.parkedSharedHistory.delete(roomId);
|
||||
return Promise.resolve(parked);
|
||||
}
|
||||
|
||||
// Session key backups
|
||||
|
||||
public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> {
|
||||
|
||||
@@ -299,7 +299,13 @@ export class VerificationBase<
|
||||
if (this.doVerification && !this.started) {
|
||||
this.started = true;
|
||||
this.resetTimer(); // restart the timeout
|
||||
Promise.resolve(this.doVerification()).then(this.done.bind(this), this.cancel.bind(this));
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const crossSignId = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(this.userId)?.getId();
|
||||
if (crossSignId === this.deviceId) {
|
||||
reject(new Error("Device ID is the same as the cross-signing ID"));
|
||||
}
|
||||
resolve();
|
||||
}).then(() => this.doVerification()).then(this.done.bind(this), this.cancel.bind(this));
|
||||
}
|
||||
return this.promise;
|
||||
}
|
||||
@@ -310,14 +316,14 @@ export class VerificationBase<
|
||||
// we try to verify all the keys that we're told about, but we might
|
||||
// not know about all of them, so keep track of the keys that we know
|
||||
// about, and ignore the rest
|
||||
const verifiedDevices = [];
|
||||
const verifiedDevices: [string, string, string][] = [];
|
||||
|
||||
for (const [keyId, keyInfo] of Object.entries(keys)) {
|
||||
const deviceId = keyId.split(':', 2)[1];
|
||||
const device = this.baseApis.getStoredDevice(userId, deviceId);
|
||||
if (device) {
|
||||
verifier(keyId, device, keyInfo);
|
||||
verifiedDevices.push(deviceId);
|
||||
verifiedDevices.push([deviceId, keyId, device.keys[keyId]]);
|
||||
} else {
|
||||
const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
|
||||
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
|
||||
@@ -326,7 +332,7 @@ export class VerificationBase<
|
||||
[keyId]: deviceId,
|
||||
},
|
||||
}, deviceId), keyInfo);
|
||||
verifiedDevices.push(deviceId);
|
||||
verifiedDevices.push([deviceId, keyId, deviceId]);
|
||||
} else {
|
||||
logger.warn(
|
||||
`verification: Could not find device ${deviceId} to verify`,
|
||||
@@ -348,8 +354,15 @@ export class VerificationBase<
|
||||
// TODO: There should probably be a batch version of this, otherwise it's going
|
||||
// to upload each signature in a separate API call which is silly because the
|
||||
// API supports as many signatures as you like.
|
||||
for (const deviceId of verifiedDevices) {
|
||||
await this.baseApis.setDeviceVerified(userId, deviceId);
|
||||
for (const [deviceId, keyId, key] of verifiedDevices) {
|
||||
await this.baseApis.crypto.setDeviceVerification(userId, deviceId, true, null, null, { [keyId]: key });
|
||||
}
|
||||
|
||||
// if one of the user's own devices is being marked as verified / unverified,
|
||||
// check the key backup status, since whether or not we use this depends on
|
||||
// whether it has a signature from a verified device
|
||||
if (userId == this.baseApis.credentials.userId) {
|
||||
await this.baseApis.checkKeyBackup();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
|
||||
export interface MapperOpts {
|
||||
preventReEmit?: boolean;
|
||||
decrypt?: boolean;
|
||||
toDevice?: boolean;
|
||||
}
|
||||
|
||||
export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
|
||||
@@ -29,6 +30,10 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
||||
const decrypt = options.decrypt !== false;
|
||||
|
||||
function mapper(plainOldJsObject: Partial<IEvent>) {
|
||||
if (options.toDevice) {
|
||||
delete plainOldJsObject.room_id;
|
||||
}
|
||||
|
||||
const room = client.getRoom(plainOldJsObject.room_id);
|
||||
|
||||
let event: MatrixEvent;
|
||||
|
||||
+7
-7
@@ -365,7 +365,7 @@ export class MatrixHttpApi {
|
||||
// we're setting opts.json=false so that it doesn't JSON-encode the
|
||||
// request, which also means it doesn't JSON-decode the response. Either
|
||||
// way, we have to JSON-parse the response ourselves.
|
||||
let bodyParser = null;
|
||||
let bodyParser: ((body: string) => any) | undefined;
|
||||
if (!rawResponse) {
|
||||
bodyParser = function(rawBody: string) {
|
||||
let body = JSON.parse(rawBody);
|
||||
@@ -472,7 +472,7 @@ export class MatrixHttpApi {
|
||||
headers["Content-Length"] = "0";
|
||||
}
|
||||
|
||||
promise = this.authedRequest(
|
||||
promise = this.authedRequest<UploadContentResponseType<O>>(
|
||||
opts.callback, Method.Post, "/upload", queryParams, body, {
|
||||
prefix: "/_matrix/media/r0",
|
||||
headers,
|
||||
@@ -590,10 +590,10 @@ export class MatrixHttpApi {
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
public authedRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
callback: Callback<T> | undefined,
|
||||
method: Method,
|
||||
path: string,
|
||||
queryParams?: Record<string, string | string[]>,
|
||||
queryParams?: Record<string, string | string[] | undefined>,
|
||||
data?: CoreOptions["body"],
|
||||
opts?: O | number, // number is legacy
|
||||
): IAbortablePromise<ResponseType<T, O>> {
|
||||
@@ -667,7 +667,7 @@ export class MatrixHttpApi {
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
public request<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
callback: Callback<T> | undefined,
|
||||
method: Method,
|
||||
path: string,
|
||||
queryParams?: CoreOptions["qs"],
|
||||
@@ -711,7 +711,7 @@ export class MatrixHttpApi {
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
public requestOtherUrl<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
callback: Callback<T> | undefined,
|
||||
method: Method,
|
||||
uri: string,
|
||||
queryParams?: CoreOptions["qs"],
|
||||
@@ -778,7 +778,7 @@ export class MatrixHttpApi {
|
||||
* Generic O should be inferred
|
||||
*/
|
||||
private doRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
callback: Callback<T> | undefined,
|
||||
method: Method,
|
||||
uri: string,
|
||||
queryParams?: Record<string, string>,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { IContent, MatrixEvent } from "./event";
|
||||
import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
|
||||
import { EventTimeline } from "./event-timeline";
|
||||
import { FileType } from "../http-api";
|
||||
import type { ISendEventResponse } from "..";
|
||||
import type { ISendEventResponse } from "../@types/requests";
|
||||
|
||||
/**
|
||||
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference
|
||||
|
||||
@@ -15,7 +15,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { MBeaconEventContent } from "../@types/beacon";
|
||||
import { M_TIMESTAMP } from "../@types/location";
|
||||
import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
|
||||
import { MatrixEvent } from "../matrix";
|
||||
import { sortEventsByLatestContentTimestamp } from "../utils";
|
||||
@@ -161,7 +160,9 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
|
||||
|
||||
const validLocationEvents = beaconLocationEvents.filter(event => {
|
||||
const content = event.getContent<MBeaconEventContent>();
|
||||
const timestamp = M_TIMESTAMP.findIn<number>(content);
|
||||
const parsed = parseBeaconContent(content);
|
||||
if (!parsed.uri || !parsed.timestamp) return false; // we won't be able to process these
|
||||
const { timestamp } = parsed;
|
||||
return (
|
||||
// only include positions that were taken inside the beacon's live period
|
||||
isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) &&
|
||||
|
||||
@@ -287,7 +287,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
* @return {?module:models/event-timeline~EventTimeline} timeline containing
|
||||
* the given event, or null if unknown
|
||||
*/
|
||||
public getTimelineForEvent(eventId: string): EventTimeline | null {
|
||||
public getTimelineForEvent(eventId: string | null): EventTimeline | null {
|
||||
if (eventId === null) { return null; }
|
||||
const res = this._eventIdToTimeline.get(eventId);
|
||||
return (res === undefined) ? null : res;
|
||||
}
|
||||
|
||||
+6
-1
@@ -151,6 +151,7 @@ interface IKeyRequestRecipient {
|
||||
export interface IDecryptOptions {
|
||||
emit?: boolean;
|
||||
isRetry?: boolean;
|
||||
forceRedecryptIfUntrusted?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -677,6 +678,8 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
||||
* @param {object} options
|
||||
* @param {boolean} options.isRetry True if this is a retry (enables more logging)
|
||||
* @param {boolean} options.emit Emits "event.decrypted" if set to true
|
||||
* @param {boolean} options.forceRedecryptIfUntrusted whether the message should be
|
||||
* re-decrypted if it was previously successfully decrypted with an untrusted key
|
||||
*
|
||||
* @returns {Promise} promise which resolves (to undefined) when the decryption
|
||||
* attempt is completed.
|
||||
@@ -695,7 +698,9 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
||||
throw new Error("Attempt to decrypt event which isn't encrypted");
|
||||
}
|
||||
|
||||
if (this.clearEvent && !this.isDecryptionFailure()) {
|
||||
const alreadyDecrypted = this.clearEvent && !this.isDecryptionFailure();
|
||||
const forceRedecrypt = options.forceRedecryptIfUntrusted && this.isKeySourceUntrusted();
|
||||
if (alreadyDecrypted && !forceRedecrypt) {
|
||||
// we may want to just ignore this? let's start with rejecting it.
|
||||
throw new Error(
|
||||
"Attempt to decrypt event which has already been decrypted",
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
Copyright 2022 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 { ReceiptType } from "../@types/read_receipts";
|
||||
import { EventTimelineSet, EventType, MatrixEvent } from "../matrix";
|
||||
import { ListenerMap, TypedEventEmitter } from "./typed-event-emitter";
|
||||
import * as utils from "../utils";
|
||||
|
||||
export const MAIN_ROOM_TIMELINE = "main";
|
||||
|
||||
export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[event.getId()]: {
|
||||
[receiptType]: {
|
||||
[userId]: {
|
||||
ts: event.getTs(),
|
||||
threadId: event.threadRootId ?? MAIN_ROOM_TIMELINE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
type: EventType.Receipt,
|
||||
room_id: event.getRoomId(),
|
||||
});
|
||||
}
|
||||
|
||||
export interface Receipt {
|
||||
ts: number;
|
||||
thread_id?: string;
|
||||
}
|
||||
|
||||
export interface WrappedReceipt {
|
||||
eventId: string;
|
||||
data: Receipt;
|
||||
}
|
||||
|
||||
interface CachedReceipt {
|
||||
type: ReceiptType;
|
||||
userId: string;
|
||||
data: Receipt;
|
||||
}
|
||||
|
||||
type ReceiptCache = {[eventId: string]: CachedReceipt[]};
|
||||
|
||||
export interface ReceiptContent {
|
||||
[eventId: string]: {
|
||||
[key in ReceiptType]: {
|
||||
[userId: string]: Receipt;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const ReceiptPairRealIndex = 0;
|
||||
const ReceiptPairSyntheticIndex = 1;
|
||||
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
|
||||
type Receipts = {
|
||||
[receiptType: string]: {
|
||||
[userId: string]: [WrappedReceipt | null, WrappedReceipt | null]; // Pair<real receipt, synthetic receipt> (both nullable)
|
||||
};
|
||||
};
|
||||
|
||||
export abstract class ReadReceipt<
|
||||
Events extends string,
|
||||
Arguments extends ListenerMap<Events>,
|
||||
SuperclassArguments extends ListenerMap<any> = Arguments,
|
||||
> extends TypedEventEmitter<Events, Arguments, SuperclassArguments> {
|
||||
// receipts should clobber based on receipt_type and user_id pairs hence
|
||||
// the form of this structure. This is sub-optimal for the exposed APIs
|
||||
// which pass in an event ID and get back some receipts, so we also store
|
||||
// a pre-cached list for this purpose.
|
||||
private receipts: Receipts = {}; // { receipt_type: { user_id: Receipt } }
|
||||
private receiptCacheByEventId: ReceiptCache = {}; // { event_id: CachedReceipt[] }
|
||||
|
||||
public abstract getUnfilteredTimelineSet(): EventTimelineSet;
|
||||
public abstract timeline: MatrixEvent[];
|
||||
|
||||
/**
|
||||
* Gets the latest receipt for a given user in the room
|
||||
* @param userId The id of the user for which we want the receipt
|
||||
* @param ignoreSynthesized Whether to ignore synthesized receipts or not
|
||||
* @param receiptType Optional. The type of the receipt we want to get
|
||||
* @returns the latest receipts of the chosen type for the chosen user
|
||||
*/
|
||||
public getReadReceiptForUserId(
|
||||
userId: string, ignoreSynthesized = false, receiptType = ReceiptType.Read,
|
||||
): WrappedReceipt | null {
|
||||
const [realReceipt, syntheticReceipt] = this.receipts[receiptType]?.[userId] ?? [];
|
||||
if (ignoreSynthesized) {
|
||||
return realReceipt;
|
||||
}
|
||||
|
||||
return syntheticReceipt ?? realReceipt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the event that a given user has read up to, or null if we
|
||||
* have received no read receipts from them.
|
||||
* @param {String} userId The user ID to get read receipt event ID for
|
||||
* @param {Boolean} ignoreSynthesized If true, return only receipts that have been
|
||||
* sent by the server, not implicit ones generated
|
||||
* by the JS SDK.
|
||||
* @return {String} ID of the latest event that the given user has read, or null.
|
||||
*/
|
||||
public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null {
|
||||
// XXX: This is very very ugly and I hope I won't have to ever add a new
|
||||
// receipt type here again. IMHO this should be done by the server in
|
||||
// some more intelligent manner or the client should just use timestamps
|
||||
|
||||
const timelineSet = this.getUnfilteredTimelineSet();
|
||||
const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.Read);
|
||||
const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.ReadPrivate);
|
||||
|
||||
// If we have both, compare them
|
||||
let comparison: number | null | undefined;
|
||||
if (publicReadReceipt?.eventId && privateReadReceipt?.eventId) {
|
||||
comparison = timelineSet.compareEventOrdering(publicReadReceipt?.eventId, privateReadReceipt?.eventId);
|
||||
}
|
||||
|
||||
// If we didn't get a comparison try to compare the ts of the receipts
|
||||
if (!comparison && publicReadReceipt?.data?.ts && privateReadReceipt?.data?.ts) {
|
||||
comparison = publicReadReceipt?.data?.ts - privateReadReceipt?.data?.ts;
|
||||
}
|
||||
|
||||
// The public receipt is more likely to drift out of date so the private
|
||||
// one has precedence
|
||||
if (!comparison) return privateReadReceipt?.eventId ?? publicReadReceipt?.eventId ?? null;
|
||||
|
||||
// If public read receipt is older, return the private one
|
||||
return ((comparison < 0) ? privateReadReceipt?.eventId : publicReadReceipt?.eventId) ?? null;
|
||||
}
|
||||
|
||||
public addReceiptToStructure(
|
||||
eventId: string,
|
||||
receiptType: ReceiptType,
|
||||
userId: string,
|
||||
receipt: Receipt,
|
||||
synthetic: boolean,
|
||||
): void {
|
||||
if (!this.receipts[receiptType]) {
|
||||
this.receipts[receiptType] = {};
|
||||
}
|
||||
if (!this.receipts[receiptType][userId]) {
|
||||
this.receipts[receiptType][userId] = [null, null];
|
||||
}
|
||||
|
||||
const pair = this.receipts[receiptType][userId];
|
||||
|
||||
let existingReceipt = pair[ReceiptPairRealIndex];
|
||||
if (synthetic) {
|
||||
existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
|
||||
}
|
||||
|
||||
if (existingReceipt) {
|
||||
// we only want to add this receipt if we think it is later than the one we already have.
|
||||
// This is managed server-side, but because we synthesize RRs locally we have to do it here too.
|
||||
const ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
|
||||
existingReceipt.eventId,
|
||||
eventId,
|
||||
);
|
||||
if (ordering !== null && ordering >= 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const wrappedReceipt: WrappedReceipt = {
|
||||
eventId,
|
||||
data: receipt,
|
||||
};
|
||||
|
||||
const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt;
|
||||
const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex];
|
||||
|
||||
let ordering: number | null = null;
|
||||
if (realReceipt && syntheticReceipt) {
|
||||
ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
|
||||
realReceipt.eventId,
|
||||
syntheticReceipt.eventId,
|
||||
);
|
||||
}
|
||||
|
||||
const preferSynthetic = ordering === null || ordering < 0;
|
||||
|
||||
// we don't bother caching just real receipts by event ID as there's nothing that would read it.
|
||||
// Take the current cached receipt before we overwrite the pair elements.
|
||||
const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
|
||||
|
||||
if (synthetic && preferSynthetic) {
|
||||
pair[ReceiptPairSyntheticIndex] = wrappedReceipt;
|
||||
} else if (!synthetic) {
|
||||
pair[ReceiptPairRealIndex] = wrappedReceipt;
|
||||
|
||||
if (!preferSynthetic) {
|
||||
pair[ReceiptPairSyntheticIndex] = null;
|
||||
}
|
||||
}
|
||||
|
||||
const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
|
||||
if (cachedReceipt === newCachedReceipt) return;
|
||||
|
||||
// clean up any previous cache entry
|
||||
if (cachedReceipt && this.receiptCacheByEventId[cachedReceipt.eventId]) {
|
||||
const previousEventId = cachedReceipt.eventId;
|
||||
// Remove the receipt we're about to clobber out of existence from the cache
|
||||
this.receiptCacheByEventId[previousEventId] = (
|
||||
this.receiptCacheByEventId[previousEventId].filter(r => {
|
||||
return r.type !== receiptType || r.userId !== userId;
|
||||
})
|
||||
);
|
||||
|
||||
if (this.receiptCacheByEventId[previousEventId].length < 1) {
|
||||
delete this.receiptCacheByEventId[previousEventId]; // clean up the cache keys
|
||||
}
|
||||
}
|
||||
|
||||
// cache the new one
|
||||
if (!this.receiptCacheByEventId[eventId]) {
|
||||
this.receiptCacheByEventId[eventId] = [];
|
||||
}
|
||||
this.receiptCacheByEventId[eventId].push({
|
||||
userId: userId,
|
||||
type: receiptType as ReceiptType,
|
||||
data: receipt,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of receipts for the given event.
|
||||
* @param {MatrixEvent} event the event to get receipts for
|
||||
* @return {Object[]} A list of receipts with a userId, type and data keys or
|
||||
* an empty list.
|
||||
*/
|
||||
public getReceiptsForEvent(event: MatrixEvent): CachedReceipt[] {
|
||||
return this.receiptCacheByEventId[event.getId()] || [];
|
||||
}
|
||||
|
||||
public abstract addReceipt(event: MatrixEvent, synthetic: boolean): void;
|
||||
|
||||
/**
|
||||
* Add a temporary local-echo receipt to the room to reflect in the
|
||||
* client the fact that we've sent one.
|
||||
* @param {string} userId The user ID if the receipt sender
|
||||
* @param {MatrixEvent} e The event that is to be acknowledged
|
||||
* @param {ReceiptType} receiptType The type of receipt
|
||||
*/
|
||||
public addLocalEchoReceipt(userId: string, e: MatrixEvent, receiptType: ReceiptType): void {
|
||||
this.addReceipt(synthesizeReceipt(userId, e, receiptType), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of user IDs who have <b>read up to</b> the given event.
|
||||
* @param {MatrixEvent} event the event to get read receipts for.
|
||||
* @return {String[]} A list of user IDs.
|
||||
*/
|
||||
public getUsersReadUpTo(event: MatrixEvent): string[] {
|
||||
return this.getReceiptsForEvent(event).filter(function(receipt) {
|
||||
return utils.isSupportedReceiptType(receipt.type);
|
||||
}).map(function(receipt) {
|
||||
return receipt.userId;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given user has read a particular event ID with the known
|
||||
* history of the room. This is not a definitive check as it relies only on
|
||||
* what is available to the room at the time of execution.
|
||||
* @param {String} userId The user ID to check the read state of.
|
||||
* @param {String} eventId The event ID to check if the user read.
|
||||
* @returns {Boolean} True if the user has read the event, false otherwise.
|
||||
*/
|
||||
public hasUserReadEvent(userId: string, eventId: string): boolean {
|
||||
const readUpToId = this.getEventReadUpTo(userId, false);
|
||||
if (readUpToId === eventId) return true;
|
||||
|
||||
if (this.timeline.length
|
||||
&& this.timeline[this.timeline.length - 1].getSender()
|
||||
&& this.timeline[this.timeline.length - 1].getSender() === userId) {
|
||||
// It doesn't matter where the event is in the timeline, the user has read
|
||||
// it because they've sent the latest event.
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = this.timeline.length - 1; i >= 0; --i) {
|
||||
const ev = this.timeline[i];
|
||||
|
||||
// If we encounter the target event first, the user hasn't read it
|
||||
// however if we encounter the readUpToId first then the user has read
|
||||
// it. These rules apply because we're iterating bottom-up.
|
||||
if (ev.getId() === eventId) return false;
|
||||
if (ev.getId() === readUpToId) return true;
|
||||
}
|
||||
|
||||
// We don't know if the user has read it, so assume not.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+47
-304
@@ -47,10 +47,16 @@ import {
|
||||
FILTER_RELATED_BY_SENDERS,
|
||||
ThreadFilterType,
|
||||
} from "./thread";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { ReceiptType } from "../@types/read_receipts";
|
||||
import { IStateEventWithRoomId } from "../@types/search";
|
||||
import { RelationsContainer } from "./relations-container";
|
||||
import {
|
||||
MAIN_ROOM_TIMELINE,
|
||||
ReadReceipt,
|
||||
Receipt,
|
||||
ReceiptContent,
|
||||
synthesizeReceipt,
|
||||
} from "./read-receipt";
|
||||
|
||||
// These constants are used as sane defaults when the homeserver doesn't support
|
||||
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||
@@ -61,23 +67,6 @@ import { RelationsContainer } from "./relations-container";
|
||||
export const KNOWN_SAFE_ROOM_VERSION = '9';
|
||||
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
|
||||
function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent {
|
||||
// console.log("synthesizing receipt for "+event.getId());
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[event.getId()]: {
|
||||
[receiptType]: {
|
||||
[userId]: {
|
||||
ts: event.getTs(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
type: EventType.Receipt,
|
||||
room_id: event.getRoomId(),
|
||||
});
|
||||
}
|
||||
|
||||
interface IOpts {
|
||||
storageToken?: string;
|
||||
pendingEventOrdering?: PendingEventOrdering;
|
||||
@@ -91,40 +80,6 @@ export interface IRecommendedVersion {
|
||||
urgent: boolean;
|
||||
}
|
||||
|
||||
interface IReceipt {
|
||||
ts: number;
|
||||
}
|
||||
|
||||
export interface IWrappedReceipt {
|
||||
eventId: string;
|
||||
data: IReceipt;
|
||||
}
|
||||
|
||||
interface ICachedReceipt {
|
||||
type: ReceiptType;
|
||||
userId: string;
|
||||
data: IReceipt;
|
||||
}
|
||||
|
||||
type ReceiptCache = {[eventId: string]: ICachedReceipt[]};
|
||||
|
||||
interface IReceiptContent {
|
||||
[eventId: string]: {
|
||||
[key in ReceiptType]: {
|
||||
[userId: string]: IReceipt;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const ReceiptPairRealIndex = 0;
|
||||
const ReceiptPairSyntheticIndex = 1;
|
||||
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
|
||||
type Receipts = {
|
||||
[receiptType: string]: {
|
||||
[userId: string]: [IWrappedReceipt, IWrappedReceipt]; // Pair<real receipt, synthetic receipt> (both nullable)
|
||||
};
|
||||
};
|
||||
|
||||
// When inserting a visibility event affecting event `eventId`, we
|
||||
// need to scan through existing visibility events for `eventId`.
|
||||
// In theory, this could take an unlimited amount of time if:
|
||||
@@ -225,15 +180,9 @@ export type RoomEventHandlerMap = {
|
||||
BeaconEvent.Update | BeaconEvent.Destroy | BeaconEvent.LivenessChange
|
||||
>;
|
||||
|
||||
export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap> {
|
||||
export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
|
||||
public readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
|
||||
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
|
||||
// receipts should clobber based on receipt_type and user_id pairs hence
|
||||
// the form of this structure. This is sub-optimal for the exposed APIs
|
||||
// which pass in an event ID and get back some receipts, so we also store
|
||||
// a pre-cached list for this purpose.
|
||||
private receipts: Receipts = {}; // { receipt_type: { user_id: IReceipt } }
|
||||
private receiptCacheByEventId: ReceiptCache = {}; // { event_id: ICachedReceipt[] }
|
||||
private notificationCounts: Partial<Record<NotificationCountType, number>> = {};
|
||||
private readonly timelineSets: EventTimelineSet[];
|
||||
public readonly threadsTimelineSets: EventTimelineSet[] = [];
|
||||
@@ -2400,10 +2349,10 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
threadId,
|
||||
} = this.eventShouldLiveIn(event, events, threadRoots);
|
||||
|
||||
if (shouldLiveInThread && !eventsByThread[threadId]) {
|
||||
eventsByThread[threadId] = [];
|
||||
if (shouldLiveInThread && !eventsByThread[threadId ?? ""]) {
|
||||
eventsByThread[threadId ?? ""] = [];
|
||||
}
|
||||
eventsByThread[threadId]?.push(event);
|
||||
eventsByThread[threadId ?? ""]?.push(event);
|
||||
|
||||
if (shouldLiveInRoom) {
|
||||
this.addLiveEvent(event, options);
|
||||
@@ -2436,17 +2385,17 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
}
|
||||
|
||||
if (shouldLiveInThread) {
|
||||
event.setThreadId(threadId);
|
||||
event.setThreadId(threadId ?? "");
|
||||
memo[THREAD].push(event);
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, [[], []]);
|
||||
}, [[] as MatrixEvent[], [] as MatrixEvent[]]);
|
||||
} else {
|
||||
// When `experimentalThreadSupport` is disabled treat all events as timelineEvents
|
||||
return [
|
||||
events,
|
||||
[],
|
||||
events as MatrixEvent[],
|
||||
[] as MatrixEvent[],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2458,12 +2407,43 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
const threadRoots = new Set<string>();
|
||||
for (const event of events) {
|
||||
if (event.isRelation(THREAD_RELATION_TYPE.name)) {
|
||||
threadRoots.add(event.relationEventId);
|
||||
threadRoots.add(event.relationEventId ?? "");
|
||||
}
|
||||
}
|
||||
return threadRoots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a receipt event to the room.
|
||||
* @param {MatrixEvent} event The m.receipt event.
|
||||
* @param {Boolean} synthetic True if this event is implicit.
|
||||
*/
|
||||
public addReceipt(event: MatrixEvent, synthetic = false): void {
|
||||
const content = event.getContent<ReceiptContent>();
|
||||
Object.keys(content).forEach((eventId: string) => {
|
||||
Object.keys(content[eventId]).forEach((receiptType: ReceiptType | string) => {
|
||||
Object.keys(content[eventId][receiptType]).forEach((userId: string) => {
|
||||
const receipt = content[eventId][receiptType][userId] as Receipt;
|
||||
const receiptForMainTimeline = !receipt.thread_id || receipt.thread_id === MAIN_ROOM_TIMELINE;
|
||||
const receiptDestination: Thread | this | undefined = receiptForMainTimeline
|
||||
? this
|
||||
: this.threads.get(receipt.thread_id ?? "");
|
||||
receiptDestination?.addReceiptToStructure(
|
||||
eventId,
|
||||
receiptType as ReceiptType,
|
||||
userId,
|
||||
receipt,
|
||||
synthetic,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// send events after we've regenerated the structure & cache, otherwise things that
|
||||
// listened for the event would read stale data.
|
||||
this.emit(RoomEvent.Receipt, event, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds/handles ephemeral events such as typing notifications and read receipts.
|
||||
* @param {MatrixEvent[]} events A list of events to process
|
||||
@@ -2554,243 +2534,6 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of user IDs who have <b>read up to</b> the given event.
|
||||
* @param {MatrixEvent} event the event to get read receipts for.
|
||||
* @return {String[]} A list of user IDs.
|
||||
*/
|
||||
public getUsersReadUpTo(event: MatrixEvent): string[] {
|
||||
return this.getReceiptsForEvent(event).filter(function(receipt) {
|
||||
return utils.isSupportedReceiptType(receipt.type);
|
||||
}).map(function(receipt) {
|
||||
return receipt.userId;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the latest receipt for a given user in the room
|
||||
* @param userId The id of the user for which we want the receipt
|
||||
* @param ignoreSynthesized Whether to ignore synthesized receipts or not
|
||||
* @param receiptType Optional. The type of the receipt we want to get
|
||||
* @returns the latest receipts of the chosen type for the chosen user
|
||||
*/
|
||||
public getReadReceiptForUserId(
|
||||
userId: string, ignoreSynthesized = false, receiptType = ReceiptType.Read,
|
||||
): IWrappedReceipt | null {
|
||||
const [realReceipt, syntheticReceipt] = this.receipts[receiptType]?.[userId] ?? [];
|
||||
if (ignoreSynthesized) {
|
||||
return realReceipt;
|
||||
}
|
||||
|
||||
return syntheticReceipt ?? realReceipt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the event that a given user has read up to, or null if we
|
||||
* have received no read receipts from them.
|
||||
* @param {String} userId The user ID to get read receipt event ID for
|
||||
* @param {Boolean} ignoreSynthesized If true, return only receipts that have been
|
||||
* sent by the server, not implicit ones generated
|
||||
* by the JS SDK.
|
||||
* @return {String} ID of the latest event that the given user has read, or null.
|
||||
*/
|
||||
public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null {
|
||||
// XXX: This is very very ugly and I hope I won't have to ever add a new
|
||||
// receipt type here again. IMHO this should be done by the server in
|
||||
// some more intelligent manner or the client should just use timestamps
|
||||
|
||||
const timelineSet = this.getUnfilteredTimelineSet();
|
||||
const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.Read);
|
||||
const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.ReadPrivate);
|
||||
|
||||
// If we have both, compare them
|
||||
let comparison: number | null | undefined;
|
||||
if (publicReadReceipt?.eventId && privateReadReceipt?.eventId) {
|
||||
comparison = timelineSet.compareEventOrdering(publicReadReceipt?.eventId, privateReadReceipt?.eventId);
|
||||
}
|
||||
|
||||
// If we didn't get a comparison try to compare the ts of the receipts
|
||||
if (!comparison && publicReadReceipt?.data?.ts && privateReadReceipt?.data?.ts) {
|
||||
comparison = publicReadReceipt?.data?.ts - privateReadReceipt?.data?.ts;
|
||||
}
|
||||
|
||||
// The public receipt is more likely to drift out of date so the private
|
||||
// one has precedence
|
||||
if (!comparison) return privateReadReceipt?.eventId ?? publicReadReceipt?.eventId ?? null;
|
||||
|
||||
// If public read receipt is older, return the private one
|
||||
return ((comparison < 0) ? privateReadReceipt?.eventId : publicReadReceipt?.eventId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given user has read a particular event ID with the known
|
||||
* history of the room. This is not a definitive check as it relies only on
|
||||
* what is available to the room at the time of execution.
|
||||
* @param {String} userId The user ID to check the read state of.
|
||||
* @param {String} eventId The event ID to check if the user read.
|
||||
* @returns {Boolean} True if the user has read the event, false otherwise.
|
||||
*/
|
||||
public hasUserReadEvent(userId: string, eventId: string): boolean {
|
||||
const readUpToId = this.getEventReadUpTo(userId, false);
|
||||
if (readUpToId === eventId) return true;
|
||||
|
||||
if (this.timeline.length
|
||||
&& this.timeline[this.timeline.length - 1].getSender()
|
||||
&& this.timeline[this.timeline.length - 1].getSender() === userId) {
|
||||
// It doesn't matter where the event is in the timeline, the user has read
|
||||
// it because they've sent the latest event.
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = this.timeline.length - 1; i >= 0; --i) {
|
||||
const ev = this.timeline[i];
|
||||
|
||||
// If we encounter the target event first, the user hasn't read it
|
||||
// however if we encounter the readUpToId first then the user has read
|
||||
// it. These rules apply because we're iterating bottom-up.
|
||||
if (ev.getId() === eventId) return false;
|
||||
if (ev.getId() === readUpToId) return true;
|
||||
}
|
||||
|
||||
// We don't know if the user has read it, so assume not.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of receipts for the given event.
|
||||
* @param {MatrixEvent} event the event to get receipts for
|
||||
* @return {Object[]} A list of receipts with a userId, type and data keys or
|
||||
* an empty list.
|
||||
*/
|
||||
public getReceiptsForEvent(event: MatrixEvent): ICachedReceipt[] {
|
||||
return this.receiptCacheByEventId[event.getId()] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a receipt event to the room.
|
||||
* @param {MatrixEvent} event The m.receipt event.
|
||||
* @param {Boolean} synthetic True if this event is implicit.
|
||||
*/
|
||||
public addReceipt(event: MatrixEvent, synthetic = false): void {
|
||||
this.addReceiptsToStructure(event, synthetic);
|
||||
// send events after we've regenerated the structure & cache, otherwise things that
|
||||
// listened for the event would read stale data.
|
||||
this.emit(RoomEvent.Receipt, event, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a receipt event to the room.
|
||||
* @param {MatrixEvent} event The m.receipt event.
|
||||
* @param {Boolean} synthetic True if this event is implicit.
|
||||
*/
|
||||
private addReceiptsToStructure(event: MatrixEvent, synthetic: boolean): void {
|
||||
const content = event.getContent<IReceiptContent>();
|
||||
Object.keys(content).forEach((eventId) => {
|
||||
Object.keys(content[eventId]).forEach((receiptType) => {
|
||||
Object.keys(content[eventId][receiptType]).forEach((userId) => {
|
||||
const receipt = content[eventId][receiptType][userId];
|
||||
|
||||
if (!this.receipts[receiptType]) {
|
||||
this.receipts[receiptType] = {};
|
||||
}
|
||||
if (!this.receipts[receiptType][userId]) {
|
||||
this.receipts[receiptType][userId] = [null, null];
|
||||
}
|
||||
|
||||
const pair = this.receipts[receiptType][userId];
|
||||
|
||||
let existingReceipt = pair[ReceiptPairRealIndex];
|
||||
if (synthetic) {
|
||||
existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
|
||||
}
|
||||
|
||||
if (existingReceipt) {
|
||||
// we only want to add this receipt if we think it is later than the one we already have.
|
||||
// This is managed server-side, but because we synthesize RRs locally we have to do it here too.
|
||||
const ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
|
||||
existingReceipt.eventId,
|
||||
eventId,
|
||||
);
|
||||
if (ordering !== null && ordering >= 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const wrappedReceipt: IWrappedReceipt = {
|
||||
eventId,
|
||||
data: receipt,
|
||||
};
|
||||
|
||||
const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt;
|
||||
const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex];
|
||||
|
||||
let ordering: number | null = null;
|
||||
if (realReceipt && syntheticReceipt) {
|
||||
ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
|
||||
realReceipt.eventId,
|
||||
syntheticReceipt.eventId,
|
||||
);
|
||||
}
|
||||
|
||||
const preferSynthetic = ordering === null || ordering < 0;
|
||||
|
||||
// we don't bother caching just real receipts by event ID as there's nothing that would read it.
|
||||
// Take the current cached receipt before we overwrite the pair elements.
|
||||
const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
|
||||
|
||||
if (synthetic && preferSynthetic) {
|
||||
pair[ReceiptPairSyntheticIndex] = wrappedReceipt;
|
||||
} else if (!synthetic) {
|
||||
pair[ReceiptPairRealIndex] = wrappedReceipt;
|
||||
|
||||
if (!preferSynthetic) {
|
||||
pair[ReceiptPairSyntheticIndex] = null;
|
||||
}
|
||||
}
|
||||
|
||||
const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
|
||||
if (cachedReceipt === newCachedReceipt) return;
|
||||
|
||||
// clean up any previous cache entry
|
||||
if (cachedReceipt && this.receiptCacheByEventId[cachedReceipt.eventId]) {
|
||||
const previousEventId = cachedReceipt.eventId;
|
||||
// Remove the receipt we're about to clobber out of existence from the cache
|
||||
this.receiptCacheByEventId[previousEventId] = (
|
||||
this.receiptCacheByEventId[previousEventId].filter(r => {
|
||||
return r.type !== receiptType || r.userId !== userId;
|
||||
})
|
||||
);
|
||||
|
||||
if (this.receiptCacheByEventId[previousEventId].length < 1) {
|
||||
delete this.receiptCacheByEventId[previousEventId]; // clean up the cache keys
|
||||
}
|
||||
}
|
||||
|
||||
// cache the new one
|
||||
if (!this.receiptCacheByEventId[eventId]) {
|
||||
this.receiptCacheByEventId[eventId] = [];
|
||||
}
|
||||
this.receiptCacheByEventId[eventId].push({
|
||||
userId: userId,
|
||||
type: receiptType as ReceiptType,
|
||||
data: receipt,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a temporary local-echo receipt to the room to reflect in the
|
||||
* client the fact that we've sent one.
|
||||
* @param {string} userId The user ID if the receipt sender
|
||||
* @param {MatrixEvent} e The event that is to be acknowledged
|
||||
* @param {ReceiptType} receiptType The type of receipt
|
||||
*/
|
||||
public addLocalEchoReceipt(userId: string, e: MatrixEvent, receiptType: ReceiptType): void {
|
||||
this.addReceipt(synthesizeReceipt(userId, e, receiptType), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the room-tag event for the room. The previous one is overwritten.
|
||||
* @param {MatrixEvent} event the m.tag event
|
||||
|
||||
+14
-2
@@ -23,10 +23,10 @@ import { IThreadBundledRelationship, MatrixEvent } from "./event";
|
||||
import { Direction, EventTimeline } from "./event-timeline";
|
||||
import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-set';
|
||||
import { Room } from './room';
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { RoomState } from "./room-state";
|
||||
import { ServerControlledNamespacedValue } from "../NamespacedValue";
|
||||
import { logger } from "../logger";
|
||||
import { ReadReceipt } from "./read-receipt";
|
||||
|
||||
export enum ThreadEvent {
|
||||
New = "Thread.new",
|
||||
@@ -54,7 +54,7 @@ interface IThreadOpts {
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
public static hasServerSideSupport: boolean;
|
||||
|
||||
/**
|
||||
@@ -429,6 +429,18 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
nextBatch,
|
||||
};
|
||||
}
|
||||
|
||||
public getUnfilteredTimelineSet(): EventTimelineSet {
|
||||
return this.timelineSet;
|
||||
}
|
||||
|
||||
public get timeline(): MatrixEvent[] {
|
||||
return this.events;
|
||||
}
|
||||
|
||||
public addReceipt(event: MatrixEvent, synthetic: boolean): void {
|
||||
throw new Error("Unsupported function on the thread model");
|
||||
}
|
||||
}
|
||||
|
||||
export const FILTER_RELATED_BY_SENDERS = new ServerControlledNamespacedValue(
|
||||
|
||||
+1
-1
@@ -162,7 +162,7 @@ export interface IStore {
|
||||
* Get account data event by event type
|
||||
* @param {string} eventType The event type being queried
|
||||
*/
|
||||
getAccountData(eventType: EventType | string): MatrixEvent;
|
||||
getAccountData(eventType: EventType | string): MatrixEvent | undefined;
|
||||
|
||||
/**
|
||||
* setSyncData does nothing as there is no backing data store.
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { ISavedSync } from "./index";
|
||||
import { IEvent, IStartClientOpts, IStateEventWithRoomId, ISyncResponse } from "..";
|
||||
import { IEvent, IStartClientOpts, IStateEventWithRoomId, ISyncResponse } from "../matrix";
|
||||
import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage";
|
||||
|
||||
export interface IIndexedDBBackend {
|
||||
|
||||
@@ -18,41 +18,36 @@ import { IMinimalEvent, ISyncData, ISyncResponse, SyncAccumulator } from "../syn
|
||||
import * as utils from "../utils";
|
||||
import * as IndexedDBHelpers from "../indexeddb-helpers";
|
||||
import { logger } from '../logger';
|
||||
import { IStartClientOpts, IStateEventWithRoomId } from "..";
|
||||
import { IStartClientOpts, IStateEventWithRoomId } from "../matrix";
|
||||
import { ISavedSync } from "./index";
|
||||
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
|
||||
import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage";
|
||||
|
||||
const VERSION = 4;
|
||||
type DbMigration = (db: IDBDatabase) => void;
|
||||
const DB_MIGRATIONS: DbMigration[] = [
|
||||
(db) => {
|
||||
// Make user store, clobber based on user ID. (userId property of User objects)
|
||||
db.createObjectStore("users", { keyPath: ["userId"] });
|
||||
|
||||
function createDatabase(db: IDBDatabase): void {
|
||||
// Make user store, clobber based on user ID. (userId property of User objects)
|
||||
db.createObjectStore("users", { keyPath: ["userId"] });
|
||||
// Make account data store, clobber based on event type.
|
||||
// (event.type property of MatrixEvent objects)
|
||||
db.createObjectStore("accountData", { keyPath: ["type"] });
|
||||
|
||||
// Make account data store, clobber based on event type.
|
||||
// (event.type property of MatrixEvent objects)
|
||||
db.createObjectStore("accountData", { keyPath: ["type"] });
|
||||
|
||||
// Make /sync store (sync tokens, room data, etc), always clobber (const key).
|
||||
db.createObjectStore("sync", { keyPath: ["clobber"] });
|
||||
}
|
||||
|
||||
function upgradeSchemaV2(db: IDBDatabase): void {
|
||||
const oobMembersStore = db.createObjectStore(
|
||||
"oob_membership_events", {
|
||||
keyPath: ["room_id", "state_key"],
|
||||
});
|
||||
oobMembersStore.createIndex("room", "room_id");
|
||||
}
|
||||
|
||||
function upgradeSchemaV3(db: IDBDatabase): void {
|
||||
db.createObjectStore("client_options",
|
||||
{ keyPath: ["clobber"] });
|
||||
}
|
||||
|
||||
function upgradeSchemaV4(db: IDBDatabase): void {
|
||||
db.createObjectStore("to_device_queue", { autoIncrement: true });
|
||||
}
|
||||
// Make /sync store (sync tokens, room data, etc), always clobber (const key).
|
||||
db.createObjectStore("sync", { keyPath: ["clobber"] });
|
||||
},
|
||||
(db) => {
|
||||
const oobMembersStore = db.createObjectStore(
|
||||
"oob_membership_events", {
|
||||
keyPath: ["room_id", "state_key"],
|
||||
});
|
||||
oobMembersStore.createIndex("room", "room_id");
|
||||
},
|
||||
(db) => { db.createObjectStore("client_options", { keyPath: ["clobber"] }); },
|
||||
(db) => { db.createObjectStore("to_device_queue", { autoIncrement: true }); },
|
||||
// Expand as needed.
|
||||
];
|
||||
const VERSION = DB_MIGRATIONS.length;
|
||||
|
||||
/**
|
||||
* Helper method to collect results from a Cursor and promiseify it.
|
||||
@@ -172,20 +167,13 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
|
||||
);
|
||||
if (oldVersion < 1) { // The database did not previously exist.
|
||||
if (oldVersion < 1) {
|
||||
// The database did not previously exist
|
||||
this._isNewlyCreated = true;
|
||||
createDatabase(db);
|
||||
}
|
||||
if (oldVersion < 2) {
|
||||
upgradeSchemaV2(db);
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
upgradeSchemaV3(db);
|
||||
}
|
||||
if (oldVersion < 4) {
|
||||
upgradeSchemaV4(db);
|
||||
}
|
||||
// Expand as needed.
|
||||
DB_MIGRATIONS.forEach((migration, index) => {
|
||||
if (oldVersion <= index) migration(db);
|
||||
});
|
||||
};
|
||||
|
||||
req.onblocked = () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { logger } from "../logger";
|
||||
import { defer, IDeferred } from "../utils";
|
||||
import { ISavedSync } from "./index";
|
||||
import { IStartClientOpts } from "../client";
|
||||
import { IStateEventWithRoomId, ISyncResponse } from "..";
|
||||
import { IStateEventWithRoomId, ISyncResponse } from "../matrix";
|
||||
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
|
||||
import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage";
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ interface IState {
|
||||
export interface ITimeline {
|
||||
events: Array<IRoomEvent | IStateEvent>;
|
||||
limited?: boolean;
|
||||
prev_batch: string;
|
||||
prev_batch: string | null;
|
||||
}
|
||||
|
||||
export interface IJoinedRoom {
|
||||
@@ -401,7 +401,7 @@ export class SyncAccumulator {
|
||||
// typing forever until someone really does start typing (which
|
||||
// will prompt Synapse to send down an actual m.typing event to
|
||||
// clobber the one we persisted).
|
||||
if (e.type !== "m.receipt" || !e.content) {
|
||||
if (e.type !== EventType.Receipt || !e.content) {
|
||||
// This means we'll drop unknown ephemeral events but that
|
||||
// seems okay.
|
||||
return;
|
||||
@@ -528,7 +528,7 @@ export class SyncAccumulator {
|
||||
});
|
||||
Object.keys(this.joinRooms).forEach((roomId) => {
|
||||
const roomData = this.joinRooms[roomId];
|
||||
const roomJson = {
|
||||
const roomJson: IJoinedRoom = {
|
||||
ephemeral: { events: [] },
|
||||
account_data: { events: [] },
|
||||
state: { events: [] },
|
||||
@@ -541,12 +541,12 @@ export class SyncAccumulator {
|
||||
};
|
||||
// Add account data
|
||||
Object.keys(roomData._accountData).forEach((evType) => {
|
||||
roomJson.account_data.events.push(roomData._accountData[evType]);
|
||||
roomJson.account_data.events.push(roomData._accountData[evType] as IMinimalEvent);
|
||||
});
|
||||
|
||||
// Add receipt data
|
||||
const receiptEvent = {
|
||||
type: "m.receipt",
|
||||
type: EventType.Receipt,
|
||||
room_id: roomId,
|
||||
content: {
|
||||
// $event_id: { "m.read": { $user_id: $json } }
|
||||
@@ -566,7 +566,7 @@ export class SyncAccumulator {
|
||||
});
|
||||
// add only if we have some receipt data
|
||||
if (Object.keys(receiptEvent.content).length > 0) {
|
||||
roomJson.ephemeral.events.push(receiptEvent);
|
||||
roomJson.ephemeral.events.push(receiptEvent as IMinimalEvent);
|
||||
}
|
||||
|
||||
// Add timeline data
|
||||
@@ -609,8 +609,8 @@ export class SyncAccumulator {
|
||||
const rollBackState = Object.create(null);
|
||||
for (let i = roomJson.timeline.events.length - 1; i >=0; i--) {
|
||||
const timelineEvent = roomJson.timeline.events[i];
|
||||
if (timelineEvent.state_key === null ||
|
||||
timelineEvent.state_key === undefined) {
|
||||
if ((timelineEvent as IStateEvent).state_key === null ||
|
||||
(timelineEvent as IStateEvent).state_key === undefined) {
|
||||
continue; // not a state event
|
||||
}
|
||||
// since we're going back in time, we need to use the previous
|
||||
|
||||
+43
-3
@@ -1109,7 +1109,20 @@ export class SyncApi {
|
||||
if (Array.isArray(data.to_device?.events) && data.to_device.events.length > 0) {
|
||||
const cancelledKeyVerificationTxns = [];
|
||||
data.to_device.events
|
||||
.map(client.getEventMapper())
|
||||
.filter((eventJSON) => {
|
||||
if (
|
||||
eventJSON.type === EventType.RoomMessageEncrypted &&
|
||||
!(["m.olm.v1.curve25519-aes-sha2"].includes(eventJSON.content?.algorithm))
|
||||
) {
|
||||
logger.log(
|
||||
'Ignoring invalid encrypted to-device event from ' + eventJSON.sender,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map(client.getEventMapper({ toDevice: true }))
|
||||
.map((toDeviceEvent) => { // map is a cheap inline forEach
|
||||
// We want to flag m.key.verification.start events as cancelled
|
||||
// if there's an accompanying m.key.verification.cancel event, so
|
||||
@@ -1185,6 +1198,27 @@ export class SyncApi {
|
||||
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
|
||||
|
||||
await this.processRoomEvents(room, stateEvents);
|
||||
|
||||
const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId())?.getSender();
|
||||
|
||||
if (client.isCryptoEnabled()) {
|
||||
const parkedHistory = await client.crypto.cryptoStore.takeParkedSharedHistory(room.roomId);
|
||||
for (const parked of parkedHistory) {
|
||||
if (parked.senderId === inviter) {
|
||||
await client.crypto.olmDevice.addInboundGroupSession(
|
||||
room.roomId,
|
||||
parked.senderKey,
|
||||
parked.forwardingCurve25519KeyChain,
|
||||
parked.sessionId,
|
||||
parked.sessionKey,
|
||||
parked.keysClaimed,
|
||||
true,
|
||||
{ sharedHistory: true, untrusted: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inviteObj.isBrandNewRoom) {
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
@@ -1235,7 +1269,9 @@ export class SyncApi {
|
||||
if (joinObj.isBrandNewRoom) {
|
||||
// set the back-pagination token. Do this *before* adding any
|
||||
// events so that clients can start back-paginating.
|
||||
room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
|
||||
if (joinObj.timeline.prev_batch !== null) {
|
||||
room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
|
||||
}
|
||||
} else if (joinObj.timeline.limited) {
|
||||
let limited = true;
|
||||
|
||||
@@ -1288,7 +1324,11 @@ export class SyncApi {
|
||||
}
|
||||
}
|
||||
|
||||
await this.processRoomEvents(room, stateEvents, events, syncEventData.fromCache);
|
||||
try {
|
||||
await this.processRoomEvents(room, stateEvents, events, syncEventData.fromCache);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to process events on room ${room.roomId}:`, e);
|
||||
}
|
||||
|
||||
// set summary after processing events,
|
||||
// because it will trigger a name calculation
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ import unhomoglyph from "unhomoglyph";
|
||||
import promiseRetry from "p-retry";
|
||||
|
||||
import type * as NodeCrypto from "crypto";
|
||||
import { MatrixEvent } from ".";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { M_TIMESTAMP } from "./@types/location";
|
||||
import { ReceiptType } from "./@types/read_receipts";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user