You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +03:00
Merge branch 'develop' into robertlong/group-call
This commit is contained in:
22
.eslintrc.js
22
.eslintrc.js
@ -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: [
|
||||
|
3
.github/workflows/release-npm.yml
vendored
3
.github/workflows/release-npm.yml
vendored
@ -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 }}
|
||||
|
10
.github/workflows/static_analysis.yml
vendored
10
.github/workflows/static_analysis.yml
vendored
@ -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
|
||||
|
22
CHANGELOG.md
22
CHANGELOG.md
@ -1,3 +1,25 @@
|
||||
Changes in [20.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0) (2022-10-11)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ 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
|
||||
* Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374.
|
||||
* 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)
|
||||
==================================================================================================
|
||||
|
||||
|
@ -33,10 +33,8 @@ In Node.js
|
||||
----------
|
||||
|
||||
Ensure you have the latest LTS version of Node.js installed.
|
||||
|
||||
This SDK targets Node 12 for compatibility, which translates to ES6. If you're using
|
||||
a bundler like webpack you'll likely have to transpile dependencies, including this
|
||||
SDK, to match your target browsers.
|
||||
This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills.
|
||||
If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn` to the MatrixClient constructor options.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
if you do not have it already.
|
||||
|
22
package.json
22
package.json
@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "20.0.1",
|
||||
"version": "20.1.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=12.9.0"
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
@ -56,7 +56,6 @@
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"another-json": "^0.2.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
@ -64,7 +63,6 @@
|
||||
"matrix-widget-api": "^1.0.0",
|
||||
"p-retry": "4",
|
||||
"qs": "^6.9.6",
|
||||
"request": "^2.88.2",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
@ -81,12 +79,12 @@
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/domexception": "^4.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "16",
|
||||
"@types/request": "^2.48.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"allchange": "^1.0.6",
|
||||
@ -95,18 +93,21 @@
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "8.23.1",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.24.0",
|
||||
"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",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^28.1.3",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^27.5.1",
|
||||
"jest-sonar-reporter": "^2.0.0",
|
||||
"jsdoc": "^3.6.6",
|
||||
"matrix-mock-request": "^2.1.2",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
@ -117,6 +118,9 @@
|
||||
"testMatch": [
|
||||
"<rootDir>/spec/**/*.spec.{js,ts}"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/spec/setupTests.ts"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
|
17
scripts/switch_package_to_release.js
Executable file
17
scripts/switch_package_to_release.js
Executable file
@ -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();
|
@ -30,7 +30,6 @@ import { MockStorageApi } from "./MockStorageApi";
|
||||
import { encodeUri } from "../src/utils";
|
||||
import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration";
|
||||
import { IKeyBackupSession } from "../src/crypto/keybackup";
|
||||
import { IHttpOpts } from "../src/http-api";
|
||||
import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
|
||||
|
||||
/**
|
||||
@ -56,11 +55,11 @@ export class TestClient {
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
|
||||
const fullOptions: ICreateClientOpts = {
|
||||
baseUrl: "http://" + userId + ".test.server",
|
||||
baseUrl: "http://" + userId?.slice(1).replace(":", ".") + ".test.server",
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
request: this.httpBackend.requestFn as IHttpOpts["request"],
|
||||
fetchFn: this.httpBackend.fetchFn as typeof global.fetch,
|
||||
...options,
|
||||
};
|
||||
if (!fullOptions.cryptoStore) {
|
||||
|
@ -15,9 +15,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// stub for browser-matrix browserify tests
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = jest.fn();
|
||||
|
||||
afterAll(() => {
|
||||
// clean up XMLHttpRequest mock
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = undefined;
|
||||
});
|
@ -14,46 +14,66 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// load XmlHttpRequest mock
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import "./setupTests";
|
||||
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import type { MatrixClient, ClientEvent } from "../../src";
|
||||
|
||||
const USER_ID = "@user:test.server";
|
||||
const DEVICE_ID = "device_id";
|
||||
const ACCESS_TOKEN = "access_token";
|
||||
const ROOM_ID = "!room_id:server.test";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
matrixcs: {
|
||||
MatrixClient: typeof MatrixClient;
|
||||
ClientEvent: typeof ClientEvent;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("Browserify Test", function() {
|
||||
let client;
|
||||
let httpBackend;
|
||||
let client: MatrixClient;
|
||||
let httpBackend: HttpBackend;
|
||||
|
||||
beforeEach(() => {
|
||||
const testClient = new TestClient(USER_ID, DEVICE_ID, ACCESS_TOKEN);
|
||||
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
httpBackend = new HttpBackend();
|
||||
client = new global.matrixcs.MatrixClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: USER_ID,
|
||||
accessToken: ACCESS_TOKEN,
|
||||
deviceId: DEVICE_ID,
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
client.stopClient();
|
||||
httpBackend.stop();
|
||||
client.http.abort();
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
await httpBackend.stop();
|
||||
});
|
||||
|
||||
it("Sync", function() {
|
||||
const event = utils.mkMembership({
|
||||
room: ROOM_ID,
|
||||
mship: "join",
|
||||
user: "@other_user:server.test",
|
||||
it("Sync", async () => {
|
||||
const event = {
|
||||
type: "m.room.member",
|
||||
room_id: ROOM_ID,
|
||||
content: {
|
||||
membership: "join",
|
||||
name: "Displayname",
|
||||
});
|
||||
},
|
||||
event_id: "$foobar",
|
||||
};
|
||||
|
||||
const syncData = {
|
||||
next_batch: "batch1",
|
||||
@ -71,11 +91,16 @@ describe("Browserify Test", function() {
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
return Promise.race([
|
||||
httpBackend.flushAllExpected(),
|
||||
new Promise((_, reject) => {
|
||||
client.once("sync.unexpectedError", reject);
|
||||
}),
|
||||
]);
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const syncPromise = new Promise(r => client.once(global.matrixcs.ClientEvent.Sync, r));
|
||||
const unexpectedErrorFn = jest.fn();
|
||||
client.once(global.matrixcs.ClientEvent.SyncUnexpectedError, unexpectedErrorFn);
|
||||
|
||||
client.startClient();
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await syncPromise;
|
||||
expect(unexpectedErrorFn).not.toHaveBeenCalled();
|
||||
}, 20000); // additional timeout as this test can take quite a while
|
||||
});
|
||||
|
@ -290,8 +290,9 @@ describe("DeviceList management:", function() {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
// Alice should be tracking bob's device list
|
||||
expect(bobStat).toBeGreaterThan(
|
||||
0, "Alice should be tracking bob's device list",
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -326,8 +327,9 @@ describe("DeviceList management:", function() {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -362,8 +364,9 @@ describe("DeviceList management:", function() {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -378,13 +381,15 @@ describe("DeviceList management:", function() {
|
||||
anotherTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse([]));
|
||||
await anotherTestClient.flushSync();
|
||||
await anotherTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
await anotherTestClient.client?.crypto?.deviceList?.saveIfDirty();
|
||||
|
||||
// @ts-ignore accessing private property
|
||||
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
const bobStat = data!.trackingStatus['@bob:xyz'];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
0,
|
||||
);
|
||||
});
|
||||
} finally {
|
@ -31,8 +31,9 @@ import '../olm-loader';
|
||||
import { logger } from '../../src/logger';
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { CRYPTO_ENABLED, IUploadKeysRequest } from "../../src/client";
|
||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
|
||||
import { DeviceInfo } from '../../src/crypto/deviceinfo';
|
||||
|
||||
let aliTestClient: TestClient;
|
||||
const roomId = "!room:localhost";
|
||||
@ -71,12 +72,12 @@ function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<num
|
||||
expect(uploader.deviceKeys).toBeTruthy();
|
||||
|
||||
const uploaderKeys = {};
|
||||
uploaderKeys[uploader.deviceId] = uploader.deviceKeys;
|
||||
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys;
|
||||
querier.httpBackend.when("POST", "/keys/query")
|
||||
.respond(200, function(_path, content) {
|
||||
expect(content.device_keys[uploader.userId]).toEqual([]);
|
||||
.respond(200, function(_path, content: IUploadKeysRequest) {
|
||||
expect(content.device_keys![uploader.userId!]).toEqual([]);
|
||||
const result = {};
|
||||
result[uploader.userId] = uploaderKeys;
|
||||
result[uploader.userId!] = uploaderKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
return querier.httpBackend.flush("/keys/query", 1);
|
||||
@ -93,10 +94,10 @@ async function expectAliClaimKeys(): Promise<void> {
|
||||
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/claim",
|
||||
).respond(200, function(_path, content) {
|
||||
const claimType = content.one_time_keys[bobUserId][bobDeviceId];
|
||||
).respond(200, function(_path, content: IUploadKeysRequest) {
|
||||
const claimType = content.one_time_keys![bobUserId][bobDeviceId];
|
||||
expect(claimType).toEqual("signed_curve25519");
|
||||
let keyId = null;
|
||||
let keyId = '';
|
||||
for (keyId in keys) {
|
||||
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf(claimType + ":") === 0) {
|
||||
@ -132,13 +133,13 @@ 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];
|
||||
const devices = data!.devices[bobUserId]!;
|
||||
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
|
||||
expect(devices[bobDeviceId].verified).
|
||||
toBe(0); // DeviceVerification.UNVERIFIED
|
||||
toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
|
||||
});
|
||||
}
|
||||
|
||||
@ -237,7 +238,7 @@ function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
|
||||
|
||||
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
|
||||
const path = "/send/m.room.encrypted/";
|
||||
const prom = new Promise((resolve) => {
|
||||
const prom = new Promise<IContent>((resolve) => {
|
||||
httpBackend.when("PUT", path).respond(200, function(_path, content) {
|
||||
resolve(content);
|
||||
return {
|
||||
@ -252,14 +253,14 @@ async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]):
|
||||
}
|
||||
|
||||
function aliRecvMessage(): Promise<void> {
|
||||
const message = bobMessages.shift();
|
||||
const message = bobMessages.shift()!;
|
||||
return recvMessage(
|
||||
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
|
||||
);
|
||||
}
|
||||
|
||||
function bobRecvMessage(): Promise<void> {
|
||||
const message = aliMessages.shift();
|
||||
const message = aliMessages.shift()!;
|
||||
return recvMessage(
|
||||
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
|
||||
);
|
||||
@ -494,7 +495,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
@ -505,11 +506,11 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
const message = aliMessages.shift();
|
||||
const message = aliMessages.shift()!;
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
@ -569,7 +570,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
@ -664,11 +665,10 @@ describe("MatrixClient crypto", () => {
|
||||
]);
|
||||
logger.log(aliTestClient + ': started');
|
||||
httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (_path, content) => {
|
||||
.respond(200, (_path, content: IUploadKeysRequest) => {
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
expect(Object.keys(content.one_time_keys).length).toBeGreaterThanOrEqual(1);
|
||||
logger.log('received %i one-time keys', Object.keys(content.one_time_keys).length);
|
||||
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
|
||||
// cancel futher calls by telling the client
|
||||
// we have more than we need
|
||||
return {
|
||||
|
@ -1,25 +1,59 @@
|
||||
/*
|
||||
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 HttpBackend from "matrix-mock-request";
|
||||
|
||||
import {
|
||||
ClientEvent,
|
||||
HttpApiEvent,
|
||||
IEvent,
|
||||
MatrixClient,
|
||||
RoomEvent,
|
||||
RoomMemberEvent,
|
||||
RoomStateEvent,
|
||||
UserEvent,
|
||||
} from "../../src";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient events", function() {
|
||||
let client;
|
||||
let httpBackend;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
|
||||
const setupTests = (): [MatrixClient, HttpBackend] => {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
const client = testClient.client;
|
||||
const httpBackend = testClient.httpBackend;
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
|
||||
return [client!, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
[client!, httpBackend] = setupTests();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
httpBackend?.verifyNoOutstandingExpectation();
|
||||
client?.stopClient();
|
||||
return httpBackend?.stop();
|
||||
});
|
||||
|
||||
describe("emissions", function() {
|
||||
@ -93,10 +127,10 @@ describe("MatrixClient events", function() {
|
||||
|
||||
it("should emit events from both the first and subsequent /sync calls",
|
||||
function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
let expectedEvents = [];
|
||||
let expectedEvents: Partial<IEvent>[] = [];
|
||||
expectedEvents = expectedEvents.concat(
|
||||
SYNC_DATA.presence.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
@ -105,7 +139,7 @@ describe("MatrixClient events", function() {
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
|
||||
);
|
||||
|
||||
client.on("event", function(event) {
|
||||
client!.on(ClientEvent.Event, function(event) {
|
||||
let found = false;
|
||||
for (let i = 0; i < expectedEvents.length; i++) {
|
||||
if (expectedEvents[i].event_id === event.getId()) {
|
||||
@ -114,31 +148,27 @@ describe("MatrixClient events", function() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(found).toBe(
|
||||
true, "Unexpected 'event' emitted: " + event.getType(),
|
||||
);
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
// wait for two SYNCING events
|
||||
utils.syncPromise(client).then(() => {
|
||||
return utils.syncPromise(client);
|
||||
utils.syncPromise(client!).then(() => {
|
||||
return utils.syncPromise(client!);
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]).then(() => {
|
||||
expect(expectedEvents.length).toEqual(
|
||||
0, "Failed to see all events from /sync calls",
|
||||
);
|
||||
expect(expectedEvents.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit User events", function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let fired = false;
|
||||
client.on("User.presence", function(event, user) {
|
||||
client!.on(UserEvent.Presence, function(event, user) {
|
||||
fired = true;
|
||||
expect(user).toBeTruthy();
|
||||
expect(event).toBeTruthy();
|
||||
@ -146,58 +176,52 @@ describe("MatrixClient events", function() {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(event.event).toMatch(SYNC_DATA.presence.events[0]);
|
||||
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
|
||||
expect(user.presence).toEqual(
|
||||
SYNC_DATA.presence.events[0].content.presence,
|
||||
SYNC_DATA.presence.events[0]?.content?.presence,
|
||||
);
|
||||
});
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
httpBackend.flushAllExpected().then(function() {
|
||||
expect(fired).toBe(true, "User.presence didn't fire.");
|
||||
httpBackend!.flushAllExpected().then(function() {
|
||||
expect(fired).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Room events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let roomInvokeCount = 0;
|
||||
let roomNameInvokeCount = 0;
|
||||
let timelineFireCount = 0;
|
||||
client.on("Room", function(room) {
|
||||
client!.on(ClientEvent.Room, function(room) {
|
||||
roomInvokeCount++;
|
||||
expect(room.roomId).toEqual("!erufh:bar");
|
||||
});
|
||||
client.on("Room.timeline", function(event, room) {
|
||||
client!.on(RoomEvent.Timeline, function(event, room) {
|
||||
timelineFireCount++;
|
||||
expect(room.roomId).toEqual("!erufh:bar");
|
||||
});
|
||||
client.on("Room.name", function(room) {
|
||||
client!.on(RoomEvent.Name, function(room) {
|
||||
roomNameInvokeCount++;
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 2),
|
||||
]).then(function() {
|
||||
expect(roomInvokeCount).toEqual(
|
||||
1, "Room fired wrong number of times.",
|
||||
);
|
||||
expect(roomNameInvokeCount).toEqual(
|
||||
1, "Room.name fired wrong number of times.",
|
||||
);
|
||||
expect(timelineFireCount).toEqual(
|
||||
3, "Room.timeline fired the wrong number of times",
|
||||
);
|
||||
expect(roomInvokeCount).toEqual(1);
|
||||
expect(roomNameInvokeCount).toEqual(1);
|
||||
expect(timelineFireCount).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomState events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
const roomStateEventTypes = [
|
||||
"m.room.member", "m.room.create",
|
||||
@ -205,126 +229,106 @@ describe("MatrixClient events", function() {
|
||||
let eventsInvokeCount = 0;
|
||||
let membersInvokeCount = 0;
|
||||
let newMemberInvokeCount = 0;
|
||||
client.on("RoomState.events", function(event, state) {
|
||||
client!.on(RoomStateEvent.Events, function(event, state) {
|
||||
eventsInvokeCount++;
|
||||
const index = roomStateEventTypes.indexOf(event.getType());
|
||||
expect(index).not.toEqual(
|
||||
-1, "Unexpected room state event type: " + event.getType(),
|
||||
);
|
||||
expect(index).not.toEqual(-1);
|
||||
if (index >= 0) {
|
||||
roomStateEventTypes.splice(index, 1);
|
||||
}
|
||||
});
|
||||
client.on("RoomState.members", function(event, state, member) {
|
||||
client!.on(RoomStateEvent.Members, function(event, state, member) {
|
||||
membersInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
expect(member.membership).toEqual("join");
|
||||
});
|
||||
client.on("RoomState.newMember", function(event, state, member) {
|
||||
client!.on(RoomStateEvent.NewMember, function(event, state, member) {
|
||||
newMemberInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
expect(member.membership).toBeFalsy();
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 2),
|
||||
]).then(function() {
|
||||
expect(membersInvokeCount).toEqual(
|
||||
1, "RoomState.members fired wrong number of times",
|
||||
);
|
||||
expect(newMemberInvokeCount).toEqual(
|
||||
1, "RoomState.newMember fired wrong number of times",
|
||||
);
|
||||
expect(eventsInvokeCount).toEqual(
|
||||
2, "RoomState.events fired wrong number of times",
|
||||
);
|
||||
expect(membersInvokeCount).toEqual(1);
|
||||
expect(newMemberInvokeCount).toEqual(1);
|
||||
expect(eventsInvokeCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomMember events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
let typingInvokeCount = 0;
|
||||
let powerLevelInvokeCount = 0;
|
||||
let nameInvokeCount = 0;
|
||||
let membershipInvokeCount = 0;
|
||||
client.on("RoomMember.name", function(event, member) {
|
||||
client!.on(RoomMemberEvent.Name, function(event, member) {
|
||||
nameInvokeCount++;
|
||||
});
|
||||
client.on("RoomMember.typing", function(event, member) {
|
||||
client!.on(RoomMemberEvent.Typing, function(event, member) {
|
||||
typingInvokeCount++;
|
||||
expect(member.typing).toBe(true);
|
||||
});
|
||||
client.on("RoomMember.powerLevel", function(event, member) {
|
||||
client!.on(RoomMemberEvent.PowerLevel, function(event, member) {
|
||||
powerLevelInvokeCount++;
|
||||
});
|
||||
client.on("RoomMember.membership", function(event, member) {
|
||||
client!.on(RoomMemberEvent.Membership, function(event, member) {
|
||||
membershipInvokeCount++;
|
||||
expect(member.membership).toEqual("join");
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 2),
|
||||
]).then(function() {
|
||||
expect(typingInvokeCount).toEqual(
|
||||
1, "RoomMember.typing fired wrong number of times",
|
||||
);
|
||||
expect(powerLevelInvokeCount).toEqual(
|
||||
0, "RoomMember.powerLevel fired wrong number of times",
|
||||
);
|
||||
expect(nameInvokeCount).toEqual(
|
||||
0, "RoomMember.name fired wrong number of times",
|
||||
);
|
||||
expect(membershipInvokeCount).toEqual(
|
||||
1, "RoomMember.membership fired wrong number of times",
|
||||
);
|
||||
expect(typingInvokeCount).toEqual(1);
|
||||
expect(powerLevelInvokeCount).toEqual(0);
|
||||
expect(nameInvokeCount).toEqual(0);
|
||||
expect(membershipInvokeCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
|
||||
const error = { errcode: 'M_UNKNOWN_TOKEN' };
|
||||
httpBackend.when("GET", "/sync").respond(401, error);
|
||||
httpBackend!.when("GET", "/sync").respond(401, error);
|
||||
|
||||
let sessionLoggedOutCount = 0;
|
||||
client.on("Session.logged_out", function(errObj) {
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return httpBackend.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(
|
||||
1, "Session.logged_out fired wrong number of times",
|
||||
);
|
||||
return httpBackend!.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
|
||||
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
|
||||
httpBackend.when("GET", "/sync").respond(401, error);
|
||||
httpBackend!.when("GET", "/sync").respond(401, error);
|
||||
|
||||
let sessionLoggedOutCount = 0;
|
||||
client.on("Session.logged_out", function(errObj) {
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return httpBackend.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(
|
||||
1, "Session.logged_out fired wrong number of times",
|
||||
);
|
||||
return httpBackend!.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -5,10 +5,11 @@ import { MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { MemoryStore } from "../../src/store/memory";
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
import { IStore } from "../../src/store";
|
||||
|
||||
describe("MatrixClient opts", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let httpBackend = null;
|
||||
let httpBackend = new HttpBackend();
|
||||
const userId = "@alice:localhost";
|
||||
const userB = "@bob:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
@ -67,7 +68,7 @@ describe("MatrixClient opts", function() {
|
||||
let client;
|
||||
beforeEach(function() {
|
||||
client = new MatrixClient({
|
||||
request: httpBackend.requestFn,
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
store: undefined,
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
@ -99,7 +100,7 @@ describe("MatrixClient opts", function() {
|
||||
];
|
||||
client.on("event", function(event) {
|
||||
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
|
||||
-1, "Recv unexpected event type: " + event.getType(),
|
||||
-1,
|
||||
);
|
||||
expectedEventTypes.splice(
|
||||
expectedEventTypes.indexOf(event.getType()), 1,
|
||||
@ -118,7 +119,7 @@ describe("MatrixClient opts", function() {
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
expect(expectedEventTypes.length).toEqual(
|
||||
0, "Expected to see event types: " + expectedEventTypes,
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -127,8 +128,8 @@ describe("MatrixClient opts", function() {
|
||||
let client;
|
||||
beforeEach(function() {
|
||||
client = new MatrixClient({
|
||||
request: httpBackend.requestFn,
|
||||
store: new MemoryStore(),
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
store: new MemoryStore() as IStore,
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
@ -141,12 +142,12 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
|
||||
it("shouldn't retry sending events", function(done) {
|
||||
httpBackend.when("PUT", "/txn1").fail(500, new MatrixError({
|
||||
httpBackend.when("PUT", "/txn1").respond(500, new MatrixError({
|
||||
errcode: "M_SOMETHING",
|
||||
error: "Ruh roh",
|
||||
}));
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
|
||||
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
|
||||
expect(false).toBe(true);
|
||||
}, function(err) {
|
||||
expect(err.errcode).toEqual("M_SOMETHING");
|
||||
done();
|
127
spec/integ/matrix-client-relations.spec.ts
Normal file
127
spec/integ/matrix-client-relations.spec.ts
Normal file
@ -0,0 +1,127 @@
|
||||
/*
|
||||
Copyright 2022 Dominik Henneke
|
||||
Copyright 2022 Nordeck IT + Consulting GmbH.
|
||||
|
||||
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 HttpBackend from "matrix-mock-request";
|
||||
|
||||
import { Direction, MatrixClient, MatrixScheduler } from "../../src/matrix";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient relations", () => {
|
||||
const userId = "@alice:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!room:here";
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
|
||||
const setupTests = (): [MatrixClient, HttpBackend] => {
|
||||
const scheduler = new MatrixScheduler();
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{ scheduler },
|
||||
);
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
|
||||
return [client, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
[client, httpBackend] = setupTests();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
it("should read related events with the default options", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', null, null);
|
||||
|
||||
httpBackend!
|
||||
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||
});
|
||||
|
||||
it("should read related events with relation type", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', 'm.reference', null);
|
||||
|
||||
httpBackend!
|
||||
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||
});
|
||||
|
||||
it("should read related events with relation type and event type", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', 'm.reference', 'm.room.message');
|
||||
|
||||
httpBackend!
|
||||
.when(
|
||||
"GET",
|
||||
"/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b",
|
||||
)
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||
});
|
||||
|
||||
it("should read related events with custom options", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', null, null, {
|
||||
dir: Direction.Forward,
|
||||
from: 'FROM',
|
||||
limit: 10,
|
||||
to: 'TO',
|
||||
});
|
||||
|
||||
httpBackend!
|
||||
.when(
|
||||
"GET",
|
||||
"/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO",
|
||||
)
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||
});
|
||||
|
||||
it('should use default direction in the fetchRelations endpoint', async () => {
|
||||
const response = client!.fetchRelations(roomId, '$event-0', null, null);
|
||||
|
||||
httpBackend!
|
||||
.when(
|
||||
"GET",
|
||||
"/rooms/!room%3Ahere/relations/%24event-0?dir=b",
|
||||
)
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "chunk": [], "next_batch": "NEXT" });
|
||||
});
|
||||
});
|
@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventStatus, RoomEvent, MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src/matrix";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient retrying", function() {
|
||||
let client: MatrixClient = null;
|
||||
let httpBackend: TestClient["httpBackend"] = null;
|
||||
let scheduler;
|
||||
const userId = "@alice:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!room:here";
|
||||
let room: Room;
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
let room: Room | undefined;
|
||||
|
||||
beforeEach(function() {
|
||||
scheduler = new MatrixScheduler();
|
||||
const setupTests = (): [MatrixClient, HttpBackend, Room] => {
|
||||
const scheduler = new MatrixScheduler();
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
@ -37,15 +37,21 @@ describe("MatrixClient retrying", function() {
|
||||
undefined,
|
||||
{ scheduler },
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
room = new Room(roomId, client, userId);
|
||||
client.store.storeRoom(room);
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
const room = new Room(roomId, client, userId);
|
||||
client!.store.storeRoom(room);
|
||||
|
||||
return [client, httpBackend, room];
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
[client, httpBackend, room] = setupTests();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
return httpBackend.stop();
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
xit("should retry according to MatrixScheduler.retryFn", function() {
|
||||
@ -66,7 +72,7 @@ describe("MatrixClient retrying", function() {
|
||||
|
||||
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
|
||||
// send a couple of events; the second will be queued
|
||||
const p1 = client.sendMessage(roomId, {
|
||||
const p1 = client!.sendMessage(roomId, {
|
||||
"msgtype": "m.text",
|
||||
"body": "m1",
|
||||
}).then(function() {
|
||||
@ -79,13 +85,13 @@ describe("MatrixClient retrying", function() {
|
||||
// XXX: it turns out that the promise returned by this message
|
||||
// never gets resolved.
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/496
|
||||
client.sendMessage(roomId, {
|
||||
client!.sendMessage(roomId, {
|
||||
"msgtype": "m.text",
|
||||
"body": "m2",
|
||||
});
|
||||
|
||||
// both events should be in the timeline at this point
|
||||
const tl = room.getLiveTimeline().getEvents();
|
||||
const tl = room!.getLiveTimeline().getEvents();
|
||||
expect(tl.length).toEqual(2);
|
||||
const ev1 = tl[0];
|
||||
const ev2 = tl[1];
|
||||
@ -94,24 +100,24 @@ describe("MatrixClient retrying", function() {
|
||||
expect(ev2.status).toEqual(EventStatus.SENDING);
|
||||
|
||||
// the first message should get sent, and the second should get queued
|
||||
httpBackend.when("PUT", "/send/m.room.message/").check(function() {
|
||||
httpBackend!.when("PUT", "/send/m.room.message/").check(function() {
|
||||
// ev2 should now have been queued
|
||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||
|
||||
// now we can cancel the second and check everything looks sane
|
||||
client.cancelPendingEvent(ev2);
|
||||
client!.cancelPendingEvent(ev2);
|
||||
expect(ev2.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
// shouldn't be able to cancel the first message yet
|
||||
expect(function() {
|
||||
client.cancelPendingEvent(ev1);
|
||||
client!.cancelPendingEvent(ev1);
|
||||
}).toThrow();
|
||||
}).respond(400); // fail the first message
|
||||
|
||||
// wait for the localecho of ev1 to be updated
|
||||
const p3 = new Promise<void>((resolve, reject) => {
|
||||
room.on(RoomEvent.LocalEchoUpdated, (ev0) => {
|
||||
room!.on(RoomEvent.LocalEchoUpdated, (ev0) => {
|
||||
if (ev0 === ev1) {
|
||||
resolve();
|
||||
}
|
||||
@ -121,7 +127,7 @@ describe("MatrixClient retrying", function() {
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
// cancel the first message
|
||||
client.cancelPendingEvent(ev1);
|
||||
client!.cancelPendingEvent(ev1);
|
||||
expect(ev1.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(0);
|
||||
});
|
||||
@ -129,7 +135,7 @@ describe("MatrixClient retrying", function() {
|
||||
return Promise.all([
|
||||
p1,
|
||||
p3,
|
||||
httpBackend.flushAllExpected(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -1,16 +1,35 @@
|
||||
/*
|
||||
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 HttpBackend from "matrix-mock-request";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { EventStatus } from "../../src/models/event";
|
||||
import { RoomEvent } from "../../src";
|
||||
import { MatrixError, ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient room timelines", function() {
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!foo:bar";
|
||||
const otherUserId = "@bob:localhost";
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: userName,
|
||||
});
|
||||
@ -55,8 +74,7 @@ describe("MatrixClient room timelines", function() {
|
||||
},
|
||||
};
|
||||
|
||||
function setNextSyncData(events) {
|
||||
events = events || [];
|
||||
function setNextSyncData(events: Partial<IEvent>[] = []) {
|
||||
NEXT_SYNC_DATA = {
|
||||
next_batch: "n",
|
||||
presence: { events: [] },
|
||||
@ -77,19 +95,9 @@ describe("MatrixClient room timelines", function() {
|
||||
throw new Error("setNextSyncData only works with one room id");
|
||||
}
|
||||
if (e.state_key) {
|
||||
if (e.__prev_event === undefined) {
|
||||
throw new Error(
|
||||
"setNextSyncData needs the prev state set to '__prev_event' " +
|
||||
"for " + e.type,
|
||||
);
|
||||
}
|
||||
if (e.__prev_event !== null) {
|
||||
// push the previous state for this event type
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].state.events.push(e.__prev_event);
|
||||
}
|
||||
// push the current
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
|
||||
} else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
|
||||
} else if (["m.typing", "m.receipt"].indexOf(e.type!) !== -1) {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
|
||||
} else {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
|
||||
@ -97,7 +105,7 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
const setupTestClient = (): [MatrixClient, HttpBackend] => {
|
||||
// these tests should work with or without timelineSupport
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
@ -106,41 +114,46 @@ describe("MatrixClient room timelines", function() {
|
||||
undefined,
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
|
||||
setNextSyncData();
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return [client!, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(async function() {
|
||||
[client!, httpBackend] = setupTestClient();
|
||||
await httpBackend.flush("/versions");
|
||||
await httpBackend.flush("/pushrules");
|
||||
await httpBackend.flush("/filter");
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
client!.stopClient();
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
describe("local echo events", function() {
|
||||
it("should be added immediately after calling MatrixClient.sendEvent " +
|
||||
"with EventStatus.SENDING and the right event.sender", function(done) {
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
// check it was added
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
// check status
|
||||
@ -150,68 +163,68 @@ describe("MatrixClient room timelines", function() {
|
||||
expect(member.userId).toEqual(userId);
|
||||
expect(member.name).toEqual(userName);
|
||||
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
httpBackend!.flush("/sync", 1).then(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should be updated correctly when the send request finishes " +
|
||||
"BEFORE the event comes down the event stream", function(done) {
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
const ev = utils.mkMessage({
|
||||
body: "I am a fish", user: userId, room: roomId,
|
||||
msg: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
client.sendTextMessage(roomId, "I am a fish", "txn1").then(
|
||||
const room = client!.getRoom(roomId)!;
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(
|
||||
function() {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
httpBackend!.flush("/sync", 1).then(function() {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/txn1", 1);
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should be updated correctly when the send request finishes " +
|
||||
"AFTER the event comes down the event stream", function(done) {
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
const ev = utils.mkMessage({
|
||||
body: "I am a fish", user: userId, room: roomId,
|
||||
msg: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
const room = client!.getRoom(roomId)!;
|
||||
const promise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
httpBackend!.flush("/sync", 1).then(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
httpBackend.flush("/txn1", 1);
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
promise.then(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
@ -219,7 +232,7 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -229,7 +242,7 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
sbEvents = [];
|
||||
httpBackend.when("GET", "/messages").respond(200, function() {
|
||||
httpBackend!.when("GET", "/messages").respond(200, function() {
|
||||
return {
|
||||
chunk: sbEvents,
|
||||
start: "pagin_start",
|
||||
@ -240,26 +253,26 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
it("should set Room.oldState.paginationToken to null at the start" +
|
||||
" of the timeline.", function(done) {
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).then(function() {
|
||||
client!.scrollback(room).then(function() {
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.oldState.paginationToken).toBe(null);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should set the right event.sender values", function(done) {
|
||||
@ -275,7 +288,7 @@ describe("MatrixClient room timelines", function() {
|
||||
// make an m.room.member event for alice's join
|
||||
const joinMshipEvent = utils.mkMembership({
|
||||
mship: "join", user: userId, room: roomId, name: "Old Alice",
|
||||
url: null,
|
||||
url: undefined,
|
||||
});
|
||||
|
||||
// make an m.room.member event with prev_content for alice's nick
|
||||
@ -286,7 +299,7 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
oldMshipEvent.prev_content = {
|
||||
displayname: "Old Alice",
|
||||
avatar_url: null,
|
||||
avatar_url: undefined,
|
||||
membership: "join",
|
||||
};
|
||||
|
||||
@ -303,15 +316,15 @@ describe("MatrixClient room timelines", function() {
|
||||
joinMshipEvent,
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
// sync response
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).then(function() {
|
||||
client!.scrollback(room).then(function() {
|
||||
expect(room.timeline.length).toEqual(5);
|
||||
const joinMsg = room.timeline[0];
|
||||
expect(joinMsg.sender.name).toEqual("Old Alice");
|
||||
@ -321,14 +334,14 @@ describe("MatrixClient room timelines", function() {
|
||||
expect(newMsg.sender.name).toEqual(userName);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should add it them to the right place in the timeline", function(done) {
|
||||
@ -342,27 +355,27 @@ describe("MatrixClient room timelines", function() {
|
||||
}),
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).then(function() {
|
||||
client!.scrollback(room).then(function() {
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
expect(room.timeline[0].event).toEqual(sbEvents[1]);
|
||||
expect(room.timeline[1].event).toEqual(sbEvents[0]);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should use 'end' as the next pagination token", function(done) {
|
||||
@ -373,25 +386,25 @@ describe("MatrixClient room timelines", function() {
|
||||
}),
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.oldState.paginationToken).toBeTruthy();
|
||||
|
||||
client.scrollback(room, 1).then(function() {
|
||||
client!.scrollback(room, 1).then(function() {
|
||||
expect(room.oldState.paginationToken).toEqual(sbEndTok);
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1).then(function() {
|
||||
httpBackend!.flush("/messages", 1).then(function() {
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -404,23 +417,23 @@ describe("MatrixClient room timelines", function() {
|
||||
setNextSyncData(eventData);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
|
||||
let index = 0;
|
||||
client.on("Room.timeline", function(event, rm, toStart) {
|
||||
client!.on(RoomEvent.Timeline, function(event, rm, toStart) {
|
||||
expect(toStart).toBe(false);
|
||||
expect(rm).toEqual(room);
|
||||
expect(event.event).toEqual(eventData[index]);
|
||||
index += 1;
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend!.flush("/messages", 1);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(index).toEqual(2);
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
@ -442,17 +455,16 @@ describe("MatrixClient room timelines", function() {
|
||||
}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
|
||||
setNextSyncData(eventData);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
const preNameEvent = room.timeline[room.timeline.length - 3];
|
||||
const postNameEvent = room.timeline[room.timeline.length - 1];
|
||||
@ -468,22 +480,21 @@ describe("MatrixClient room timelines", function() {
|
||||
name: "Room 2",
|
||||
},
|
||||
});
|
||||
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
|
||||
setNextSyncData([secondRoomNameEvent]);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
let nameEmitCount = 0;
|
||||
client.on("Room.name", function(rm) {
|
||||
client!.on(RoomEvent.Name, function(rm) {
|
||||
nameEmitCount += 1;
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(nameEmitCount).toEqual(1);
|
||||
expect(room.name).toEqual("Room 2");
|
||||
@ -493,12 +504,11 @@ describe("MatrixClient room timelines", function() {
|
||||
name: "Room 3",
|
||||
},
|
||||
});
|
||||
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
|
||||
setNextSyncData([thirdRoomNameEvent]);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]);
|
||||
}).then(function() {
|
||||
expect(nameEmitCount).toEqual(2);
|
||||
@ -518,26 +528,24 @@ describe("MatrixClient room timelines", function() {
|
||||
user: userC, room: roomId, mship: "invite", skey: userD,
|
||||
}),
|
||||
];
|
||||
eventData[0].__prev_event = null;
|
||||
eventData[1].__prev_event = null;
|
||||
setNextSyncData(eventData);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(room.currentState.getMembers().length).toEqual(4);
|
||||
expect(room.currentState.getMember(userC).name).toEqual("C");
|
||||
expect(room.currentState.getMember(userC).membership).toEqual(
|
||||
expect(room.currentState.getMember(userC)!.name).toEqual("C");
|
||||
expect(room.currentState.getMember(userC)!.membership).toEqual(
|
||||
"join",
|
||||
);
|
||||
expect(room.currentState.getMember(userD).name).toEqual(userD);
|
||||
expect(room.currentState.getMember(userD).membership).toEqual(
|
||||
expect(room.currentState.getMember(userD)!.name).toEqual(userD);
|
||||
expect(room.currentState.getMember(userD)!.membership).toEqual(
|
||||
"invite",
|
||||
);
|
||||
});
|
||||
@ -554,26 +562,26 @@ describe("MatrixClient room timelines", function() {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/versions", 1),
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/versions", 1),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend!.flush("/messages", 1);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.timeline[0].event).toEqual(eventData[0]);
|
||||
expect(room.currentState.getMembers().length).toEqual(2);
|
||||
expect(room.currentState.getMember(userId).name).toEqual(userName);
|
||||
expect(room.currentState.getMember(userId).membership).toEqual(
|
||||
expect(room.currentState.getMember(userId)!.name).toEqual(userName);
|
||||
expect(room.currentState.getMember(userId)!.membership).toEqual(
|
||||
"join",
|
||||
);
|
||||
expect(room.currentState.getMember(otherUserId).name).toEqual("Bob");
|
||||
expect(room.currentState.getMember(otherUserId).membership).toEqual(
|
||||
expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob");
|
||||
expect(room.currentState.getMember(otherUserId)!.membership).toEqual(
|
||||
"join",
|
||||
);
|
||||
});
|
||||
@ -588,21 +596,21 @@ describe("MatrixClient room timelines", function() {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
|
||||
let emitCount = 0;
|
||||
client.on("Room.timelineReset", function(emitRoom) {
|
||||
client!.on(RoomEvent.TimelineReset, function(emitRoom) {
|
||||
expect(emitRoom).toEqual(room);
|
||||
emitCount++;
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend!.flush("/messages", 1);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
@ -618,7 +626,7 @@ describe("MatrixClient room timelines", function() {
|
||||
];
|
||||
|
||||
const contextUrl = `/rooms/${encodeURIComponent(roomId)}/context/` +
|
||||
`${encodeURIComponent(initialSyncEventData[2].event_id)}`;
|
||||
`${encodeURIComponent(initialSyncEventData[2].event_id!)}`;
|
||||
const contextResponse = {
|
||||
start: "start_token",
|
||||
events_before: [initialSyncEventData[1], initialSyncEventData[0]],
|
||||
@ -636,19 +644,19 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
// Create a room from the sync
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 1),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 1),
|
||||
]);
|
||||
|
||||
// Get the room after the first sync so the room is created
|
||||
room = client.getRoom(roomId);
|
||||
room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should clear and refresh messages in timeline', async () => {
|
||||
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
|
||||
// to construct a new timeline from.
|
||||
httpBackend.when("GET", contextUrl)
|
||||
httpBackend!.when("GET", contextUrl)
|
||||
.respond(200, function() {
|
||||
// The timeline should be cleared at this point in the refresh
|
||||
expect(room.timeline.length).toEqual(0);
|
||||
@ -659,7 +667,7 @@ describe("MatrixClient room timelines", function() {
|
||||
// Refresh the timeline.
|
||||
await Promise.all([
|
||||
room.refreshLiveTimeline(),
|
||||
httpBackend.flushAllExpected(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
|
||||
// Make sure the message are visible
|
||||
@ -681,7 +689,7 @@ describe("MatrixClient room timelines", function() {
|
||||
// middle of all of this refresh timeline logic. We want to make
|
||||
// sure the sync pagination still works as expected after messing
|
||||
// the refresh timline logic messes with the pagination tokens.
|
||||
httpBackend.when("GET", contextUrl)
|
||||
httpBackend!.when("GET", contextUrl)
|
||||
.respond(200, () => {
|
||||
// Now finally return and make the `/context` request respond
|
||||
return contextResponse;
|
||||
@ -700,7 +708,7 @@ describe("MatrixClient room timelines", function() {
|
||||
const racingSyncEventData = [
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
const waitForRaceySyncAfterResetPromise = new Promise((resolve, reject) => {
|
||||
const waitForRaceySyncAfterResetPromise = new Promise<void>((resolve, reject) => {
|
||||
let eventFired = false;
|
||||
// Throw a more descriptive error if this part of the test times out.
|
||||
const failTimeout = setTimeout(() => {
|
||||
@ -726,12 +734,12 @@ describe("MatrixClient room timelines", function() {
|
||||
// Then make a `/sync` happen by sending a message and seeing that it
|
||||
// shows up (simulate a /sync naturally racing with us).
|
||||
setNextSyncData(racingSyncEventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
||||
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
await Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client, 1),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!, 1),
|
||||
]);
|
||||
// Make sure the timeline has the racey sync data
|
||||
const afterRaceySyncTimelineEvents = room
|
||||
@ -761,7 +769,7 @@ describe("MatrixClient room timelines", function() {
|
||||
await Promise.all([
|
||||
refreshLiveTimelinePromise,
|
||||
// Then flush the remaining `/context` to left the refresh logic complete
|
||||
httpBackend.flushAllExpected(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
|
||||
// Make sure sync pagination still works by seeing a new message show up
|
||||
@ -770,12 +778,12 @@ describe("MatrixClient room timelines", function() {
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(afterRefreshEventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
||||
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 1),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 1),
|
||||
]);
|
||||
|
||||
// Make sure the timeline includes the the events from the `/sync`
|
||||
@ -794,22 +802,19 @@ describe("MatrixClient room timelines", function() {
|
||||
it('Timeline recovers after `/context` request to generate new timeline fails', async () => {
|
||||
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
|
||||
// to construct a new timeline from.
|
||||
httpBackend.when("GET", contextUrl)
|
||||
.respond(500, function() {
|
||||
httpBackend!.when("GET", contextUrl).check(() => {
|
||||
// The timeline should be cleared at this point in the refresh
|
||||
expect(room.timeline.length).toEqual(0);
|
||||
|
||||
return {
|
||||
}).respond(500, new MatrixError({
|
||||
errcode: 'TEST_FAKE_ERROR',
|
||||
error: 'We purposely intercepted this /context request to make it fail ' +
|
||||
'in order to test whether the refresh timeline code is resilient',
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
// Refresh the timeline and expect it to fail
|
||||
const settledFailedRefreshPromises = await Promise.allSettled([
|
||||
room.refreshLiveTimeline(),
|
||||
httpBackend.flushAllExpected(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
// We only expect `TEST_FAKE_ERROR` here. Anything else is
|
||||
// unexpected and should fail the test.
|
||||
@ -825,7 +830,7 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
// `/messages` request for `refreshLiveTimeline()` ->
|
||||
// `getLatestTimeline()` to construct a new timeline from.
|
||||
httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
|
||||
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
|
||||
.respond(200, function() {
|
||||
return {
|
||||
chunk: [{
|
||||
@ -837,7 +842,7 @@ describe("MatrixClient room timelines", function() {
|
||||
// `/context` request for `refreshLiveTimeline()` ->
|
||||
// `getLatestTimeline()` -> `getEventTimeline()` to construct a new
|
||||
// timeline from.
|
||||
httpBackend.when("GET", contextUrl)
|
||||
httpBackend!.when("GET", contextUrl)
|
||||
.respond(200, function() {
|
||||
// The timeline should be cleared at this point in the refresh
|
||||
expect(room.timeline.length).toEqual(0);
|
||||
@ -848,7 +853,7 @@ describe("MatrixClient room timelines", function() {
|
||||
// Refresh the timeline again but this time it should pass
|
||||
await Promise.all([
|
||||
room.refreshLiveTimeline(),
|
||||
httpBackend.flushAllExpected(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
|
||||
// Make sure sync pagination still works by seeing a new message show up
|
||||
@ -857,12 +862,12 @@ describe("MatrixClient room timelines", function() {
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(afterRefreshEventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
||||
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 1),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 1),
|
||||
]);
|
||||
|
||||
// Make sure the message are visible
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import { Optional } from "matrix-events-sdk/lib/types";
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import {
|
||||
@ -29,13 +28,18 @@ import {
|
||||
MatrixClient,
|
||||
ClientEvent,
|
||||
IndexedDBCryptoStore,
|
||||
ISyncResponse,
|
||||
IRoomEvent,
|
||||
IJoinedRoom,
|
||||
IStateEvent,
|
||||
IMinimalEvent,
|
||||
NotificationCountType,
|
||||
} from "../../src";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from '../../src/@types/sync';
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient syncing", () => {
|
||||
let client: Optional<MatrixClient> = null;
|
||||
let httpBackend: Optional<HttpBackend> = null;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
const otherUserId = "@bob:localhost";
|
||||
@ -44,14 +48,21 @@ describe("MatrixClient syncing", () => {
|
||||
const userC = "@claire:bar";
|
||||
const roomOne = "!foo:localhost";
|
||||
const roomTwo = "!bar:localhost";
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
const setupTestClient = (): [MatrixClient, HttpBackend] => {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
return [client, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
[client, httpBackend] = setupTestClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -80,7 +91,7 @@ describe("MatrixClient syncing", () => {
|
||||
it("should pass the 'next_batch' token from /sync to the since= param of the next /sync", (done) => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend!.when("GET", "/sync").check((req) => {
|
||||
expect(req.queryParams.since).toEqual(syncData.next_batch);
|
||||
expect(req.queryParams!.since).toEqual(syncData.next_batch);
|
||||
}).respond(200, syncData);
|
||||
|
||||
client!.startClient();
|
||||
@ -91,7 +102,7 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
|
||||
await client.initCrypto();
|
||||
await client!.initCrypto();
|
||||
|
||||
const roomId = "!cycles:example.org";
|
||||
|
||||
@ -202,7 +213,7 @@ describe("MatrixClient syncing", () => {
|
||||
client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true);
|
||||
|
||||
httpBackend!.when("GET", "/sync").check((req) => {
|
||||
expect(JSON.parse(req.queryParams.filter).room.state.lazy_load_members).toBeTruthy();
|
||||
expect(JSON.parse(req.queryParams!.filter).room.state.lazy_load_members).toBeTruthy();
|
||||
}).respond(200, syncData);
|
||||
|
||||
client!.setGuest(false);
|
||||
@ -217,7 +228,7 @@ describe("MatrixClient syncing", () => {
|
||||
client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true);
|
||||
|
||||
httpBackend!.when("GET", "/sync").check((req) => {
|
||||
expect(JSON.parse(req.queryParams.filter).room?.state?.lazy_load_members).toBeFalsy();
|
||||
expect(JSON.parse(req.queryParams!.filter).room?.state?.lazy_load_members).toBeFalsy();
|
||||
}).respond(200, syncData);
|
||||
|
||||
client!.setGuest(true);
|
||||
@ -225,6 +236,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", () => {
|
||||
@ -237,11 +286,11 @@ describe("MatrixClient syncing", () => {
|
||||
it("should only apply initialSyncLimit to the initial sync", () => {
|
||||
// 1st request
|
||||
httpBackend!.when("GET", "/sync").check((req) => {
|
||||
expect(JSON.parse(req.queryParams.filter).room.timeline.limit).toEqual(1);
|
||||
expect(JSON.parse(req.queryParams!.filter).room.timeline.limit).toEqual(1);
|
||||
}).respond(200, syncData);
|
||||
// 2nd request
|
||||
httpBackend!.when("GET", "/sync").check((req) => {
|
||||
expect(req.queryParams.filter).toEqual("a filter id");
|
||||
expect(req.queryParams!.filter).toEqual("a filter id");
|
||||
}).respond(200, syncData);
|
||||
|
||||
client!.startClient({ initialSyncLimit: 1 });
|
||||
@ -252,7 +301,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
it("should not apply initialSyncLimit to a first sync if we have a stored token", () => {
|
||||
httpBackend!.when("GET", "/sync").check((req) => {
|
||||
expect(req.queryParams.filter).toEqual("a filter id");
|
||||
expect(req.queryParams!.filter).toEqual("a filter id");
|
||||
}).respond(200, syncData);
|
||||
|
||||
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
|
||||
@ -263,26 +312,29 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
describe("resolving invites to profile info", () => {
|
||||
const syncData = {
|
||||
const syncData: ISyncResponse = {
|
||||
account_data: {
|
||||
events: [],
|
||||
},
|
||||
next_batch: "s_5_3",
|
||||
presence: {
|
||||
events: [],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
},
|
||||
join: {},
|
||||
invite: {},
|
||||
leave: {},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
syncData.presence.events = [];
|
||||
syncData.presence!.events = [];
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
}) as IRoomEvent,
|
||||
],
|
||||
},
|
||||
state: {
|
||||
@ -301,14 +353,14 @@ describe("MatrixClient syncing", () => {
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
} as unknown as IJoinedRoom;
|
||||
});
|
||||
|
||||
it("should resolve incoming invites from /sync", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
}) as IStateEvent,
|
||||
);
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
@ -327,26 +379,26 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
const member = client!.getRoom(roomOne).getMember(userC);
|
||||
const member = client!.getRoom(roomOne)!.getMember(userC)!;
|
||||
expect(member.name).toEqual("The Boss");
|
||||
expect(
|
||||
member.getAvatarUrl("home.server.url", null, null, null, false, false),
|
||||
member.getAvatarUrl("home.server.url", 1, 1, '', false, false),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should use cached values from m.presence wherever possible", () => {
|
||||
syncData.presence.events = [
|
||||
syncData.presence!.events = [
|
||||
utils.mkPresence({
|
||||
user: userC,
|
||||
presence: "online",
|
||||
name: "The Ghost",
|
||||
}),
|
||||
}) as IMinimalEvent,
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
}) as IStateEvent,
|
||||
);
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
@ -359,28 +411,28 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
const member = client!.getRoom(roomOne).getMember(userC);
|
||||
const member = client!.getRoom(roomOne)!.getMember(userC)!;
|
||||
expect(member.name).toEqual("The Ghost");
|
||||
});
|
||||
});
|
||||
|
||||
it("should result in events on the room member firing", () => {
|
||||
syncData.presence.events = [
|
||||
syncData.presence!.events = [
|
||||
utils.mkPresence({
|
||||
user: userC,
|
||||
presence: "online",
|
||||
name: "The Ghost",
|
||||
}),
|
||||
}) as IMinimalEvent,
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
}) as IStateEvent,
|
||||
);
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
let latestFiredName = null;
|
||||
let latestFiredName: string;
|
||||
client!.on(RoomMemberEvent.Name, (event, m) => {
|
||||
if (m.userId === userC && m.roomId === roomOne) {
|
||||
latestFiredName = m.name;
|
||||
@ -403,7 +455,7 @@ describe("MatrixClient syncing", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
}) as IStateEvent,
|
||||
);
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
@ -414,10 +466,10 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
const member = client!.getRoom(roomOne).getMember(userC);
|
||||
const member = client!.getRoom(roomOne)!.getMember(userC)!;
|
||||
expect(member.name).toEqual(userC);
|
||||
expect(
|
||||
member.getAvatarUrl("home.server.url", null, null, null, false, false),
|
||||
member.getAvatarUrl("home.server.url", 1, 1, '', false, false),
|
||||
).toBe(null);
|
||||
});
|
||||
});
|
||||
@ -449,8 +501,8 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
expect(client!.getUser(userA).presence).toEqual("online");
|
||||
expect(client!.getUser(userB).presence).toEqual("unavailable");
|
||||
expect(client!.getUser(userA)!.presence).toEqual("online");
|
||||
expect(client!.getUser(userB)!.presence).toEqual("unavailable");
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -571,7 +623,7 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
// should have clobbered the name to the one from /events
|
||||
expect(room.name).toEqual(
|
||||
nextSyncData.rooms.join[roomOne].state.events[0].content.name,
|
||||
@ -589,7 +641,7 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomTwo);
|
||||
const room = client!.getRoom(roomTwo)!;
|
||||
// should have added the message from /events
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[1].getContent().body).toEqual(msgText);
|
||||
@ -605,7 +657,7 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomTwo);
|
||||
const room = client!.getRoom(roomTwo)!;
|
||||
// should use the display name of the other person.
|
||||
expect(room.name).toEqual(otherDisplayName);
|
||||
});
|
||||
@ -621,11 +673,11 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomTwo);
|
||||
let member = room.getMember(otherUserId);
|
||||
const room = client!.getRoom(roomTwo)!;
|
||||
let member = room.getMember(otherUserId)!;
|
||||
expect(member).toBeTruthy();
|
||||
expect(member.typing).toEqual(true);
|
||||
member = room.getMember(selfUserId);
|
||||
member = room.getMember(selfUserId)!;
|
||||
expect(member).toBeTruthy();
|
||||
expect(member.typing).toEqual(false);
|
||||
});
|
||||
@ -644,7 +696,7 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
const stateAtStart = room.getLiveTimeline().getState(
|
||||
EventTimeline.BACKWARDS,
|
||||
);
|
||||
@ -742,7 +794,7 @@ describe("MatrixClient syncing", () => {
|
||||
awaitSyncEvent(2),
|
||||
]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(false);
|
||||
});
|
||||
|
||||
@ -812,7 +864,7 @@ describe("MatrixClient syncing", () => {
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(false);
|
||||
});
|
||||
|
||||
@ -842,7 +894,7 @@ describe("MatrixClient syncing", () => {
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(false);
|
||||
});
|
||||
|
||||
@ -875,7 +927,7 @@ describe("MatrixClient syncing", () => {
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(false);
|
||||
});
|
||||
|
||||
@ -909,7 +961,7 @@ describe("MatrixClient syncing", () => {
|
||||
]);
|
||||
|
||||
// Get the room after the first sync so the room is created
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
|
||||
let emitCount = 0;
|
||||
room.on(RoomEvent.HistoryImportedWithinTimeline, (markerEvent, room) => {
|
||||
@ -965,7 +1017,7 @@ describe("MatrixClient syncing", () => {
|
||||
awaitSyncEvent(2),
|
||||
]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.getTimelineNeedsRefresh()).toEqual(true);
|
||||
});
|
||||
});
|
||||
@ -1020,7 +1072,7 @@ describe("MatrixClient syncing", () => {
|
||||
]);
|
||||
|
||||
// Get the room after the first sync so the room is created
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room).toBeTruthy();
|
||||
|
||||
let stateEventEmitCount = 0;
|
||||
@ -1094,7 +1146,7 @@ describe("MatrixClient syncing", () => {
|
||||
]);
|
||||
|
||||
// Get the room after the first sync so the room is created
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room).toBeTruthy();
|
||||
|
||||
let stateEventEmitCount = 0;
|
||||
@ -1191,7 +1243,7 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomTwo);
|
||||
const room = client!.getRoom(roomTwo)!;
|
||||
expect(room).toBeTruthy();
|
||||
const tok = room.getLiveTimeline()
|
||||
.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
@ -1234,7 +1286,7 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
const tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
expect(resetCallCount).toEqual(1);
|
||||
@ -1313,7 +1365,7 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomOne);
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
|
||||
type: "m.read",
|
||||
userId: userC,
|
||||
@ -1325,6 +1377,73 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("unread notifications", () => {
|
||||
const THREAD_ID = "$ThisIsARandomEventId";
|
||||
|
||||
const syncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
[roomOne]: {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "world",
|
||||
}),
|
||||
],
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomOne, user: otherUserId,
|
||||
content: {
|
||||
name: "Room name",
|
||||
},
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomOne, user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
it("should sync unread notifications.", () => {
|
||||
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
|
||||
[THREAD_ID]: {
|
||||
"highlight_count": 2,
|
||||
"notification_count": 5,
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend!.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomOne);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("of a room", () => {
|
||||
xit("should sync when a join event (which changes state) for the user" +
|
||||
" arrives down the event stream (e.g. join from another device)", () => {
|
||||
@ -1362,7 +1481,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
const prom = new Promise<void>((resolve) => {
|
||||
httpBackend!.when("GET", "/sync").check((req) => {
|
||||
expect(req.queryParams.filter).toEqual("another_id");
|
||||
expect(req.queryParams!.filter).toEqual("another_id");
|
||||
resolve();
|
||||
}).respond(200, {});
|
||||
});
|
||||
@ -1407,7 +1526,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
return Promise.all([
|
||||
client!.syncLeftRooms().then(() => {
|
||||
const room = client!.getRoom(roomTwo);
|
||||
const room = client!.getRoom(roomTwo)!;
|
||||
const tok = room.getLiveTimeline().getPaginationToken(
|
||||
EventTimeline.BACKWARDS);
|
||||
|
||||
@ -1429,7 +1548,7 @@ describe("MatrixClient syncing", () => {
|
||||
* @returns {Promise} promise which resolves after the sync events have happened
|
||||
*/
|
||||
function awaitSyncEvent(numSyncs?: number) {
|
||||
return utils.syncPromise(client, numSyncs);
|
||||
return utils.syncPromise(client!, numSyncs);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1453,7 +1572,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
const idbHttpBackend = idbTestClient.httpBackend;
|
||||
const idbClient = idbTestClient.client;
|
||||
idbHttpBackend.when("GET", "/versions").respond(200, {});
|
||||
idbHttpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
|
||||
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
|
||||
await idbClient.initCrypto();
|
||||
|
@ -95,26 +95,31 @@ describe("megolm key backups", function() {
|
||||
return;
|
||||
}
|
||||
const Olm = global.Olm;
|
||||
|
||||
let testOlmAccount: Account;
|
||||
let testOlmAccount: Olm.Account;
|
||||
let aliceTestClient: TestClient;
|
||||
|
||||
const setupTestClient = (): [Account, TestClient] => {
|
||||
const aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs",
|
||||
);
|
||||
const testOlmAccount = new Olm.Account();
|
||||
testOlmAccount!.create();
|
||||
|
||||
return [testOlmAccount, aliceTestClient];
|
||||
};
|
||||
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
beforeEach(async function() {
|
||||
aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs",
|
||||
);
|
||||
testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
await aliceTestClient.client.initCrypto();
|
||||
aliceTestClient.client.crypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
|
||||
[testOlmAccount, aliceTestClient] = setupTestClient();
|
||||
await aliceTestClient!.client.initCrypto();
|
||||
aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
return aliceTestClient.stop();
|
||||
return aliceTestClient!.stop();
|
||||
});
|
||||
|
||||
it("Alice checks key backups when receiving a message she can't decrypt", function() {
|
||||
@ -130,22 +135,22 @@ describe("megolm key backups", function() {
|
||||
},
|
||||
};
|
||||
|
||||
return aliceTestClient.start().then(() => {
|
||||
return aliceTestClient!.start().then(() => {
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then(() => {
|
||||
const privkey = decodeRecoveryKey(RECOVERY_KEY);
|
||||
return aliceTestClient.client.crypto.storeSessionBackupPrivateKey(privkey);
|
||||
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
|
||||
}).then(() => {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
aliceTestClient.expectKeyBackupQuery(
|
||||
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
aliceTestClient!.expectKeyBackupQuery(
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
200,
|
||||
CURVE25519_KEY_BACKUP_DATA,
|
||||
);
|
||||
return aliceTestClient.httpBackend.flushAllExpected();
|
||||
return aliceTestClient!.httpBackend.flushAllExpected();
|
||||
}).then(function(): Promise<MatrixEvent> {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
|
||||
if (event.getContent()) {
|
||||
|
@ -207,9 +207,11 @@ describe("megolm", () => {
|
||||
}
|
||||
const Olm = global.Olm;
|
||||
|
||||
let testOlmAccount: Olm.Account;
|
||||
let testSenderKey: string;
|
||||
let aliceTestClient: TestClient;
|
||||
let testOlmAccount = {} as unknown as Olm.Account;
|
||||
let testSenderKey = '';
|
||||
let aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "device2", "access_token2",
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the device keys for testOlmAccount in a format suitable for a
|
||||
@ -283,12 +285,12 @@ describe("megolm", () => {
|
||||
|
||||
it("Alice receives a megolm message", async () => {
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
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";
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
@ -322,7 +324,7 @@ describe("megolm", () => {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
const decryptedEvent = await testUtils.awaitDecryption(event);
|
||||
@ -332,12 +334,12 @@ 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({});
|
||||
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";
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event, but don't send it yet
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
@ -362,7 +364,7 @@ describe("megolm", () => {
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
expect(room.getLiveTimeline().getEvents()[0].getContent().msgtype).toEqual('m.bad.encrypted');
|
||||
|
||||
// now she gets the room_key event
|
||||
@ -392,12 +394,12 @@ describe("megolm", () => {
|
||||
|
||||
it("Alice gets a second room_key message", async () => {
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
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";
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted1 = encryptGroupSessionKey({
|
||||
@ -451,7 +453,7 @@ describe("megolm", () => {
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
await room.decryptCriticalEvents();
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
@ -499,7 +501,7 @@ describe("megolm", () => {
|
||||
let inboundGroupSession: Olm.InboundGroupSession;
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||
).respond(200, function(_path, content) {
|
||||
).respond(200, function(_path, content: any) {
|
||||
const m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
const ct = m.ciphertext[testSenderKey];
|
||||
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||
@ -525,7 +527,7 @@ describe("megolm", () => {
|
||||
return { event_id: '$event_id' };
|
||||
});
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const pendingMsg = room.getPendingEvents()[0];
|
||||
|
||||
await Promise.all([
|
||||
@ -628,7 +630,7 @@ describe("megolm", () => {
|
||||
let megolmSessionId: string;
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||
).respond(200, function(_path, content) {
|
||||
).respond(200, function(_path, content: any) {
|
||||
logger.log('sendToDevice: ', content);
|
||||
const m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
const ct = m.ciphertext[testSenderKey];
|
||||
@ -706,7 +708,7 @@ describe("megolm", () => {
|
||||
// invalidate the device cache for all members in e2e rooms (ie,
|
||||
// herself), and do a key query.
|
||||
aliceTestClient.expectKeyQuery(
|
||||
getTestKeysQueryResponse(aliceTestClient.userId),
|
||||
getTestKeysQueryResponse(aliceTestClient.userId!),
|
||||
);
|
||||
|
||||
await aliceTestClient.httpBackend.flushAllExpected();
|
||||
@ -716,28 +718,30 @@ describe("megolm", () => {
|
||||
await aliceTestClient.client.sendTextMessage(ROOM_ID, 'test');
|
||||
throw new Error("sendTextMessage succeeded on an unknown device");
|
||||
} catch (e) {
|
||||
expect(e.name).toEqual("UnknownDeviceError");
|
||||
expect(Object.keys(e.devices)).toEqual([aliceTestClient.userId]);
|
||||
expect(Object.keys(e.devices[aliceTestClient.userId])).
|
||||
expect((e as any).name).toEqual("UnknownDeviceError");
|
||||
expect(Object.keys((e as any).devices)).toEqual([aliceTestClient.userId!]);
|
||||
expect(Object.keys((e as any)?.devices[aliceTestClient.userId!])).
|
||||
toEqual(['DEVICE_ID']);
|
||||
}
|
||||
|
||||
// mark the device as known, and resend.
|
||||
aliceTestClient.client.setDeviceKnown(aliceTestClient.userId, 'DEVICE_ID');
|
||||
aliceTestClient.client.setDeviceKnown(aliceTestClient.userId!, 'DEVICE_ID');
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/claim').respond(
|
||||
200, function(_path, content) {
|
||||
expect(content.one_time_keys[aliceTestClient.userId].DEVICE_ID)
|
||||
200, function(_path, content: IClaimOTKsResult) {
|
||||
expect(content.one_time_keys[aliceTestClient.userId!].DEVICE_ID)
|
||||
.toEqual("signed_curve25519");
|
||||
return getTestKeysClaimResponse(aliceTestClient.userId);
|
||||
return getTestKeysClaimResponse(aliceTestClient.userId!);
|
||||
});
|
||||
|
||||
let p2pSession: Olm.Session;
|
||||
let inboundGroupSession: Olm.InboundGroupSession;
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||
).respond(200, function(_path, content) {
|
||||
).respond(200, function(_path, content: {
|
||||
messages: { [userId: string]: { [deviceId: string]: Record<string, any> }};
|
||||
}) {
|
||||
logger.log("sendToDevice: ", content);
|
||||
const m = content.messages[aliceTestClient.userId].DEVICE_ID;
|
||||
const m = content.messages[aliceTestClient.userId!].DEVICE_ID;
|
||||
const ct = m.ciphertext[testSenderKey];
|
||||
expect(ct.type).toEqual(0); // pre-key message
|
||||
|
||||
@ -751,7 +755,7 @@ describe("megolm", () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
let decrypted: IEvent;
|
||||
let decrypted: Partial<IEvent> = {};
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, function(_path, content: IContent) {
|
||||
@ -766,7 +770,7 @@ describe("megolm", () => {
|
||||
});
|
||||
|
||||
// Grab the event that we'll need to resend
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const pendingEvents = room.getPendingEvents();
|
||||
expect(pendingEvents.length).toEqual(1);
|
||||
const unsentEvent = pendingEvents[0];
|
||||
@ -781,7 +785,7 @@ describe("megolm", () => {
|
||||
]);
|
||||
|
||||
expect(decrypted.type).toEqual('m.room.message');
|
||||
expect(decrypted.content.body).toEqual('test');
|
||||
expect(decrypted.content?.body).toEqual('test');
|
||||
});
|
||||
|
||||
it('Alice should wait for device list to complete when sending a megolm message', async () => {
|
||||
@ -830,11 +834,11 @@ 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({});
|
||||
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";
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
@ -867,7 +871,7 @@ describe("megolm", () => {
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
await room.decryptCriticalEvents();
|
||||
expect(room.getLiveTimeline().getEvents()[0].getContent().body).toEqual('42');
|
||||
|
||||
@ -883,7 +887,7 @@ describe("megolm", () => {
|
||||
await aliceTestClient.client.importRoomKeys(exported);
|
||||
await aliceTestClient.start();
|
||||
|
||||
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
@ -927,7 +931,7 @@ describe("megolm", () => {
|
||||
...rawEvent,
|
||||
room: ROOM_ID,
|
||||
});
|
||||
await event1.attemptDecryption(testClient.client.crypto, { isRetry: true });
|
||||
await event1.attemptDecryption(testClient.client.crypto!, { isRetry: true });
|
||||
expect(event1.isKeySourceUntrusted()).toBeTruthy();
|
||||
|
||||
const event2 = testUtils.mkEvent({
|
||||
@ -943,26 +947,26 @@ describe("megolm", () => {
|
||||
// @ts-ignore - private
|
||||
event2.senderCurve25519Key = testSenderKey;
|
||||
// @ts-ignore - private
|
||||
testClient.client.crypto.onRoomKeyEvent(event2);
|
||||
testClient.client.crypto!.onRoomKeyEvent(event2);
|
||||
|
||||
const event3 = testUtils.mkEvent({
|
||||
event: true,
|
||||
...rawEvent,
|
||||
room: ROOM_ID,
|
||||
});
|
||||
await event3.attemptDecryption(testClient.client.crypto, { isRetry: true });
|
||||
await event3.attemptDecryption(testClient.client.crypto!, { isRetry: true });
|
||||
expect(event3.isKeySourceUntrusted()).toBeFalsy();
|
||||
testClient.stop();
|
||||
});
|
||||
|
||||
it("Alice can decrypt a message with falsey content", async () => {
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
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";
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
@ -1005,7 +1009,7 @@ describe("megolm", () => {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
const decryptedEvent = await testUtils.awaitDecryption(event);
|
||||
@ -1018,12 +1022,12 @@ 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({});
|
||||
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";
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
@ -1072,10 +1076,10 @@ describe("megolm", () => {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
await event.attemptDecryption(aliceTestClient.client.crypto);
|
||||
await event.attemptDecryption(aliceTestClient.client.crypto!);
|
||||
expect(event.getContent()).toEqual({});
|
||||
const redactionEvent: any = event.getRedactionEvent();
|
||||
expect(redactionEvent.content.reason).toEqual("redaction test");
|
||||
@ -1089,7 +1093,7 @@ describe("megolm", () => {
|
||||
await beccaTestClient.client.initCrypto();
|
||||
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
await beccaTestClient.start();
|
||||
|
||||
const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {});
|
||||
@ -1107,7 +1111,7 @@ describe("megolm", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await beccaTestClient.client.crypto.encryptEvent(event, beccaRoom);
|
||||
await beccaTestClient.client.crypto!.encryptEvent(event, beccaRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
@ -1116,23 +1120,23 @@ describe("megolm", () => {
|
||||
// @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();
|
||||
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(
|
||||
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
beccaTestClient.client.crypto.cryptoStore.getAccount(txn, (pickledAccount: string) => {
|
||||
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string) => {
|
||||
const account = new global.Olm.Account();
|
||||
try {
|
||||
account.unpickle(beccaTestClient.client.crypto.olmDevice.pickleKey, pickledAccount);
|
||||
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount);
|
||||
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
|
||||
} finally {
|
||||
account.free();
|
||||
@ -1142,7 +1146,7 @@ describe("megolm", () => {
|
||||
);
|
||||
|
||||
const content = event.getWireContent();
|
||||
const groupSessionKey = await beccaTestClient.client.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
const groupSessionKey = await beccaTestClient.client.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
ROOM_ID,
|
||||
content.sender_key,
|
||||
content.session_id,
|
||||
@ -1213,7 +1217,7 @@ describe("megolm", () => {
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const roomEvent = room.getLiveTimeline().getEvents()[0];
|
||||
expect(roomEvent.isEncrypted()).toBe(true);
|
||||
const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
|
||||
@ -1246,7 +1250,7 @@ describe("megolm", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await beccaTestClient.client.crypto.encryptEvent(event, beccaRoom);
|
||||
await beccaTestClient.client.crypto!.encryptEvent(event, beccaRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
@ -1255,22 +1259,22 @@ describe("megolm", () => {
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
|
||||
const device = new DeviceInfo(beccaTestClient.client.deviceId);
|
||||
aliceTestClient.client.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
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(
|
||||
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
beccaTestClient.client.crypto.cryptoStore.getAccount(txn, (pickledAccount: string) => {
|
||||
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string) => {
|
||||
const account = new global.Olm.Account();
|
||||
try {
|
||||
account.unpickle(beccaTestClient.client.crypto.olmDevice.pickleKey, pickledAccount);
|
||||
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount);
|
||||
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
|
||||
} finally {
|
||||
account.free();
|
||||
@ -1280,7 +1284,7 @@ describe("megolm", () => {
|
||||
);
|
||||
|
||||
const content = event.getWireContent();
|
||||
const groupSessionKey = await beccaTestClient.client.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
const groupSessionKey = await beccaTestClient.client.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
ROOM_ID,
|
||||
content.sender_key,
|
||||
content.session_id,
|
||||
@ -1352,7 +1356,7 @@ describe("megolm", () => {
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
// Decryption should fail, because Alice hasn't received any keys she can trust
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const roomEvent = room.getLiveTimeline().getEvents()[0];
|
||||
expect(roomEvent.isEncrypted()).toBe(true);
|
||||
const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
|
||||
|
@ -23,18 +23,19 @@ import { TestClient } from "../TestClient";
|
||||
import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator";
|
||||
import {
|
||||
MatrixClient, MatrixEvent, NotificationCountType, JoinRule, MatrixError,
|
||||
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent,
|
||||
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent,
|
||||
} from "../../src";
|
||||
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { IStoredClientOpts } from "../../src/client";
|
||||
import { logger } from "../../src/logger";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
|
||||
describe("SlidingSyncSdk", () => {
|
||||
let client: MatrixClient = null;
|
||||
let httpBackend: MockHttpBackend = null;
|
||||
let sdk: SlidingSyncSdk = null;
|
||||
let mockSlidingSync: SlidingSync = null;
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: MockHttpBackend | undefined;
|
||||
let sdk: SlidingSyncSdk | undefined;
|
||||
let mockSlidingSync: SlidingSync | undefined;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
@ -66,7 +67,7 @@ describe("SlidingSyncSdk", () => {
|
||||
event_id: "$" + eventIdCounter,
|
||||
};
|
||||
};
|
||||
const mkOwnStateEvent = (evType: string, content: object, stateKey?: string): IStateEvent => {
|
||||
const mkOwnStateEvent = (evType: string, content: object, stateKey = ''): IStateEvent => {
|
||||
eventIdCounter++;
|
||||
return {
|
||||
type: evType,
|
||||
@ -103,24 +104,24 @@ describe("SlidingSyncSdk", () => {
|
||||
client = testClient.client;
|
||||
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
|
||||
if (testOpts.withCrypto) {
|
||||
httpBackend.when("GET", "/room_keys/version").respond(404, {});
|
||||
await client.initCrypto();
|
||||
testOpts.crypto = client.crypto;
|
||||
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
|
||||
await client!.initCrypto();
|
||||
testOpts.crypto = client!.crypto;
|
||||
}
|
||||
httpBackend.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
|
||||
httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
|
||||
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts);
|
||||
};
|
||||
|
||||
// tear down client/httpBackend globals
|
||||
const teardownClient = () => {
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
client!.stopClient();
|
||||
return httpBackend!.stop();
|
||||
};
|
||||
|
||||
// find an extension on a SlidingSyncSdk instance
|
||||
const findExtension = (name: string): Extension => {
|
||||
expect(mockSlidingSync.registerExtension).toHaveBeenCalled();
|
||||
const mockFn = mockSlidingSync.registerExtension as jest.Mock;
|
||||
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
|
||||
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
|
||||
// find the extension
|
||||
for (let i = 0; i < mockFn.mock.calls.length; i++) {
|
||||
const calledExtension = mockFn.mock.calls[i][0] as Extension;
|
||||
@ -137,14 +138,14 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
afterAll(teardownClient);
|
||||
it("can sync()", async () => {
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
expect(mockSlidingSync.start).toBeCalled();
|
||||
expect(mockSlidingSync!.start).toBeCalled();
|
||||
});
|
||||
it("can stop()", async () => {
|
||||
sdk.stop();
|
||||
expect(mockSlidingSync.stop).toBeCalled();
|
||||
sdk!.stop();
|
||||
expect(mockSlidingSync!.stop).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -156,8 +157,8 @@ describe("SlidingSyncSdk", () => {
|
||||
|
||||
describe("initial", () => {
|
||||
beforeAll(async () => {
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
});
|
||||
// inject some rooms with different fields set.
|
||||
@ -277,8 +278,8 @@ describe("SlidingSyncSdk", () => {
|
||||
};
|
||||
|
||||
it("can be created with required_state and timeline", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
|
||||
const gotRoom = client.getRoom(roomA);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.name).toEqual(data[roomA].name);
|
||||
@ -287,8 +288,8 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can be created with timeline only", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
|
||||
const gotRoom = client.getRoom(roomB);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
|
||||
const gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.name).toEqual(data[roomB].name);
|
||||
@ -297,8 +298,8 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can be created with a highlight_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
|
||||
const gotRoom = client.getRoom(roomC);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
|
||||
const gotRoom = client!.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
@ -307,8 +308,8 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can be created with a notification_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
|
||||
const gotRoom = client.getRoom(roomD);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
|
||||
const gotRoom = client!.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
@ -317,8 +318,8 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can be created with an invited/joined_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
|
||||
const gotRoom = client.getRoom(roomG);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
|
||||
const gotRoom = client!.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
|
||||
@ -326,8 +327,8 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can be created with invite_state", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
|
||||
const gotRoom = client.getRoom(roomE);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
|
||||
const gotRoom = client!.getRoom(roomE);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getMyMembership()).toEqual("invite");
|
||||
@ -335,8 +336,8 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("uses the 'name' field to caluclate the room name", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
|
||||
const gotRoom = client.getRoom(roomF);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
|
||||
const gotRoom = client!.getRoom(roomF);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
@ -347,12 +348,12 @@ describe("SlidingSyncSdk", () => {
|
||||
describe("updating", () => {
|
||||
it("can update with a new timeline event", async () => {
|
||||
const newEvent = mkOwnEvent(EventType.RoomMessage, { body: "new event A" });
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||
timeline: [newEvent],
|
||||
required_state: [],
|
||||
name: data[roomA].name,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomA);
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
const newTimeline = data[roomA].timeline;
|
||||
@ -361,31 +362,31 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can update with a new required_state event", async () => {
|
||||
let gotRoom = client.getRoom(roomB);
|
||||
let gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
|
||||
required_state: [
|
||||
mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""),
|
||||
],
|
||||
timeline: [],
|
||||
name: data[roomB].name,
|
||||
});
|
||||
gotRoom = client.getRoom(roomB);
|
||||
gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
|
||||
});
|
||||
|
||||
it("can update with a new highlight_count", async () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, {
|
||||
name: data[roomC].name,
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
highlight_count: 1,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomC);
|
||||
const gotRoom = client!.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
@ -394,13 +395,13 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can update with a new notification_count", async () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, {
|
||||
name: data[roomD].name,
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
notification_count: 1,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomD);
|
||||
const gotRoom = client!.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
@ -409,13 +410,13 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can update with a new joined_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, {
|
||||
name: data[roomD].name,
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
joined_count: 1,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomG);
|
||||
const gotRoom = client!.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
|
||||
@ -433,13 +434,13 @@ describe("SlidingSyncSdk", () => {
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "old event C" }),
|
||||
...timeline,
|
||||
];
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||
timeline: oldTimeline,
|
||||
required_state: [],
|
||||
name: data[roomA].name,
|
||||
initial: true, // e.g requested via room subscription
|
||||
});
|
||||
const gotRoom = client.getRoom(roomA);
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
|
||||
@ -458,50 +459,50 @@ describe("SlidingSyncSdk", () => {
|
||||
describe("lifecycle", () => {
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
});
|
||||
const FAILED_SYNC_ERROR_THRESHOLD = 3; // would be nice to export the const in the actual class...
|
||||
|
||||
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
|
||||
mockSlidingSync.emit(
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete,
|
||||
{ pos: "h", lists: [], rooms: {}, extensions: {} }, null,
|
||||
);
|
||||
expect(sdk.getSyncState()).toEqual(SyncState.Syncing);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
|
||||
|
||||
mockSlidingSync.emit(
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
|
||||
);
|
||||
expect(sdk.getSyncState()).toEqual(SyncState.Reconnecting);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Reconnecting);
|
||||
|
||||
for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) {
|
||||
mockSlidingSync.emit(
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
|
||||
);
|
||||
}
|
||||
expect(sdk.getSyncState()).toEqual(SyncState.Error);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
|
||||
});
|
||||
|
||||
it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
|
||||
mockSlidingSync.emit(
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle,
|
||||
SlidingSyncState.Complete,
|
||||
{ pos: "i", lists: [], rooms: {}, extensions: {} },
|
||||
null,
|
||||
);
|
||||
expect(sdk.getSyncState()).toEqual(SyncState.Syncing);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
|
||||
});
|
||||
|
||||
it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => {
|
||||
expect(mockSlidingSync.stop).not.toBeCalled();
|
||||
mockSlidingSync.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({
|
||||
expect(mockSlidingSync!.stop).not.toBeCalled();
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({
|
||||
errcode: "M_UNKNOWN_TOKEN",
|
||||
message: "Oh no your access token is no longer valid",
|
||||
}));
|
||||
expect(sdk.getSyncState()).toEqual(SyncState.Error);
|
||||
expect(mockSlidingSync.stop).toBeCalled();
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
|
||||
expect(mockSlidingSync!.stop).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -517,8 +518,8 @@ describe("SlidingSyncSdk", () => {
|
||||
avatar_url: "mxc://foobar",
|
||||
displayname: "The Invitee",
|
||||
};
|
||||
httpBackend.when("GET", "/profile").respond(200, inviteeProfile);
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
httpBackend!.when("GET", "/profile").respond(200, inviteeProfile);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
initial: true,
|
||||
name: "Room with Invite",
|
||||
required_state: [],
|
||||
@ -529,10 +530,11 @@ describe("SlidingSyncSdk", () => {
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
|
||||
],
|
||||
});
|
||||
await httpBackend.flush("/profile", 1, 1000);
|
||||
const room = client.getRoom(roomId);
|
||||
await httpBackend!.flush("/profile", 1, 1000);
|
||||
await emitPromise(client!, RoomMemberEvent.Name);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
const inviteeMember = room.getMember(invitee);
|
||||
const inviteeMember = room.getMember(invitee)!;
|
||||
expect(inviteeMember).toBeDefined();
|
||||
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
|
||||
expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
|
||||
@ -545,8 +547,8 @@ describe("SlidingSyncSdk", () => {
|
||||
await setupClient({
|
||||
withCrypto: true,
|
||||
});
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
ext = findExtension("e2ee");
|
||||
});
|
||||
@ -554,7 +556,7 @@ describe("SlidingSyncSdk", () => {
|
||||
// needed else we do some async operations in the background which can cause Jest to whine:
|
||||
// "Cannot log after tests are done. Did you forget to wait for something async in your test?"
|
||||
// Attempted to log "Saving device tracking data null"."
|
||||
client.crypto.stop();
|
||||
client!.crypto!.stop();
|
||||
});
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
@ -572,38 +574,38 @@ describe("SlidingSyncSdk", () => {
|
||||
// TODO: more assertions?
|
||||
});
|
||||
it("can update OTK counts", () => {
|
||||
client.crypto.updateOneTimeKeyCount = jest.fn();
|
||||
client!.crypto!.updateOneTimeKeyCount = jest.fn();
|
||||
ext.onResponse({
|
||||
device_one_time_keys_count: {
|
||||
signed_curve25519: 42,
|
||||
},
|
||||
});
|
||||
expect(client.crypto.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
|
||||
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
|
||||
ext.onResponse({
|
||||
device_one_time_keys_count: {
|
||||
not_signed_curve25519: 42,
|
||||
// missing field -> default to 0
|
||||
},
|
||||
});
|
||||
expect(client.crypto.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
|
||||
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
|
||||
});
|
||||
it("can update fallback keys", () => {
|
||||
ext.onResponse({
|
||||
device_unused_fallback_key_types: ["signed_curve25519"],
|
||||
});
|
||||
expect(client.crypto.getNeedsNewFallback()).toEqual(false);
|
||||
expect(client!.crypto!.getNeedsNewFallback()).toEqual(false);
|
||||
ext.onResponse({
|
||||
device_unused_fallback_key_types: ["not_signed_curve25519"],
|
||||
});
|
||||
expect(client.crypto.getNeedsNewFallback()).toEqual(true);
|
||||
expect(client!.crypto!.getNeedsNewFallback()).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe("ExtensionAccountData", () => {
|
||||
let ext: Extension;
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
ext = findExtension("account_data");
|
||||
});
|
||||
@ -618,7 +620,7 @@ describe("SlidingSyncSdk", () => {
|
||||
const globalContent = {
|
||||
info: "here",
|
||||
};
|
||||
let globalData = client.getAccountData(globalType);
|
||||
let globalData = client!.getAccountData(globalType);
|
||||
expect(globalData).toBeUndefined();
|
||||
ext.onResponse({
|
||||
global: [
|
||||
@ -628,13 +630,13 @@ describe("SlidingSyncSdk", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
globalData = client.getAccountData(globalType);
|
||||
globalData = client!.getAccountData(globalType)!;
|
||||
expect(globalData).toBeDefined();
|
||||
expect(globalData.getContent()).toEqual(globalContent);
|
||||
});
|
||||
it("processes rooms account data", async () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with account data",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
@ -660,9 +662,9 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
const event = room.getAccountData(roomType);
|
||||
const event = room.getAccountData(roomType)!;
|
||||
expect(event).toBeDefined();
|
||||
expect(event.getContent()).toEqual(roomContent);
|
||||
});
|
||||
@ -681,9 +683,9 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
const room = client.getRoom(unknownRoomId);
|
||||
const room = client!.getRoom(unknownRoomId);
|
||||
expect(room).toBeNull();
|
||||
expect(client.getAccountData(roomType)).toBeUndefined();
|
||||
expect(client!.getAccountData(roomType)).toBeUndefined();
|
||||
});
|
||||
it("can update push rules via account data", async () => {
|
||||
const roomId = "!foo:bar";
|
||||
@ -703,7 +705,7 @@ describe("SlidingSyncSdk", () => {
|
||||
}],
|
||||
},
|
||||
};
|
||||
let pushRule = client.getRoomPushRule("global", roomId);
|
||||
let pushRule = client!.getRoomPushRule("global", roomId);
|
||||
expect(pushRule).toBeUndefined();
|
||||
ext.onResponse({
|
||||
global: [
|
||||
@ -713,16 +715,16 @@ describe("SlidingSyncSdk", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
pushRule = client.getRoomPushRule("global", roomId);
|
||||
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific][0]);
|
||||
pushRule = client!.getRoomPushRule("global", roomId)!;
|
||||
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific]![0]);
|
||||
});
|
||||
});
|
||||
describe("ExtensionToDevice", () => {
|
||||
let ext: Extension;
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
ext = findExtension("to_device");
|
||||
});
|
||||
@ -753,7 +755,7 @@ describe("SlidingSyncSdk", () => {
|
||||
foo: "bar",
|
||||
};
|
||||
let called = false;
|
||||
client.once(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
client!.once(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
expect(ev.getContent()).toEqual(toDeviceContent);
|
||||
expect(ev.getType()).toEqual(toDeviceType);
|
||||
called = true;
|
||||
@ -771,7 +773,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
it("can cancel key verification requests", async () => {
|
||||
const seen: Record<string, boolean> = {};
|
||||
client.on(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
client!.on(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
const evType = ev.getType();
|
||||
expect(seen[evType]).toBeFalsy();
|
||||
seen[evType] = true;
|
||||
|
@ -22,7 +22,6 @@ import { SlidingSync, SlidingSyncState, ExtensionState, SlidingSyncEvent } from
|
||||
import { TestClient } from "../TestClient";
|
||||
import { logger } from "../../src/logger";
|
||||
import { MatrixClient } from "../../src";
|
||||
import { sleep } from "../../src/utils";
|
||||
|
||||
/**
|
||||
* Tests for sliding sync. These tests are broken down into sub-tests which are reliant upon one another.
|
||||
@ -30,8 +29,8 @@ import { sleep } from "../../src/utils";
|
||||
* Each test will call different functions on SlidingSync which may depend on state from previous tests.
|
||||
*/
|
||||
describe("SlidingSync", () => {
|
||||
let client: MatrixClient = null;
|
||||
let httpBackend: MockHttpBackend = null;
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: MockHttpBackend | undefined;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
const proxyBaseUrl = "http://localhost:8008";
|
||||
@ -46,9 +45,9 @@ describe("SlidingSync", () => {
|
||||
|
||||
// tear down client/httpBackend globals
|
||||
const teardownClient = () => {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
client!.stopClient();
|
||||
return httpBackend!.stop();
|
||||
};
|
||||
|
||||
describe("start/stop", () => {
|
||||
@ -57,14 +56,14 @@ describe("SlidingSync", () => {
|
||||
let slidingSync: SlidingSync;
|
||||
|
||||
it("should start the sync loop upon calling start()", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
const fakeResp = {
|
||||
pos: "a",
|
||||
lists: [],
|
||||
rooms: {},
|
||||
extensions: {},
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).respond(200, fakeResp);
|
||||
httpBackend!.when("POST", syncUrl).respond(200, fakeResp);
|
||||
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
|
||||
expect(state).toEqual(SlidingSyncState.RequestFinished);
|
||||
expect(resp).toEqual(fakeResp);
|
||||
@ -72,13 +71,113 @@ describe("SlidingSync", () => {
|
||||
return true;
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
});
|
||||
|
||||
it("should stop the sync loop upon calling stop()", () => {
|
||||
slidingSync.stop();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
});
|
||||
|
||||
it("should reset the connection on HTTP 400 and send everything again", async () => {
|
||||
// seed the connection with some lists, extensions and subscriptions to verify they are sent again
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
||||
const roomId = "!sub:localhost";
|
||||
const subInfo = {
|
||||
timeline_limit: 42,
|
||||
required_state: [["m.room.create", ""]],
|
||||
};
|
||||
const listInfo = {
|
||||
ranges: [[0, 10]],
|
||||
filters: {
|
||||
is_dm: true,
|
||||
},
|
||||
};
|
||||
const ext = {
|
||||
name: () => "custom_extension",
|
||||
onRequest: (initial) => { return { initial: initial }; },
|
||||
onResponse: (res) => { return {}; },
|
||||
when: () => ExtensionState.PreProcess,
|
||||
};
|
||||
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
slidingSync.modifyRoomSubscriptionInfo(subInfo);
|
||||
slidingSync.setList(0, listInfo);
|
||||
slidingSync.registerExtension(ext);
|
||||
slidingSync.start();
|
||||
|
||||
// expect everything to be sent
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toEqual({
|
||||
[roomId]: subInfo,
|
||||
});
|
||||
expect(body.lists[0]).toEqual(listInfo);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
|
||||
expect(req.queryParams["pos"]).toBeUndefined();
|
||||
txnId = body.txn_id;
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
pos: "11",
|
||||
lists: [{ count: 5 }],
|
||||
extensions: {},
|
||||
txn_id: txnId,
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
// expect nothing but ranges and non-initial extensions to be sent
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
expect(body.lists[0]).toEqual({
|
||||
ranges: [[0, 10]],
|
||||
});
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions["custom_extension"]).toEqual({ initial: false });
|
||||
expect(req.queryParams["pos"]).toEqual("11");
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
pos: "12",
|
||||
lists: [{ count: 5 }],
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
// now we expire the session
|
||||
httpBackend.when("POST", syncUrl).respond(400, function() {
|
||||
logger.debug("sending session expired 400");
|
||||
return {
|
||||
error: "HTTP 400 : session expired",
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
// ...and everything should be sent again
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toEqual({
|
||||
[roomId]: subInfo,
|
||||
});
|
||||
expect(body.lists[0]).toEqual(listInfo);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
|
||||
expect(req.queryParams["pos"]).toBeUndefined();
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
pos: "1",
|
||||
lists: [{ count: 6 }],
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
slidingSync.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@ -103,9 +202,9 @@ describe("SlidingSync", () => {
|
||||
|
||||
it("should be able to subscribe to a room", async () => {
|
||||
// add the subscription
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
|
||||
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("room sub", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
@ -125,7 +224,7 @@ describe("SlidingSync", () => {
|
||||
return true;
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
});
|
||||
|
||||
@ -137,7 +236,7 @@ describe("SlidingSync", () => {
|
||||
["m.room.member", "*"],
|
||||
],
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("adjusted sub", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
@ -158,7 +257,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
slidingSync.modifyRoomSubscriptionInfo(newSubInfo);
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
// need to set what the new subscription info is for subsequent tests
|
||||
roomSubInfo = newSubInfo;
|
||||
@ -179,7 +278,7 @@ describe("SlidingSync", () => {
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("new subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
@ -204,12 +303,12 @@ describe("SlidingSync", () => {
|
||||
const subs = slidingSync.getRoomSubscriptions();
|
||||
subs.add(anotherRoomID);
|
||||
slidingSync.modifyRoomSubscriptions(subs);
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
});
|
||||
|
||||
it("should be able to unsubscribe from a room", async () => {
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("unsub request", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
@ -226,7 +325,7 @@ describe("SlidingSync", () => {
|
||||
// remove the subscription for the first room
|
||||
slidingSync.modifyRoomSubscriptions(new Set([anotherRoomID]));
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
|
||||
slidingSync.stop();
|
||||
@ -273,8 +372,8 @@ describe("SlidingSync", () => {
|
||||
is_dm: true,
|
||||
},
|
||||
};
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client, 1);
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client!, 1);
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("list", body);
|
||||
expect(body.lists).toBeTruthy();
|
||||
@ -301,7 +400,7 @@ describe("SlidingSync", () => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
|
||||
expect(listenerData[roomA]).toEqual(rooms[roomA]);
|
||||
@ -327,7 +426,7 @@ describe("SlidingSync", () => {
|
||||
|
||||
it("should be possible to adjust list ranges", async () => {
|
||||
// modify the list ranges
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("next ranges", body.lists[0].ranges);
|
||||
expect(body.lists).toBeTruthy();
|
||||
@ -351,7 +450,7 @@ describe("SlidingSync", () => {
|
||||
return state === SlidingSyncState.RequestFinished;
|
||||
});
|
||||
slidingSync.setListRanges(0, newRanges);
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
});
|
||||
|
||||
@ -364,7 +463,7 @@ describe("SlidingSync", () => {
|
||||
"is_dm": true,
|
||||
},
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("extra list", body);
|
||||
expect(body.lists).toBeTruthy();
|
||||
@ -403,13 +502,13 @@ describe("SlidingSync", () => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
slidingSync.setList(1, extraListReq);
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
});
|
||||
|
||||
it("should be possible to get list DELETE/INSERTs", async () => {
|
||||
// move C (2) to A (0)
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "e",
|
||||
lists: [{
|
||||
count: 500,
|
||||
@ -440,12 +539,12 @@ describe("SlidingSync", () => {
|
||||
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
|
||||
// move C (0) back to A (2)
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "f",
|
||||
lists: [{
|
||||
count: 500,
|
||||
@ -476,13 +575,13 @@ describe("SlidingSync", () => {
|
||||
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
|
||||
it("should ignore invalid list indexes", async () => {
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "e",
|
||||
lists: [{
|
||||
count: 500,
|
||||
@ -509,13 +608,13 @@ describe("SlidingSync", () => {
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
|
||||
it("should be possible to update a list", async () => {
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "g",
|
||||
lists: [{
|
||||
count: 42,
|
||||
@ -555,7 +654,7 @@ describe("SlidingSync", () => {
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
@ -567,7 +666,7 @@ describe("SlidingSync", () => {
|
||||
1: roomC,
|
||||
};
|
||||
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId);
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "f",
|
||||
// currently the list is [B,C] so we will insert D then immediately delete it
|
||||
lists: [{
|
||||
@ -598,7 +697,7 @@ describe("SlidingSync", () => {
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
@ -608,7 +707,7 @@ describe("SlidingSync", () => {
|
||||
0: roomB,
|
||||
1: roomC,
|
||||
});
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "g",
|
||||
lists: [{
|
||||
count: 499,
|
||||
@ -634,7 +733,7 @@ describe("SlidingSync", () => {
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
@ -643,7 +742,7 @@ describe("SlidingSync", () => {
|
||||
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
});
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "h",
|
||||
lists: [{
|
||||
count: 500,
|
||||
@ -670,11 +769,11 @@ describe("SlidingSync", () => {
|
||||
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "h",
|
||||
lists: [{
|
||||
count: 501,
|
||||
@ -702,7 +801,7 @@ describe("SlidingSync", () => {
|
||||
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
slidingSync.stop();
|
||||
@ -725,11 +824,11 @@ describe("SlidingSync", () => {
|
||||
],
|
||||
};
|
||||
// add the subscription
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
|
||||
// modification before SlidingSync.start()
|
||||
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
@ -752,7 +851,7 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await subscribePromise;
|
||||
});
|
||||
it("should resolve setList during a connection", async () => {
|
||||
@ -761,7 +860,7 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
const promise = slidingSync.setList(0, newList);
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
@ -776,14 +875,14 @@ describe("SlidingSync", () => {
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await promise;
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
it("should resolve setListRanges during a connection", async () => {
|
||||
const promise = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
@ -800,7 +899,7 @@ describe("SlidingSync", () => {
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await promise;
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
@ -809,7 +908,7 @@ describe("SlidingSync", () => {
|
||||
timeline_limit: 99,
|
||||
});
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
@ -825,22 +924,22 @@ describe("SlidingSync", () => {
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await promise;
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
it("should reject earlier pending promises if a later transaction is acknowledged", async () => {
|
||||
// i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected.
|
||||
const gotTxnIds = [];
|
||||
const gotTxnIds: any[] = [];
|
||||
const pushTxn = function(req) {
|
||||
gotTxnIds.push(req.data.txn_id);
|
||||
};
|
||||
const failPromise = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
|
||||
await httpBackend.flushAllExpected();
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
const failPromise2 = slidingSync.setListRanges(0, [[60, 70]]);
|
||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
|
||||
await httpBackend.flushAllExpected();
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
|
||||
// which is a fail.
|
||||
@ -849,7 +948,7 @@ describe("SlidingSync", () => {
|
||||
|
||||
const okPromise = slidingSync.setListRanges(0, [[0, 20]]);
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check((req) => {
|
||||
httpBackend!.when("POST", syncUrl).check((req) => {
|
||||
txnId = req.data.txn_id;
|
||||
}).respond(200, () => {
|
||||
// include the txn_id, earlier requests should now be reject()ed.
|
||||
@ -858,23 +957,23 @@ describe("SlidingSync", () => {
|
||||
txn_id: txnId,
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await okPromise;
|
||||
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
it("should not reject later pending promises if an earlier transaction is acknowledged", async () => {
|
||||
// i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should.
|
||||
const gotTxnIds = [];
|
||||
const gotTxnIds: any[] = [];
|
||||
const pushTxn = function(req) {
|
||||
gotTxnIds.push(req.data.txn_id);
|
||||
gotTxnIds.push(req.data?.txn_id);
|
||||
};
|
||||
const A = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
|
||||
await httpBackend.flushAllExpected();
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
|
||||
await httpBackend!.flushAllExpected();
|
||||
const B = slidingSync.setListRanges(0, [[60, 70]]);
|
||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
|
||||
await httpBackend.flushAllExpected();
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
|
||||
// which is a fail.
|
||||
@ -885,14 +984,14 @@ describe("SlidingSync", () => {
|
||||
C.finally(() => {
|
||||
pendingC = false;
|
||||
});
|
||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, () => {
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, () => {
|
||||
// include the txn_id for B, so C's promise is outstanding
|
||||
return {
|
||||
pos: "C",
|
||||
txn_id: gotTxnIds[1],
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
// A is rejected, see above
|
||||
expect(B).resolves.toEqual(gotTxnIds[1]); // B is resolved
|
||||
expect(pendingC).toBe(true); // C is pending still
|
||||
@ -904,7 +1003,7 @@ describe("SlidingSync", () => {
|
||||
pending = false;
|
||||
});
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
@ -921,7 +1020,7 @@ describe("SlidingSync", () => {
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(txnId).toBeDefined();
|
||||
expect(pending).toBe(true);
|
||||
slidingSync.stop();
|
||||
@ -963,10 +1062,10 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
|
||||
it("should be able to register an extension", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
slidingSync.registerExtension(extPre);
|
||||
|
||||
const callbackOrder = [];
|
||||
const callbackOrder: string[] = [];
|
||||
let extensionOnResponseCalled = false;
|
||||
onPreExtensionRequest = () => {
|
||||
return extReq;
|
||||
@ -977,7 +1076,7 @@ describe("SlidingSync", () => {
|
||||
expect(resp).toEqual(extResp);
|
||||
};
|
||||
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("ext req", body);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
@ -998,7 +1097,7 @@ describe("SlidingSync", () => {
|
||||
}
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
expect(extensionOnResponseCalled).toBe(true);
|
||||
expect(callbackOrder).toEqual(["onPreExtensionResponse", "Lifecycle"]);
|
||||
@ -1012,7 +1111,7 @@ describe("SlidingSync", () => {
|
||||
onPreExtensionResponse = (resp) => {
|
||||
responseCalled = true;
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("ext req nothing", body);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
@ -1030,7 +1129,7 @@ describe("SlidingSync", () => {
|
||||
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
expect(responseCalled).toBe(false);
|
||||
});
|
||||
@ -1041,13 +1140,13 @@ describe("SlidingSync", () => {
|
||||
return extReq;
|
||||
};
|
||||
let responseCalled = false;
|
||||
const callbackOrder = [];
|
||||
const callbackOrder: string[] = [];
|
||||
onPostExtensionResponse = (resp) => {
|
||||
expect(resp).toEqual(extResp);
|
||||
responseCalled = true;
|
||||
callbackOrder.push("onPostExtensionResponse");
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("ext req after start", body);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
@ -1071,7 +1170,7 @@ describe("SlidingSync", () => {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
expect(responseCalled).toBe(true);
|
||||
expect(callbackOrder).toEqual(["Lifecycle", "onPostExtensionResponse"]);
|
||||
@ -1079,16 +1178,27 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("is not possible to register the same extension name twice", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
slidingSync.registerExtension(extPre);
|
||||
expect(() => { slidingSync.registerExtension(extPre); }).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function timeout(delayMs: number, reason: string): Promise<never> {
|
||||
await sleep(delayMs);
|
||||
throw new Error(`timeout: ${delayMs}ms - ${reason}`);
|
||||
function timeout(delayMs: number, reason: string): { promise: Promise<never>, cancel: () => void } {
|
||||
let timeoutId;
|
||||
return {
|
||||
promise: new Promise((resolve, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(new Error(`timeout: ${delayMs}ms - ${reason}`));
|
||||
}, delayMs);
|
||||
}),
|
||||
cancel: () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1106,19 +1216,22 @@ function listenUntil<T>(
|
||||
callback: (...args: any[]) => T,
|
||||
timeoutMs = 500,
|
||||
): Promise<T> {
|
||||
const trace = new Error().stack.split(`\n`)[2];
|
||||
const trace = new Error().stack?.split(`\n`)[2];
|
||||
const t = timeout(timeoutMs, "timed out waiting for event " + eventName + " " + trace);
|
||||
return Promise.race([new Promise<T>((resolve, reject) => {
|
||||
const wrapper = (...args) => {
|
||||
try {
|
||||
const data = callback(...args);
|
||||
if (data) {
|
||||
emitter.off(eventName, wrapper);
|
||||
t.cancel();
|
||||
resolve(data);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
t.cancel();
|
||||
}
|
||||
};
|
||||
emitter.on(eventName, wrapper);
|
||||
}), timeout(timeoutMs, "timed out waiting for event " + eventName + " " + trace)]);
|
||||
}), t.promise]);
|
||||
}
|
||||
|
19
spec/setupTests.ts
Normal file
19
spec/setupTests.ts
Normal file
@ -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.
|
||||
*/
|
||||
|
||||
import DOMException from "domexception";
|
||||
|
||||
global.DOMException = DOMException;
|
94
spec/test-utils/client.ts
Normal file
94
spec/test-utils/client.ts
Normal file
@ -0,0 +1,94 @@
|
||||
/*
|
||||
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 { MethodKeysOf, mocked, MockedObject } from "jest-mock";
|
||||
|
||||
import { ClientEventHandlerMap, EmittedEvents, MatrixClient } from "../../src/client";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { User } from "../../src/models/user";
|
||||
|
||||
/**
|
||||
* Mock client with real event emitter
|
||||
* useful for testing code that listens
|
||||
* to MatrixClient events
|
||||
*/
|
||||
export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents, ClientEventHandlerMap> {
|
||||
constructor(mockProperties: Partial<Record<MethodKeysOf<MatrixClient>, unknown>> = {}) {
|
||||
super();
|
||||
Object.assign(this, mockProperties);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* - make a mock client
|
||||
* - cast the type to mocked(MatrixClient)
|
||||
* - spy on MatrixClientPeg.get to return the mock
|
||||
* eg
|
||||
* ```
|
||||
* const mockClient = getMockClientWithEventEmitter({
|
||||
getUserId: jest.fn().mockReturnValue(aliceId),
|
||||
});
|
||||
* ```
|
||||
*/
|
||||
export const getMockClientWithEventEmitter = (
|
||||
mockProperties: Partial<Record<MethodKeysOf<MatrixClient>, unknown>>,
|
||||
): MockedObject<MatrixClient> => {
|
||||
const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
|
||||
return mock;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns basic mocked client methods related to the current user
|
||||
* ```
|
||||
* const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser('@mytestuser:domain'),
|
||||
});
|
||||
* ```
|
||||
*/
|
||||
export const mockClientMethodsUser = (userId = '@alice:domain') => ({
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getUser: jest.fn().mockReturnValue(new User(userId)),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'),
|
||||
credentials: { userId },
|
||||
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
|
||||
getAccessToken: jest.fn(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns basic mocked client methods related to rendering events
|
||||
* ```
|
||||
* const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser('@mytestuser:domain'),
|
||||
});
|
||||
* ```
|
||||
*/
|
||||
export const mockClientMethodsEvents = () => ({
|
||||
decryptEventIfNeeded: jest.fn(),
|
||||
getPushActionsForEvent: jest.fn(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns basic mocked client methods related to server support
|
||||
*/
|
||||
export const mockClientMethodsServer = (): Partial<Record<MethodKeysOf<MatrixClient>, unknown>> => ({
|
||||
doesServerSupportSeparateAddAndBind: jest.fn(),
|
||||
getIdentityServerUrl: jest.fn(),
|
||||
getHomeserverUrl: jest.fn(),
|
||||
getCapabilities: jest.fn().mockReturnValue({}),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
||||
});
|
||||
|
@ -74,6 +74,7 @@ interface IEventOpts {
|
||||
sender?: string;
|
||||
skey?: string;
|
||||
content: IContent;
|
||||
prev_content?: IContent;
|
||||
user?: string;
|
||||
unsigned?: IUnsigned;
|
||||
redacts?: string;
|
||||
@ -103,6 +104,7 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
|
||||
room_id: opts.room,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: opts.content,
|
||||
prev_content: opts.prev_content,
|
||||
unsigned: opts.unsigned || {},
|
||||
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
|
||||
txn_id: "~" + Math.random(),
|
||||
|
@ -17,13 +17,12 @@ limitations under the License.
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { request } from "../../src/matrix";
|
||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
|
||||
describe("AutoDiscovery", function() {
|
||||
const getHttpBackend = (): MockHttpBackend => {
|
||||
const httpBackend = new MockHttpBackend();
|
||||
request(httpBackend.requestFn);
|
||||
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch);
|
||||
return httpBackend;
|
||||
};
|
||||
|
||||
@ -176,8 +175,7 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
|
||||
"m.homeserver (empty string)", function() {
|
||||
it("should return FAIL_PROMPT when .well-known does not have a base_url for m.homeserver (empty string)", () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@ -205,8 +203,7 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
|
||||
"m.homeserver (no property)", function() {
|
||||
it("should return FAIL_PROMPT when .well-known does not have a base_url for m.homeserver (no property)", () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {},
|
||||
@ -232,8 +229,7 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (disallowed scheme)", function() {
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for m.homeserver (disallowed scheme)", () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@ -679,4 +675,76 @@ describe("AutoDiscovery", function() {
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT for connection errors", () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, undefined);
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT for fetch errors", () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, new Error("CORS or something"));
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT for invalid JSON", () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "<html>", true);
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -30,7 +30,7 @@ import { Crypto } from "../../../src/crypto";
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { BackupManager } from "../../../src/crypto/backup";
|
||||
import { StubStore } from "../../../src/store/stub";
|
||||
import { IAbortablePromise, MatrixScheduler } from '../../../src';
|
||||
import { MatrixScheduler } from '../../../src';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@ -131,7 +131,7 @@ function makeTestClient(cryptoStore) {
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
accessToken: "my.access.token",
|
||||
request: jest.fn(), // NOP
|
||||
fetchFn: jest.fn(), // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: "@alice:bar",
|
||||
@ -197,7 +197,7 @@ describe("MegolmBackup", function() {
|
||||
// to tick the clock between the first try and the retry.
|
||||
const realSetTimeout = global.setTimeout;
|
||||
jest.spyOn(global, 'setTimeout').mockImplementation(function(f, n) {
|
||||
return realSetTimeout(f, n/100);
|
||||
return realSetTimeout(f!, n/100);
|
||||
});
|
||||
});
|
||||
|
||||
@ -298,25 +298,25 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
client.http.authedRequest = function<T>(
|
||||
method, path, queryParams, data, opts,
|
||||
): Promise<T> {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(1);
|
||||
if (numCalls >= 2) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
return Promise.resolve({} as T);
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe('1');
|
||||
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
resolve();
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
return Promise.resolve({} as T);
|
||||
};
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
@ -381,25 +381,25 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
client.http.authedRequest = function<T>(
|
||||
method, path, queryParams, data, opts,
|
||||
): Promise<T> {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(1);
|
||||
if (numCalls >= 2) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
return Promise.resolve({} as T);
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe('1');
|
||||
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
resolve();
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
return Promise.resolve({} as T);
|
||||
};
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
@ -439,7 +439,7 @@ describe("MegolmBackup", function() {
|
||||
new Promise<void>((resolve, reject) => {
|
||||
let backupInfo;
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
@ -449,23 +449,23 @@ describe("MegolmBackup", function() {
|
||||
try {
|
||||
// make sure auth_data is signed by the master key
|
||||
olmlib.pkVerify(
|
||||
data.auth_data, client.getCrossSigningId(), "@alice:bar",
|
||||
(data as Record<string, any>).auth_data, client.getCrossSigningId(), "@alice:bar",
|
||||
);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
return Promise.resolve({});
|
||||
}
|
||||
backupInfo = data;
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
return Promise.resolve({});
|
||||
} else if (numCalls === 2) {
|
||||
expect(method).toBe("GET");
|
||||
expect(path).toBe("/room_keys/version");
|
||||
resolve();
|
||||
return Promise.resolve(backupInfo) as IAbortablePromise<any>;
|
||||
return Promise.resolve(backupInfo);
|
||||
} else {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many times"));
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
return Promise.resolve({});
|
||||
}
|
||||
};
|
||||
}),
|
||||
@ -495,7 +495,7 @@ describe("MegolmBackup", function() {
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
accessToken: "my.access.token",
|
||||
request: jest.fn(), // NOP
|
||||
fetchFn: jest.fn(), // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: "@alice:bar",
|
||||
@ -542,30 +542,30 @@ describe("MegolmBackup", function() {
|
||||
let numCalls = 0;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
client.http.authedRequest = function<T>(
|
||||
method, path, queryParams, data, opts,
|
||||
): Promise<T> {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
if (numCalls >= 3) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
return Promise.resolve({} as T);
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe('1');
|
||||
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
if (numCalls > 1) {
|
||||
resolve();
|
||||
return Promise.resolve({}) as IAbortablePromise<any>;
|
||||
return Promise.resolve({} as T);
|
||||
} else {
|
||||
return Promise.reject(
|
||||
new Error("this is an expected failure"),
|
||||
) as IAbortablePromise<any>;
|
||||
);
|
||||
}
|
||||
};
|
||||
return client.crypto.backupManager.backupGroupSession(
|
||||
|
@ -141,7 +141,7 @@ describe("Cross Signing", function() {
|
||||
};
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
alice.setAccountData = async () => ({});
|
||||
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T> => ({} as T);
|
||||
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T | null> => ({} as T);
|
||||
const authUploadDeviceSigningKeys = async func => await func({});
|
||||
|
||||
// Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass
|
||||
|
@ -109,16 +109,13 @@ describe("Secrets", function() {
|
||||
const secretStorage = alice.crypto.secretStorage;
|
||||
|
||||
jest.spyOn(alice, 'setAccountData').mockImplementation(
|
||||
async function(eventType, contents, callback) {
|
||||
async function(eventType, contents) {
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
}),
|
||||
]);
|
||||
if (callback) {
|
||||
callback(undefined, undefined);
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
@ -192,7 +189,7 @@ describe("Secrets", function() {
|
||||
},
|
||||
},
|
||||
);
|
||||
alice.setAccountData = async function(eventType, contents, callback) {
|
||||
alice.setAccountData = async function(eventType, contents) {
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: eventType,
|
||||
@ -332,7 +329,7 @@ describe("Secrets", function() {
|
||||
);
|
||||
bob.uploadDeviceSigningKeys = async () => ({});
|
||||
bob.uploadKeySignatures = jest.fn().mockResolvedValue(undefined);
|
||||
bob.setAccountData = async function(eventType, contents, callback) {
|
||||
bob.setAccountData = async function(eventType, contents) {
|
||||
const event = new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
|
@ -29,7 +29,7 @@ describe("eventMapperFor", function() {
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
request: function() {} as any, // NOP
|
||||
fetchFn: function() {} as any, // NOP
|
||||
store: {
|
||||
getRoom(roomId: string): Room | null {
|
||||
return rooms.find(r => r.roomId === roomId);
|
||||
|
62
spec/unit/feature.spec.ts
Normal file
62
spec/unit/feature.spec.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
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 { buildFeatureSupportMap, Feature, ServerSupport } from "../../src/feature";
|
||||
|
||||
describe("Feature detection", () => {
|
||||
it("checks the matrix version", async () => {
|
||||
const support = await buildFeatureSupportMap({
|
||||
versions: ["v1.3"],
|
||||
unstable_features: {},
|
||||
});
|
||||
|
||||
expect(support.get(Feature.Thread)).toBe(ServerSupport.Stable);
|
||||
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported);
|
||||
});
|
||||
|
||||
it("checks the matrix msc number", async () => {
|
||||
const support = await buildFeatureSupportMap({
|
||||
versions: ["v1.2"],
|
||||
unstable_features: {
|
||||
"org.matrix.msc3771": true,
|
||||
"org.matrix.msc3773": true,
|
||||
},
|
||||
});
|
||||
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unstable);
|
||||
});
|
||||
|
||||
it("requires two MSCs to pass", async () => {
|
||||
const support = await buildFeatureSupportMap({
|
||||
versions: ["v1.2"],
|
||||
unstable_features: {
|
||||
"org.matrix.msc3771": false,
|
||||
"org.matrix.msc3773": true,
|
||||
},
|
||||
});
|
||||
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported);
|
||||
});
|
||||
|
||||
it("requires two MSCs OR matrix versions to pass", async () => {
|
||||
const support = await buildFeatureSupportMap({
|
||||
versions: ["v1.4"],
|
||||
unstable_features: {
|
||||
"org.matrix.msc3771": false,
|
||||
"org.matrix.msc3773": true,
|
||||
},
|
||||
});
|
||||
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Stable);
|
||||
});
|
||||
});
|
@ -1,3 +1,4 @@
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
|
||||
import { Filter, IFilterDefinition } from "../../src/filter";
|
||||
|
||||
describe("Filter", function() {
|
||||
@ -43,4 +44,17 @@ describe("Filter", function() {
|
||||
expect(filter.getDefinition()).toEqual(definition);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setUnreadThreadNotifications", function() {
|
||||
it("setUnreadThreadNotifications", function() {
|
||||
filter.setUnreadThreadNotifications(true);
|
||||
expect(filter.getDefinition()).toEqual({
|
||||
room: {
|
||||
timeline: {
|
||||
[UNREAD_THREAD_NOTIFICATIONS.name]: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
11
spec/unit/http-api/__snapshots__/index.spec.ts.snap
Normal file
11
spec/unit/http-api/__snapshots__/index.spec.ts.snap
Normal file
@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`] = `
|
||||
{
|
||||
"base": "http://baseUrl",
|
||||
"params": {
|
||||
"access_token": "token",
|
||||
},
|
||||
"path": "/_matrix/media/r0/upload",
|
||||
}
|
||||
`;
|
223
spec/unit/http-api/fetch.spec.ts
Normal file
223
spec/unit/http-api/fetch.spec.ts
Normal file
@ -0,0 +1,223 @@
|
||||
/*
|
||||
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 { FetchHttpApi } from "../../../src/http-api/fetch";
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import { ClientPrefix, HttpApiEvent, HttpApiEventHandlerMap, IdentityPrefix, IHttpOpts, Method } from "../../../src";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
|
||||
describe("FetchHttpApi", () => {
|
||||
const baseUrl = "http://baseUrl";
|
||||
const idBaseUrl = "http://idBaseUrl";
|
||||
const prefix = ClientPrefix.V3;
|
||||
|
||||
it("should support aborting multiple times", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
|
||||
api.request(Method.Get, "/foo");
|
||||
api.request(Method.Get, "/baz");
|
||||
expect(fetchFn.mock.calls[0][0].href.endsWith("/foo")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeFalsy();
|
||||
expect(fetchFn.mock.calls[1][0].href.endsWith("/baz")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeFalsy();
|
||||
|
||||
api.abort();
|
||||
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeTruthy();
|
||||
|
||||
api.request(Method.Get, "/bar");
|
||||
expect(fetchFn.mock.calls[2][0].href.endsWith("/bar")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeFalsy();
|
||||
|
||||
api.abort();
|
||||
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should fall back to global fetch if fetchFn not provided", () => {
|
||||
global.fetch = jest.fn();
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
api.fetch("test");
|
||||
expect(global.fetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should update identity server base url", () => {
|
||||
const api = new FetchHttpApi<IHttpOpts>(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
expect(api.opts.idBaseUrl).toBeUndefined();
|
||||
api.setIdBaseUrl("https://id.foo.bar");
|
||||
expect(api.opts.idBaseUrl).toBe("https://id.foo.bar");
|
||||
});
|
||||
|
||||
describe("idServerRequest", () => {
|
||||
it("should throw if no idBaseUrl", () => {
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
expect(() => api.idServerRequest(Method.Get, "/test", {}, IdentityPrefix.V2))
|
||||
.toThrow("No identity server base URL set");
|
||||
});
|
||||
|
||||
it("should send params as query string for GET requests", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
|
||||
api.idServerRequest(Method.Get, "/test", { foo: "bar", via: ["a", "b"] }, IdentityPrefix.V2);
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).toBe("bar");
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.getAll("via")).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("should send params as body for non-GET requests", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
|
||||
const params = { foo: "bar", via: ["a", "b"] };
|
||||
api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2);
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).not.toBe("bar");
|
||||
expect(JSON.parse(fetchFn.mock.calls[0][1].body)).toStrictEqual(params);
|
||||
});
|
||||
|
||||
it("should add Authorization header if token provided", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
|
||||
api.idServerRequest(Method.Post, "/test", {}, IdentityPrefix.V2, "token");
|
||||
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBe("Bearer token");
|
||||
});
|
||||
});
|
||||
|
||||
it("should return the Response object if onlyData=false", async () => {
|
||||
const res = { ok: true };
|
||||
const fetchFn = jest.fn().mockResolvedValue(res);
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: false });
|
||||
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(res);
|
||||
});
|
||||
|
||||
it("should return text if json=false", async () => {
|
||||
const text = "418 I'm a teapot";
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue(text) });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
await expect(api.requestOtherUrl(Method.Get, "http://url", undefined, {
|
||||
json: false,
|
||||
})).resolves.toBe(text);
|
||||
});
|
||||
|
||||
it("should send token via query params if useAuthorizationHeader=false", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: false,
|
||||
});
|
||||
api.authedRequest(Method.Get, "/path");
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("token");
|
||||
});
|
||||
|
||||
it("should send token via headers by default", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
});
|
||||
api.authedRequest(Method.Get, "/path");
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
|
||||
});
|
||||
|
||||
it("should not send a token if not calling `authedRequest`", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
});
|
||||
api.request(Method.Get, "/path");
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should ensure no token is leaked out via query params if sending via headers", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: true,
|
||||
});
|
||||
api.authedRequest(Method.Get, "/path", { access_token: "123" });
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
|
||||
});
|
||||
|
||||
it("should not override manually specified access token via query params", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: false,
|
||||
});
|
||||
api.authedRequest(Method.Get, "/path", { access_token: "RealToken" });
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("RealToken");
|
||||
});
|
||||
|
||||
it("should not override manually specified access token via header", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: true,
|
||||
});
|
||||
api.authedRequest(Method.Get, "/path", undefined, undefined, {
|
||||
headers: { Authorization: "Bearer RealToken" },
|
||||
});
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer RealToken");
|
||||
});
|
||||
|
||||
it("should not override Accept header", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
api.authedRequest(Method.Get, "/path", undefined, undefined, {
|
||||
headers: { Accept: "text/html" },
|
||||
});
|
||||
expect(fetchFn.mock.calls[0][1].headers["Accept"]).toBe("text/html");
|
||||
});
|
||||
|
||||
it("should emit NoConsent when given errcode=M_CONTENT_NOT_GIVEN", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
},
|
||||
text: jest.fn().mockResolvedValue(JSON.stringify({
|
||||
errcode: "M_CONSENT_NOT_GIVEN",
|
||||
error: "Ye shall ask for consent",
|
||||
})),
|
||||
});
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
|
||||
|
||||
await Promise.all([
|
||||
emitPromise(emitter, HttpApiEvent.NoConsent),
|
||||
expect(api.authedRequest(Method.Get, "/path")).rejects.toThrow("Ye shall ask for consent"),
|
||||
]);
|
||||
});
|
||||
});
|
228
spec/unit/http-api/index.spec.ts
Normal file
228
spec/unit/http-api/index.spec.ts
Normal file
@ -0,0 +1,228 @@
|
||||
/*
|
||||
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 DOMException from "domexception";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { ClientPrefix, MatrixHttpApi, Method, UploadResponse } from "../../../src";
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
|
||||
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("MatrixHttpApi", () => {
|
||||
const baseUrl = "http://baseUrl";
|
||||
const prefix = ClientPrefix.V3;
|
||||
|
||||
let xhr: Partial<Writeable<XMLHttpRequest>>;
|
||||
let upload: Promise<UploadResponse>;
|
||||
|
||||
const DONE = 0;
|
||||
|
||||
global.DOMException = DOMException;
|
||||
|
||||
beforeEach(() => {
|
||||
xhr = {
|
||||
upload: {} as XMLHttpRequestUpload,
|
||||
open: jest.fn(),
|
||||
send: jest.fn(),
|
||||
abort: jest.fn(),
|
||||
setRequestHeader: jest.fn(),
|
||||
onreadystatechange: undefined,
|
||||
getResponseHeader: jest.fn(),
|
||||
};
|
||||
// We stub out XHR here as it is not available in JSDOM
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest.DONE = DONE;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
upload?.catch(() => {});
|
||||
// Abort any remaining requests
|
||||
xhr.readyState = DONE;
|
||||
xhr.status = 0;
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
});
|
||||
|
||||
it("should fall back to `fetch` where xhr is unavailable", () => {
|
||||
global.XMLHttpRequest = undefined;
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(fetchFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should prefer xhr where available", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(fetchFn).not.toHaveBeenCalled();
|
||||
expect(xhr.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should send access token in query params if header disabled", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: false,
|
||||
});
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(xhr.open)
|
||||
.toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?access_token=token");
|
||||
expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
|
||||
});
|
||||
|
||||
it("should send access token in header by default", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
accessToken: "token",
|
||||
});
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
|
||||
expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
|
||||
});
|
||||
|
||||
it("should include filename by default", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File, { name: "name" });
|
||||
expect(xhr.open)
|
||||
.toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?filename=name");
|
||||
});
|
||||
|
||||
it("should allow not sending the filename", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File, { name: "name", includeFilename: false });
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
|
||||
});
|
||||
|
||||
it("should abort xhr when the upload is aborted", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
api.cancelUpload(upload);
|
||||
expect(xhr.abort).toHaveBeenCalled();
|
||||
return expect(upload).rejects.toThrow("Aborted");
|
||||
});
|
||||
|
||||
it("should timeout if no progress in 30s", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
jest.advanceTimersByTime(25000);
|
||||
// @ts-ignore
|
||||
xhr.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
|
||||
jest.advanceTimersByTime(25000);
|
||||
expect(xhr.abort).not.toHaveBeenCalled();
|
||||
jest.advanceTimersByTime(5000);
|
||||
expect(xhr.abort).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call progressHandler", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const progressHandler = jest.fn();
|
||||
upload = api.uploadContent({} as File, { progressHandler });
|
||||
const progressEvent = new Event("progress") as ProgressEvent;
|
||||
Object.assign(progressEvent, { loaded: 1, total: 100 });
|
||||
// @ts-ignore
|
||||
xhr.upload.onprogress(progressEvent);
|
||||
expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 });
|
||||
|
||||
Object.assign(progressEvent, { loaded: 95, total: 100 });
|
||||
// @ts-ignore
|
||||
xhr.upload.onprogress(progressEvent);
|
||||
expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 });
|
||||
});
|
||||
|
||||
it("should error when no response body", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.responseText = "";
|
||||
xhr.status = 200;
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
|
||||
return expect(upload).rejects.toThrow("No response body.");
|
||||
});
|
||||
|
||||
it("should error on a 400-code", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
|
||||
xhr.status = 404;
|
||||
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
|
||||
return expect(upload).rejects.toThrow("Not found");
|
||||
});
|
||||
|
||||
it("should return response on successful upload", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.responseText = '{"content_uri": "mxc://server/foobar"}';
|
||||
xhr.status = 200;
|
||||
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
|
||||
return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" });
|
||||
});
|
||||
|
||||
it("should abort xhr when calling `cancelUpload`", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(api.cancelUpload(upload)).toBeTruthy();
|
||||
expect(xhr.abort).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return false when `cancelUpload` is called but unsuccessful", async () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.status = 500;
|
||||
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
await upload.catch(() => {});
|
||||
|
||||
expect(api.cancelUpload(upload)).toBeFalsy();
|
||||
expect(xhr.abort).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return active uploads in `getCurrentUploads`", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeTruthy();
|
||||
api.cancelUpload(upload);
|
||||
expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return expected object from `getContentUri`", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, accessToken: "token" });
|
||||
expect(api.getContentUri()).toMatchSnapshot();
|
||||
});
|
||||
});
|
183
spec/unit/http-api/utils.spec.ts
Normal file
183
spec/unit/http-api/utils.spec.ts
Normal file
@ -0,0 +1,183 @@
|
||||
/*
|
||||
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 { mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
anySignal,
|
||||
ConnectionError,
|
||||
MatrixError,
|
||||
parseErrorResponse,
|
||||
retryNetworkOperation,
|
||||
timeoutSignal,
|
||||
} from "../../../src";
|
||||
import { sleep } from "../../../src/utils";
|
||||
|
||||
jest.mock("../../../src/utils");
|
||||
|
||||
describe("timeoutSignal", () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
it("should fire abort signal after specified timeout", () => {
|
||||
const signal = timeoutSignal(3000);
|
||||
const onabort = jest.fn();
|
||||
signal.onabort = onabort;
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(3000);
|
||||
expect(signal.aborted).toBeTruthy();
|
||||
expect(onabort).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("anySignal", () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
it("should fire when any signal fires", () => {
|
||||
const { signal } = anySignal([
|
||||
timeoutSignal(3000),
|
||||
timeoutSignal(2000),
|
||||
]);
|
||||
|
||||
const onabort = jest.fn();
|
||||
signal.onabort = onabort;
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(2000);
|
||||
expect(signal.aborted).toBeTruthy();
|
||||
expect(onabort).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should cleanup when instructed", () => {
|
||||
const { signal, cleanup } = anySignal([
|
||||
timeoutSignal(3000),
|
||||
timeoutSignal(2000),
|
||||
]);
|
||||
|
||||
const onabort = jest.fn();
|
||||
signal.onabort = onabort;
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
|
||||
cleanup();
|
||||
jest.advanceTimersByTime(2000);
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should abort immediately if passed an aborted signal", () => {
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
const { signal } = anySignal([controller.signal]);
|
||||
expect(signal.aborted).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseErrorResponse", () => {
|
||||
it("should resolve Matrix Errors from XHR", () => {
|
||||
expect(parseErrorResponse({
|
||||
getResponseHeader(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
status: 500,
|
||||
} as XMLHttpRequest, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
|
||||
errcode: "TEST",
|
||||
}, 500));
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from fetch", () => {
|
||||
expect(parseErrorResponse({
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
},
|
||||
status: 500,
|
||||
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
|
||||
errcode: "TEST",
|
||||
}, 500));
|
||||
});
|
||||
|
||||
it("should handle no type gracefully", () => {
|
||||
expect(parseErrorResponse({
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
status: 500,
|
||||
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new Error("Server returned 500 error"));
|
||||
});
|
||||
|
||||
it("should handle invalid type gracefully", () => {
|
||||
expect(parseErrorResponse({
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? " " : null;
|
||||
},
|
||||
},
|
||||
status: 500,
|
||||
} as Response, '{"errcode": "TEST"}'))
|
||||
.toStrictEqual(new Error("Error parsing Content-Type ' ': TypeError: invalid media type"));
|
||||
});
|
||||
|
||||
it("should handle plaintext errors", () => {
|
||||
expect(parseErrorResponse({
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? "text/plain" : null;
|
||||
},
|
||||
},
|
||||
status: 418,
|
||||
} as Response, "I'm a teapot")).toStrictEqual(new Error("Server returned 418 error: I'm a teapot"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("retryNetworkOperation", () => {
|
||||
it("should retry given number of times with exponential sleeps", async () => {
|
||||
const err = new ConnectionError("test");
|
||||
const fn = jest.fn().mockRejectedValue(err);
|
||||
mocked(sleep).mockResolvedValue(undefined);
|
||||
await expect(retryNetworkOperation(4, fn)).rejects.toThrow(err);
|
||||
expect(fn).toHaveBeenCalledTimes(4);
|
||||
expect(mocked(sleep)).toHaveBeenCalledTimes(3);
|
||||
expect(mocked(sleep).mock.calls[0][0]).toBe(2000);
|
||||
expect(mocked(sleep).mock.calls[1][0]).toBe(4000);
|
||||
expect(mocked(sleep).mock.calls[2][0]).toBe(8000);
|
||||
});
|
||||
|
||||
it("should bail out on errors other than ConnectionError", async () => {
|
||||
const err = new TypeError("invalid JSON");
|
||||
const fn = jest.fn().mockRejectedValue(err);
|
||||
mocked(sleep).mockResolvedValue(undefined);
|
||||
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should return newest ConnectionError when giving up", async () => {
|
||||
const err1 = new ConnectionError("test1");
|
||||
const err2 = new ConnectionError("test2");
|
||||
const err3 = new ConnectionError("test3");
|
||||
const errors = [err1, err2, err3];
|
||||
const fn = jest.fn().mockImplementation(() => {
|
||||
throw errors.shift();
|
||||
});
|
||||
mocked(sleep).mockResolvedValue(undefined);
|
||||
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err3);
|
||||
});
|
||||
});
|
@ -103,7 +103,7 @@ describe("MatrixClient", function() {
|
||||
];
|
||||
let acceptKeepalives: boolean;
|
||||
let pendingLookup = null;
|
||||
function httpReq(cb, method, path, qp, data, prefix) {
|
||||
function httpReq(method, path, qp, data, prefix) {
|
||||
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
|
||||
return Promise.resolve({
|
||||
unstable_features: {
|
||||
@ -132,7 +132,6 @@ describe("MatrixClient", function() {
|
||||
method: method,
|
||||
path: path,
|
||||
};
|
||||
pendingLookup.promise.abort = () => {}; // to make it a valid IAbortablePromise
|
||||
return pendingLookup.promise;
|
||||
}
|
||||
if (next.path === path && next.method === method) {
|
||||
@ -178,7 +177,7 @@ describe("MatrixClient", function() {
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
accessToken: "my.access.token",
|
||||
request: function() {} as any, // NOP
|
||||
fetchFn: function() {} as any, // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: userId,
|
||||
@ -1153,8 +1152,7 @@ describe("MatrixClient", function() {
|
||||
|
||||
// event type combined
|
||||
const expectedEventType = M_BEACON_INFO.name;
|
||||
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
|
||||
expect(callback).toBeFalsy();
|
||||
const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
|
||||
expect(method).toBe('PUT');
|
||||
expect(path).toEqual(
|
||||
`/rooms/${encodeURIComponent(roomId)}/state/` +
|
||||
@ -1168,7 +1166,7 @@ describe("MatrixClient", function() {
|
||||
await client.unstable_setLiveBeacon(roomId, content);
|
||||
|
||||
// event type combined
|
||||
const [, , path, , requestContent] = client.http.authedRequest.mock.calls[0];
|
||||
const [, path, , requestContent] = client.http.authedRequest.mock.calls[0];
|
||||
expect(path).toEqual(
|
||||
`/rooms/${encodeURIComponent(roomId)}/state/` +
|
||||
`${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`,
|
||||
@ -1229,7 +1227,7 @@ describe("MatrixClient", function() {
|
||||
it("is called with plain text topic and callback and sends state event", async () => {
|
||||
const sendStateEvent = createSendStateEventMock("pizza");
|
||||
client.sendStateEvent = sendStateEvent;
|
||||
await client.setRoomTopic(roomId, "pizza", () => {});
|
||||
await client.setRoomTopic(roomId, "pizza");
|
||||
expect(sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@ -1244,15 +1242,9 @@ describe("MatrixClient", function() {
|
||||
describe("setPassword", () => {
|
||||
const auth = { session: 'abcdef', type: 'foo' };
|
||||
const newPassword = 'newpassword';
|
||||
const callback = () => {};
|
||||
|
||||
const passwordTest = (expectedRequestContent: any, expectedCallback?: Function) => {
|
||||
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
|
||||
if (expectedCallback) {
|
||||
expect(callback).toBe(expectedCallback);
|
||||
} else {
|
||||
expect(callback).toBeFalsy();
|
||||
}
|
||||
const passwordTest = (expectedRequestContent: any) => {
|
||||
const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
|
||||
expect(method).toBe('POST');
|
||||
expect(path).toEqual('/account/password');
|
||||
expect(queryParams).toBeFalsy();
|
||||
@ -1269,8 +1261,8 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("no logout_devices specified + callback", async () => {
|
||||
await client.setPassword(auth, newPassword, callback);
|
||||
passwordTest({ auth, new_password: newPassword }, callback);
|
||||
await client.setPassword(auth, newPassword);
|
||||
passwordTest({ auth, new_password: newPassword });
|
||||
});
|
||||
|
||||
it("overload logoutDevices=true", async () => {
|
||||
@ -1279,8 +1271,8 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("overload logoutDevices=true + callback", async () => {
|
||||
await client.setPassword(auth, newPassword, true, callback);
|
||||
passwordTest({ auth, new_password: newPassword, logout_devices: true }, callback);
|
||||
await client.setPassword(auth, newPassword, true);
|
||||
passwordTest({ auth, new_password: newPassword, logout_devices: true });
|
||||
});
|
||||
|
||||
it("overload logoutDevices=false", async () => {
|
||||
@ -1289,8 +1281,8 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("overload logoutDevices=false + callback", async () => {
|
||||
await client.setPassword(auth, newPassword, false, callback);
|
||||
passwordTest({ auth, new_password: newPassword, logout_devices: false }, callback);
|
||||
await client.setPassword(auth, newPassword, false);
|
||||
passwordTest({ auth, new_password: newPassword, logout_devices: false });
|
||||
});
|
||||
});
|
||||
|
||||
@ -1305,8 +1297,7 @@ describe("MatrixClient", function() {
|
||||
const result = await client.getLocalAliases(roomId);
|
||||
|
||||
// Current version of the endpoint we support is v3
|
||||
const [callback, method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0];
|
||||
expect(callback).toBeFalsy();
|
||||
const [method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0];
|
||||
expect(data).toBeFalsy();
|
||||
expect(method).toBe('GET');
|
||||
expect(path).toEqual(`/rooms/${encodeURIComponent(roomId)}/aliases`);
|
||||
|
@ -890,9 +890,8 @@ describe("MSC3089TreeSpace", () => {
|
||||
expect(contents.length).toEqual(fileContents.length);
|
||||
expect(opts).toMatchObject({
|
||||
includeFilename: false,
|
||||
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
|
||||
});
|
||||
return Promise.resolve(mxc);
|
||||
return Promise.resolve({ content_uri: mxc });
|
||||
});
|
||||
client.uploadContent = uploadFn;
|
||||
|
||||
@ -950,9 +949,8 @@ describe("MSC3089TreeSpace", () => {
|
||||
expect(contents.length).toEqual(fileContents.length);
|
||||
expect(opts).toMatchObject({
|
||||
includeFilename: false,
|
||||
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
|
||||
});
|
||||
return Promise.resolve(mxc);
|
||||
return Promise.resolve({ content_uri: mxc });
|
||||
});
|
||||
client.uploadContent = uploadFn;
|
||||
|
||||
|
134
spec/unit/notifications.spec.ts
Normal file
134
spec/unit/notifications.spec.ts
Normal file
@ -0,0 +1,134 @@
|
||||
/*
|
||||
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 { Feature, ServerSupport } from "../../src/feature";
|
||||
import {
|
||||
EventType,
|
||||
fixNotificationCountOnDecryption,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MsgType,
|
||||
NotificationCountType,
|
||||
RelationType,
|
||||
Room,
|
||||
RoomEvent,
|
||||
} from "../../src/matrix";
|
||||
import { IActionsObject } from "../../src/pushprocessor";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client";
|
||||
import { mkEvent, mock } from "../test-utils/test-utils";
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
let room: Room;
|
||||
let event: MatrixEvent;
|
||||
let threadEvent: MatrixEvent;
|
||||
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
let THREAD_ID;
|
||||
|
||||
function mkPushAction(notify, highlight): IActionsObject {
|
||||
return {
|
||||
notify,
|
||||
tweaks: {
|
||||
highlight,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("fixNotificationCountOnDecryption", () => {
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
getPushActionsForEvent: jest.fn().mockReturnValue(mkPushAction(true, true)),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
mockClient.reEmitter = mock(ReEmitter, 'ReEmitter');
|
||||
mockClient.canSupport = new Map();
|
||||
Object.keys(Feature).forEach(feature => {
|
||||
mockClient.canSupport.set(feature as Feature, ServerSupport.Stable);
|
||||
});
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "");
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
||||
|
||||
event = mkEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Hello world!",
|
||||
},
|
||||
event: true,
|
||||
}, mockClient);
|
||||
|
||||
THREAD_ID = event.getId();
|
||||
threadEvent = mkEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Thread,
|
||||
event_id: THREAD_ID,
|
||||
},
|
||||
"msgtype": MsgType.Text,
|
||||
"body": "Thread reply",
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
room.createThread(THREAD_ID, event, [threadEvent], false);
|
||||
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
|
||||
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||
threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||
});
|
||||
|
||||
it("changes the room count to highlight on decryption", () => {
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, event);
|
||||
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
it("changes the thread count to highlight on decryption", () => {
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, threadEvent);
|
||||
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
it("emits events", () => {
|
||||
const cb = jest.fn();
|
||||
room.on(RoomEvent.UnreadNotifications, cb);
|
||||
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||
expect(cb).toHaveBeenLastCalledWith({ highlight: 0, total: 1 });
|
||||
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 5);
|
||||
expect(cb).toHaveBeenLastCalledWith({ highlight: 5, total: 1 });
|
||||
|
||||
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 5);
|
||||
expect(cb).toHaveBeenLastCalledWith({ highlight: 5 }, "$123");
|
||||
});
|
||||
});
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
|
||||
import { IHttpOpts, MatrixClient, PUSHER_ENABLED } from "../../src/matrix";
|
||||
import { MatrixClient, PUSHER_ENABLED } from "../../src/matrix";
|
||||
import { mkPusher } from '../test-utils/test-utils';
|
||||
|
||||
const realSetTimeout = setTimeout;
|
||||
@ -35,7 +35,7 @@ describe("Pushers", () => {
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
request: httpBackend.requestFn as unknown as IHttpOpts["request"],
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { PushProcessor } from "../../src/pushprocessor";
|
||||
import { EventType, MatrixClient, MatrixEvent } from "../../src";
|
||||
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
|
||||
import { EventType, IContent, MatrixClient, MatrixEvent } from "../../src";
|
||||
|
||||
describe('NotificationService', function() {
|
||||
const testUserId = "@ali:matrix.org";
|
||||
@ -336,4 +336,102 @@ describe('NotificationService', function() {
|
||||
enabled: true,
|
||||
}, testEvent)).toBe(true);
|
||||
});
|
||||
|
||||
describe("performCustomEventHandling()", () => {
|
||||
const getActionsForEvent = (prevContent: IContent, content: IContent): IActionsObject => {
|
||||
testEvent = utils.mkEvent({
|
||||
type: "org.matrix.msc3401.call",
|
||||
room: testRoomId,
|
||||
user: "@alice:foo",
|
||||
skey: "state_key",
|
||||
event: true,
|
||||
content: content,
|
||||
prev_content: prevContent,
|
||||
});
|
||||
|
||||
return pushProcessor.actionsForEvent(testEvent);
|
||||
};
|
||||
|
||||
const assertDoesNotify = (actions: IActionsObject): void => {
|
||||
expect(actions.notify).toBeTruthy();
|
||||
expect(actions.tweaks.sound).toBeTruthy();
|
||||
expect(actions.tweaks.highlight).toBeFalsy();
|
||||
};
|
||||
|
||||
const assertDoesNotNotify = (actions: IActionsObject): void => {
|
||||
expect(actions.notify).toBeFalsy();
|
||||
expect(actions.tweaks.sound).toBeFalsy();
|
||||
expect(actions.tweaks.highlight).toBeFalsy();
|
||||
};
|
||||
|
||||
it.each(
|
||||
["m.ring", "m.prompt"],
|
||||
)("should notify when new group call event appears with %s intent", (intent: string) => {
|
||||
assertDoesNotify(getActionsForEvent({}, {
|
||||
"m.intent": intent,
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
}));
|
||||
});
|
||||
|
||||
it("should notify when a call is un-terminated", () => {
|
||||
assertDoesNotify(getActionsForEvent({
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
"m.terminated": "All users left",
|
||||
}, {
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
}));
|
||||
});
|
||||
|
||||
it("should not notify when call is terminated", () => {
|
||||
assertDoesNotNotify(getActionsForEvent({
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
}, {
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
"m.terminated": "All users left",
|
||||
}));
|
||||
});
|
||||
|
||||
it("should ignore with m.room intent", () => {
|
||||
assertDoesNotNotify(getActionsForEvent({}, {
|
||||
"m.intent": "m.room",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
}));
|
||||
});
|
||||
|
||||
describe("ignoring non-relevant state changes", () => {
|
||||
it("should ignore intent changes", () => {
|
||||
assertDoesNotNotify(getActionsForEvent({
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
}, {
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.video",
|
||||
"m.name": "Call",
|
||||
}));
|
||||
});
|
||||
|
||||
it("should ignore name changes", () => {
|
||||
assertDoesNotNotify(getActionsForEvent({
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "Call",
|
||||
}, {
|
||||
"m.intent": "m.ring",
|
||||
"m.type": "m.voice",
|
||||
"m.name": "New call",
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
import { indexedDB as fakeIndexedDB } from 'fake-indexeddb';
|
||||
|
||||
import { IHttpOpts, IndexedDBStore, MatrixEvent, MemoryStore, Room } from "../../src";
|
||||
import { IndexedDBStore, MatrixEvent, MemoryStore, Room } from "../../src";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { ToDeviceBatch } from '../../src/models/ToDeviceMessage';
|
||||
import { logger } from '../../src/logger';
|
||||
@ -77,7 +77,7 @@ describe.each([
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
request: httpBackend.requestFn as IHttpOpts["request"],
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
store,
|
||||
});
|
||||
});
|
||||
|
@ -18,7 +18,6 @@ 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';
|
||||
@ -87,7 +86,7 @@ describe("Read receipt", () => {
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
request: httpBackend.requestFn as unknown as IHttpOpts["request"],
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
});
|
||||
client.isGuest = () => false;
|
||||
});
|
||||
@ -146,5 +145,23 @@ describe("Read receipt", () => {
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("sends a valid room read receipt even when body omitted", async () => {
|
||||
httpBackend.when(
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId(),
|
||||
}),
|
||||
).check((request) => {
|
||||
expect(request.data).toEqual({});
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, false);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, undefined);
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -32,13 +32,13 @@ import {
|
||||
RoomEvent,
|
||||
} from "../../src";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { NotificationCountType, 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 { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread";
|
||||
import { WrappedReceipt } from "../../src/models/read-receipt";
|
||||
|
||||
describe("Room", function() {
|
||||
@ -2408,7 +2408,7 @@ describe("Room", function() {
|
||||
});
|
||||
|
||||
it("should aggregate relations in thread event timeline set", () => {
|
||||
Thread.setServerSideSupport(true, true);
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
const threadRoot = mkMessage();
|
||||
const rootReaction = mkReaction(threadRoot);
|
||||
const threadResponse = mkThreadResponse(threadRoot);
|
||||
@ -2562,4 +2562,78 @@ describe("Room", function() {
|
||||
expect(client.roomNameGenerator).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("thread notifications", () => {
|
||||
let room;
|
||||
|
||||
beforeEach(() => {
|
||||
const client = new TestClient(userA).client;
|
||||
room = new Room(roomId, client, userA);
|
||||
});
|
||||
|
||||
it("defaults to undefined", () => {
|
||||
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(0);
|
||||
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("lets you set values", () => {
|
||||
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 1);
|
||||
|
||||
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(1);
|
||||
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(0);
|
||||
|
||||
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 10);
|
||||
|
||||
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(1);
|
||||
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(10);
|
||||
});
|
||||
|
||||
it("lets you reset threads notifications", () => {
|
||||
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666);
|
||||
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 123);
|
||||
|
||||
expect(room.getThreadsAggregateNotificationType()).toBe(NotificationCountType.Highlight);
|
||||
|
||||
room.resetThreadUnreadNotificationCount();
|
||||
|
||||
expect(room.getThreadsAggregateNotificationType()).toBe(null);
|
||||
|
||||
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(0);
|
||||
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("sets the room threads notification type", () => {
|
||||
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666);
|
||||
expect(room.getThreadsAggregateNotificationType()).toBe(NotificationCountType.Total);
|
||||
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 123);
|
||||
expect(room.getThreadsAggregateNotificationType()).toBe(NotificationCountType.Highlight);
|
||||
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 333);
|
||||
expect(room.getThreadsAggregateNotificationType()).toBe(NotificationCountType.Highlight);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasThreadUnreadNotification", () => {
|
||||
it('has no notifications by default', () => {
|
||||
expect(room.hasThreadUnreadNotification()).toBe(false);
|
||||
});
|
||||
|
||||
it('main timeline notification does not affect this', () => {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 1);
|
||||
expect(room.hasThreadUnreadNotification()).toBe(false);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||
expect(room.hasThreadUnreadNotification()).toBe(false);
|
||||
|
||||
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 1);
|
||||
expect(room.hasThreadUnreadNotification()).toBe(true);
|
||||
});
|
||||
|
||||
it('lets you reset', () => {
|
||||
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 1);
|
||||
expect(room.hasThreadUnreadNotification()).toBe(true);
|
||||
|
||||
room.resetThreadUnreadNotificationCount();
|
||||
|
||||
expect(room.hasThreadUnreadNotification()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -30,6 +30,12 @@ const RES_WITH_AGE = {
|
||||
account_data: { events: [] },
|
||||
ephemeral: { events: [] },
|
||||
unread_notifications: {},
|
||||
unread_thread_notifications: {
|
||||
"$143273582443PhrSn:example.org": {
|
||||
highlight_count: 0,
|
||||
notification_count: 1,
|
||||
},
|
||||
},
|
||||
timeline: {
|
||||
events: [
|
||||
Object.freeze({
|
||||
@ -439,6 +445,13 @@ describe("SyncAccumulator", function() {
|
||||
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should retrieve unread thread notifications", () => {
|
||||
sa.accumulate(RES_WITH_AGE);
|
||||
const output = sa.getJSON();
|
||||
expect(output.roomsData.join["!foo:bar"]
|
||||
.unread_thread_notifications["$143273582443PhrSn:example.org"]).not.toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -26,9 +26,7 @@ describe("utils", function() {
|
||||
foo: "bar",
|
||||
baz: "beer@",
|
||||
};
|
||||
expect(utils.encodeParams(params)).toEqual(
|
||||
"foo=bar&baz=beer%40",
|
||||
);
|
||||
expect(utils.encodeParams(params).toString()).toEqual("foo=bar&baz=beer%40");
|
||||
});
|
||||
|
||||
it("should handle boolean and numeric values", function() {
|
||||
@ -37,7 +35,24 @@ describe("utils", function() {
|
||||
number: 12345,
|
||||
boolean: false,
|
||||
};
|
||||
expect(utils.encodeParams(params)).toEqual("string=foobar&number=12345&boolean=false");
|
||||
expect(utils.encodeParams(params).toString()).toEqual("string=foobar&number=12345&boolean=false");
|
||||
});
|
||||
|
||||
it("should handle string arrays", () => {
|
||||
const params = {
|
||||
via: ["one", "two", "three"],
|
||||
};
|
||||
expect(utils.encodeParams(params).toString()).toEqual("via=one&via=two&via=three");
|
||||
});
|
||||
});
|
||||
|
||||
describe("decodeParams", () => {
|
||||
it("should be able to decode multiple values into an array", () => {
|
||||
const params = "foo=bar&via=a&via=b&via=c";
|
||||
expect(utils.decodeParams(params)).toEqual({
|
||||
foo: "bar",
|
||||
via: ["a", "b", "c"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -44,7 +44,7 @@ import {
|
||||
MockRTCRtpSender,
|
||||
} from "../../test-utils/webrtc";
|
||||
import { CallFeed } from "../../../src/webrtc/callFeed";
|
||||
import { Callback, EventType, IContent, ISendEventResponse, MatrixEvent, Room } from "../../../src";
|
||||
import { EventType, IContent, ISendEventResponse, MatrixEvent, Room } from "../../../src";
|
||||
|
||||
const FAKE_ROOM_ID = "!foo:bar";
|
||||
const CALL_LIFETIME = 60000;
|
||||
@ -106,7 +106,7 @@ describe('Call', function() {
|
||||
let prevDocument: Document;
|
||||
let prevWindow: Window & typeof globalThis;
|
||||
// We retain a reference to this in the correct Mock type
|
||||
let mockSendEvent: jest.Mock<Promise<ISendEventResponse>, [string, string, IContent, string, Callback<any>]>;
|
||||
let mockSendEvent: jest.Mock<Promise<ISendEventResponse>, [string, string, IContent, string]>;
|
||||
|
||||
const errorListener = () => {};
|
||||
|
||||
|
@ -1123,10 +1123,7 @@ describe('Group Call', function() {
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new MatrixClient({
|
||||
baseUrl: "base_url",
|
||||
request: (() => {}) as any, // NOP
|
||||
});
|
||||
client = new MatrixClient({ baseUrl: "base_url" });
|
||||
|
||||
jest.spyOn(client, "sendStateEvent").mockResolvedValue({} as any);
|
||||
});
|
||||
|
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
@ -30,6 +30,8 @@ declare global {
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
localStorage: Storage;
|
||||
// marker variable used to detect both the browser & node entrypoints being used at once
|
||||
__js_sdk_entrypoint: unknown;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,11 +40,6 @@ export enum Preset {
|
||||
|
||||
export type ResizeMethod = "crop" | "scale";
|
||||
|
||||
// TODO move to http-api after TSification
|
||||
export interface IAbortablePromise<T> extends Promise<T> {
|
||||
abort(): void;
|
||||
}
|
||||
|
||||
export type IdServerUnbindResult = "no-support" | "success";
|
||||
|
||||
// Knock and private are reserved keywords which are not yet implemented.
|
||||
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Callback } from "../client";
|
||||
import { IContent, IEvent } from "../models/event";
|
||||
import { Preset, Visibility } from "./partials";
|
||||
import { IEventWithRoomId, SearchKey } from "./search";
|
||||
@ -22,7 +21,7 @@ import { IRoomEventFilter } from "../filter";
|
||||
import { Direction } from "../models/event-timeline";
|
||||
import { PushRuleAction } from "./PushRules";
|
||||
import { IRoomEvent } from "../sync-accumulator";
|
||||
import { RoomType } from "./event";
|
||||
import { EventType, RoomType } from "./event";
|
||||
|
||||
// allow camelcase as these are things that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
@ -98,7 +97,18 @@ export interface ICreateRoomOpts {
|
||||
name?: string;
|
||||
topic?: string;
|
||||
preset?: Preset;
|
||||
power_level_content_override?: object;
|
||||
power_level_content_override?: {
|
||||
ban?: number;
|
||||
events?: Record<EventType | string, number>;
|
||||
events_default?: number;
|
||||
invite?: number;
|
||||
kick?: number;
|
||||
notifications?: Record<string, number>;
|
||||
redact?: number;
|
||||
state_default?: number;
|
||||
users?: Record<string, number>;
|
||||
users_default?: number;
|
||||
};
|
||||
creation_content?: object;
|
||||
initial_state?: ICreateRoomStateEvent[];
|
||||
invite?: string[];
|
||||
@ -119,16 +129,6 @@ export interface IRoomDirectoryOptions {
|
||||
third_party_instance_id?: string;
|
||||
}
|
||||
|
||||
export interface IUploadOpts {
|
||||
name?: string;
|
||||
includeFilename?: boolean;
|
||||
type?: string;
|
||||
rawResponse?: boolean;
|
||||
onlyContentUri?: boolean;
|
||||
callback?: Callback;
|
||||
progressHandler?: (state: {loaded: number, total: number}) => void;
|
||||
}
|
||||
|
||||
export interface IAddThreePidOnlyBody {
|
||||
auth?: {
|
||||
type: string;
|
||||
@ -149,7 +149,7 @@ export interface IRelationsRequestOpts {
|
||||
from?: string;
|
||||
to?: string;
|
||||
limit?: number;
|
||||
direction?: Direction;
|
||||
dir?: Direction;
|
||||
}
|
||||
|
||||
export interface IRelationsResponse {
|
||||
|
26
src/@types/sync.ts
Normal file
26
src/@types/sync.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
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 { ServerControlledNamespacedValue } from "../NamespacedValue";
|
||||
|
||||
/**
|
||||
* https://github.com/matrix-org/matrix-doc/pull/3773
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export const UNREAD_THREAD_NOTIFICATIONS = new ServerControlledNamespacedValue(
|
||||
"unread_thread_notifications",
|
||||
"org.matrix.msc3773.unread_thread_notifications");
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IAuthData } from "..";
|
||||
import { IAuthData } from "../interactive-auth";
|
||||
|
||||
/**
|
||||
* Helper type to represent HTTP request body for a UIA enabled endpoint
|
||||
|
@ -28,7 +28,7 @@ const MAX_BATCH_SIZE = 20;
|
||||
export class ToDeviceMessageQueue {
|
||||
private sending = false;
|
||||
private running = true;
|
||||
private retryTimeout: number = null;
|
||||
private retryTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private retryAttempts = 0;
|
||||
|
||||
constructor(private client: MatrixClient) {
|
||||
@ -68,7 +68,7 @@ export class ToDeviceMessageQueue {
|
||||
logger.debug("Attempting to send queued to-device messages");
|
||||
|
||||
this.sending = true;
|
||||
let headBatch;
|
||||
let headBatch: IndexedToDeviceBatch;
|
||||
try {
|
||||
while (this.running) {
|
||||
headBatch = await this.client.store.getOldestToDeviceBatch();
|
||||
@ -92,7 +92,7 @@ export class ToDeviceMessageQueue {
|
||||
// bored and giving up for now
|
||||
if (Math.floor(e.httpStatus / 100) === 4) {
|
||||
logger.error("Fatal error when sending to-device message - dropping to-device batch!", e);
|
||||
await this.client.store.removeToDeviceBatch(headBatch.id);
|
||||
await this.client.store.removeToDeviceBatch(headBatch!.id);
|
||||
} else {
|
||||
logger.info("Automatic retry limit reached for to-device messages.");
|
||||
}
|
||||
|
@ -17,10 +17,9 @@ limitations under the License.
|
||||
|
||||
/** @module auto-discovery */
|
||||
|
||||
import { ServerResponse } from "http";
|
||||
|
||||
import { IClientWellKnown, IWellKnownConfig } from "./client";
|
||||
import { logger } from './logger';
|
||||
import { MatrixError, Method, timeoutSignal } from "./http-api";
|
||||
|
||||
// Dev note: Auto discovery is part of the spec.
|
||||
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
||||
@ -395,6 +394,19 @@ export class AutoDiscovery {
|
||||
}
|
||||
}
|
||||
|
||||
private static fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
|
||||
if (this.fetchFn) {
|
||||
return this.fetchFn(resource, options);
|
||||
}
|
||||
return global.fetch(resource, options);
|
||||
}
|
||||
|
||||
private static fetchFn?: typeof global.fetch;
|
||||
|
||||
public static setFetchFn(fetchFn: typeof global.fetch): void {
|
||||
AutoDiscovery.fetchFn = fetchFn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a JSON object from a given URL, as expected by all .well-known
|
||||
* related lookups. If the server gives a 404 then the `action` will be
|
||||
@ -411,45 +423,55 @@ export class AutoDiscovery {
|
||||
* @return {Promise<object>} Resolves to the returned state.
|
||||
* @private
|
||||
*/
|
||||
private static fetchWellKnownObject(uri: string): Promise<IWellKnownConfig> {
|
||||
return new Promise((resolve) => {
|
||||
// eslint-disable-next-line
|
||||
const request = require("./matrix").getRequest();
|
||||
if (!request) throw new Error("No request library available");
|
||||
request(
|
||||
{ method: "GET", uri, timeout: 5000 },
|
||||
(error: Error, response: ServerResponse, body: string) => {
|
||||
if (error || response?.statusCode < 200 || response?.statusCode >= 300) {
|
||||
const result = { error, raw: {} };
|
||||
return resolve(response?.statusCode === 404
|
||||
? {
|
||||
...result,
|
||||
private static async fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await AutoDiscovery.fetch(url, {
|
||||
method: Method.Get,
|
||||
signal: timeoutSignal(5000),
|
||||
});
|
||||
|
||||
if (response.status === 404) {
|
||||
return {
|
||||
raw: {},
|
||||
action: AutoDiscoveryAction.IGNORE,
|
||||
reason: AutoDiscovery.ERROR_MISSING_WELLKNOWN,
|
||||
} : {
|
||||
...result,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
raw: {},
|
||||
action: AutoDiscoveryAction.FAIL_PROMPT,
|
||||
reason: error?.message || "General failure",
|
||||
});
|
||||
reason: "General failure",
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as Error | string | undefined;
|
||||
return {
|
||||
error,
|
||||
raw: {},
|
||||
action: AutoDiscoveryAction.FAIL_PROMPT,
|
||||
reason: (<Error>error)?.message || "General failure",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return resolve({
|
||||
raw: JSON.parse(body),
|
||||
return {
|
||||
raw: await response.json(),
|
||||
action: AutoDiscoveryAction.SUCCESS,
|
||||
});
|
||||
};
|
||||
} catch (err) {
|
||||
return resolve({
|
||||
error: err,
|
||||
const error = err as Error | string | undefined;
|
||||
return {
|
||||
error,
|
||||
raw: {},
|
||||
action: AutoDiscoveryAction.FAIL_PROMPT,
|
||||
reason: err?.name === "SyntaxError"
|
||||
reason: (error as MatrixError)?.name === "SyntaxError"
|
||||
? AutoDiscovery.ERROR_INVALID_JSON
|
||||
: AutoDiscovery.ERROR_INVALID,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,25 +14,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import request from "browser-request";
|
||||
import queryString from "qs";
|
||||
|
||||
import * as matrixcs from "./matrix";
|
||||
|
||||
if (matrixcs.getRequest()) {
|
||||
if (global.__js_sdk_entrypoint) {
|
||||
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
|
||||
}
|
||||
|
||||
matrixcs.request(function(opts, fn) {
|
||||
// We manually fix the query string for browser-request because
|
||||
// it doesn't correctly handle cases like ?via=one&via=two. Instead
|
||||
// we mimic `request`'s query string interface to make it all work
|
||||
// as expected.
|
||||
// browser-request will happily take the constructed string as the
|
||||
// query string without trying to modify it further.
|
||||
opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions);
|
||||
return request(opts, fn);
|
||||
});
|
||||
global.__js_sdk_entrypoint = true;
|
||||
|
||||
// just *accessing* indexedDB throws an exception in firefox with
|
||||
// indexeddb disabled.
|
||||
|
1368
src/client.ts
1368
src/client.ts
File diff suppressed because it is too large
Load Diff
@ -18,7 +18,7 @@ import { logger } from "../logger";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { Method, PREFIX_UNSTABLE } from "../http-api";
|
||||
import { Method, ClientPrefix } from "../http-api";
|
||||
import { Crypto, ICryptoCallbacks, IBootstrapCrossSigningOpts } from "./index";
|
||||
import {
|
||||
ClientEvent,
|
||||
@ -240,19 +240,19 @@ export class EncryptionSetupOperation {
|
||||
// Sign the backup with the cross signing key so the key backup can
|
||||
// be trusted via cross-signing.
|
||||
await baseApis.http.authedRequest(
|
||||
undefined, Method.Put, "/room_keys/version/" + this.keyBackupInfo.version,
|
||||
Method.Put, "/room_keys/version/" + this.keyBackupInfo.version,
|
||||
undefined, {
|
||||
algorithm: this.keyBackupInfo.algorithm,
|
||||
auth_data: this.keyBackupInfo.auth_data,
|
||||
},
|
||||
{ prefix: PREFIX_UNSTABLE },
|
||||
{ prefix: ClientPrefix.V3 },
|
||||
);
|
||||
} else {
|
||||
// add new key backup
|
||||
await baseApis.http.authedRequest(
|
||||
undefined, Method.Post, "/room_keys/version",
|
||||
Method.Post, "/room_keys/version",
|
||||
undefined, this.keyBackupInfo,
|
||||
{ prefix: PREFIX_UNSTABLE },
|
||||
{ prefix: ClientPrefix.V3 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -550,7 +550,7 @@ export class SecretStorage {
|
||||
|
||||
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
|
||||
olmlib.OLM_ALGORITHM,
|
||||
content.sender_key,
|
||||
event.getSenderKey() || "",
|
||||
);
|
||||
if (senderKeyUser !== event.getSender()) {
|
||||
logger.error("sending device does not belong to the user it claims to be from");
|
||||
|
@ -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";
|
||||
|
@ -1438,9 +1438,10 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
memberEvent?.getPrevContent()?.membership === "invite");
|
||||
const fromUs = event.getSender() === this.baseApis.getUserId();
|
||||
|
||||
if (!weRequested) {
|
||||
// If someone sends us an unsolicited key and it's not
|
||||
// shared history, ignore it
|
||||
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;
|
||||
@ -1449,7 +1450,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
// 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 && !fromUs) {
|
||||
if (room && !fromInviter) {
|
||||
logger.log("forwarded key not from inviter or from us - ignoring");
|
||||
return;
|
||||
}
|
||||
@ -1794,12 +1795,17 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
* @private
|
||||
* @param {String} senderKey
|
||||
* @param {String} sessionId
|
||||
* @param {Boolean} keyTrusted
|
||||
* @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 with trusted keys
|
||||
*/
|
||||
private async retryDecryption(senderKey: string, sessionId: string, keyTrusted?: boolean): Promise<boolean> {
|
||||
private async retryDecryption(
|
||||
senderKey: string,
|
||||
sessionId: string,
|
||||
forceRedecryptIfUntrusted?: boolean,
|
||||
): Promise<boolean> {
|
||||
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||
if (!senderPendingEvents) {
|
||||
return true;
|
||||
@ -1814,7 +1820,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
|
||||
await Promise.all([...pending].map(async (ev) => {
|
||||
try {
|
||||
await ev.attemptDecryption(this.crypto, { isRetry: true, keyTrusted });
|
||||
await ev.attemptDecryption(this.crypto, { isRetry: true, forceRedecryptIfUntrusted });
|
||||
} catch (e) {
|
||||
// don't die if something goes wrong
|
||||
}
|
||||
|
@ -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";
|
||||
|
||||
|
@ -210,7 +210,6 @@ export class DehydrationManager {
|
||||
logger.log("Uploading account to server");
|
||||
// eslint-disable-next-line camelcase
|
||||
const dehydrateResult = await this.crypto.baseApis.http.authedRequest<{ device_id: string }>(
|
||||
undefined,
|
||||
Method.Put,
|
||||
"/dehydrated_device",
|
||||
undefined,
|
||||
@ -273,7 +272,6 @@ export class DehydrationManager {
|
||||
|
||||
logger.log("Uploading keys to server");
|
||||
await this.crypto.baseApis.http.authedRequest(
|
||||
undefined,
|
||||
Method.Post,
|
||||
"/keys/upload/" + encodeURI(deviceId),
|
||||
undefined,
|
||||
|
@ -34,7 +34,6 @@ import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
|
||||
export const VERSION = 11;
|
||||
const PROFILE_TRANSACTIONS = false;
|
||||
|
||||
/**
|
||||
@ -950,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"],
|
||||
});
|
||||
@ -997,18 +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"],
|
||||
});
|
||||
}
|
||||
if (oldVersion < 11) {
|
||||
},
|
||||
(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 {
|
||||
|
62
src/feature.ts
Normal file
62
src/feature.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
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 { IServerVersions } from "./client";
|
||||
|
||||
export enum ServerSupport {
|
||||
Stable,
|
||||
Unstable,
|
||||
Unsupported
|
||||
}
|
||||
|
||||
export enum Feature {
|
||||
Thread = "Thread",
|
||||
ThreadUnreadNotifications = "ThreadUnreadNotifications",
|
||||
}
|
||||
|
||||
type FeatureSupportCondition = {
|
||||
unstablePrefixes?: string[];
|
||||
matrixVersion?: string;
|
||||
};
|
||||
|
||||
const featureSupportResolver: Record<string, FeatureSupportCondition> = {
|
||||
[Feature.Thread]: {
|
||||
unstablePrefixes: ["org.matrix.msc3440"],
|
||||
matrixVersion: "v1.3",
|
||||
},
|
||||
[Feature.ThreadUnreadNotifications]: {
|
||||
unstablePrefixes: ["org.matrix.msc3771", "org.matrix.msc3773"],
|
||||
matrixVersion: "v1.4",
|
||||
},
|
||||
};
|
||||
|
||||
export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {
|
||||
const supportMap = new Map<Feature, ServerSupport>();
|
||||
for (const [feature, supportCondition] of Object.entries(featureSupportResolver)) {
|
||||
const supportMatrixVersion = versions.versions?.includes(supportCondition.matrixVersion || "") ?? false;
|
||||
const supportUnstablePrefixes = supportCondition.unstablePrefixes?.every(unstablePrefix => {
|
||||
return versions.unstable_features?.[unstablePrefix] === true;
|
||||
}) ?? false;
|
||||
if (supportMatrixVersion) {
|
||||
supportMap.set(feature as Feature, ServerSupport.Stable);
|
||||
} else if (supportUnstablePrefixes) {
|
||||
supportMap.set(feature as Feature, ServerSupport.Unstable);
|
||||
} else {
|
||||
supportMap.set(feature as Feature, ServerSupport.Unsupported);
|
||||
}
|
||||
}
|
||||
return supportMap;
|
||||
}
|
@ -73,7 +73,7 @@ export interface IFilterComponent {
|
||||
* @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true }
|
||||
*/
|
||||
export class FilterComponent {
|
||||
constructor(private filterJson: IFilterComponent, public readonly userId?: string) {}
|
||||
constructor(private filterJson: IFilterComponent, public readonly userId?: string | undefined | null) {}
|
||||
|
||||
/**
|
||||
* Checks with the filter component matches the given event
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
EventType,
|
||||
RelationType,
|
||||
} from "./@types/event";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
|
||||
import { FilterComponent, IFilterComponent } from "./filter-component";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
|
||||
@ -57,6 +58,8 @@ export interface IRoomEventFilter extends IFilterComponent {
|
||||
types?: Array<EventType | string>;
|
||||
related_by_senders?: Array<RelationType | string>;
|
||||
related_by_rel_types?: string[];
|
||||
unread_thread_notifications?: boolean;
|
||||
"org.matrix.msc3773.unread_thread_notifications"?: boolean;
|
||||
|
||||
// Unstable values
|
||||
"io.element.relation_senders"?: Array<RelationType | string>;
|
||||
@ -97,7 +100,7 @@ export class Filter {
|
||||
* @param {Object} jsonObj
|
||||
* @return {Filter}
|
||||
*/
|
||||
public static fromJson(userId: string, filterId: string, jsonObj: IFilterDefinition): Filter {
|
||||
public static fromJson(userId: string | undefined | null, filterId: string, jsonObj: IFilterDefinition): Filter {
|
||||
const filter = new Filter(userId, filterId);
|
||||
filter.setDefinition(jsonObj);
|
||||
return filter;
|
||||
@ -107,7 +110,7 @@ export class Filter {
|
||||
private roomFilter: FilterComponent;
|
||||
private roomTimelineFilter: FilterComponent;
|
||||
|
||||
constructor(public readonly userId: string, public filterId?: string) {}
|
||||
constructor(public readonly userId: string | undefined | null, public filterId?: string) {}
|
||||
|
||||
/**
|
||||
* Get the ID of this filter on your homeserver (if known)
|
||||
@ -220,7 +223,24 @@ export class Filter {
|
||||
setProp(this.definition, "room.timeline.limit", limit);
|
||||
}
|
||||
|
||||
setLazyLoadMembers(enabled: boolean) {
|
||||
/**
|
||||
* Enable threads unread notification
|
||||
* @param {boolean} enabled
|
||||
*/
|
||||
public setUnreadThreadNotifications(enabled: boolean): void {
|
||||
this.definition = {
|
||||
...this.definition,
|
||||
room: {
|
||||
...this.definition?.room,
|
||||
timeline: {
|
||||
...this.definition?.room?.timeline,
|
||||
[UNREAD_THREAD_NOTIFICATIONS.name]: !!enabled,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
setLazyLoadMembers(enabled: boolean): void {
|
||||
setProp(this.definition, "room.state.lazy_load_members", !!enabled);
|
||||
}
|
||||
|
||||
|
1140
src/http-api.ts
1140
src/http-api.ts
File diff suppressed because it is too large
Load Diff
70
src/http-api/errors.ts
Normal file
70
src/http-api/errors.ts
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
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 { IUsageLimit } from "../@types/partials";
|
||||
|
||||
interface IErrorJson extends Partial<IUsageLimit> {
|
||||
[key: string]: any; // extensible
|
||||
errcode?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Matrix error. This is a JavaScript Error with additional
|
||||
* information specific to the standard Matrix error response.
|
||||
* @constructor
|
||||
* @param {Object} errorJson The Matrix error JSON returned from the homeserver.
|
||||
* @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN".
|
||||
* @prop {string} name Same as MatrixError.errcode but with a default unknown string.
|
||||
* @prop {string} message The Matrix 'error' value, e.g. "Missing token."
|
||||
* @prop {Object} data The raw Matrix error JSON used to construct this object.
|
||||
* @prop {number} httpStatus The numeric HTTP status code given
|
||||
*/
|
||||
export class MatrixError extends Error {
|
||||
public readonly errcode?: string;
|
||||
public readonly data: IErrorJson;
|
||||
|
||||
constructor(errorJson: IErrorJson = {}, public httpStatus?: number, public url?: string) {
|
||||
let message = errorJson.error || "Unknown message";
|
||||
if (httpStatus) {
|
||||
message = `[${httpStatus}] ${message}`;
|
||||
}
|
||||
if (url) {
|
||||
message = `${message} (${url})`;
|
||||
}
|
||||
super(`MatrixError: ${message}`);
|
||||
this.errcode = errorJson.errcode;
|
||||
this.name = errorJson.errcode || "Unknown error code";
|
||||
this.data = errorJson;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a ConnectionError. This is a JavaScript Error indicating
|
||||
* that a request failed because of some error with the connection, either
|
||||
* CORS was not correctly configured on the server, the server didn't response,
|
||||
* the request timed out, or the internet connection on the client side went down.
|
||||
* @constructor
|
||||
*/
|
||||
export class ConnectionError extends Error {
|
||||
constructor(message: string, cause?: Error) {
|
||||
super(message + (cause ? `: ${cause.message}` : ""));
|
||||
}
|
||||
|
||||
get name() {
|
||||
return "ConnectionError";
|
||||
}
|
||||
}
|
327
src/http-api/fetch.ts
Normal file
327
src/http-api/fetch.ts
Normal file
@ -0,0 +1,327 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is an internal module. See {@link MatrixHttpApi} for the public class.
|
||||
* @module http-api
|
||||
*/
|
||||
|
||||
import * as utils from "../utils";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { Method } from "./method";
|
||||
import { ConnectionError, MatrixError } from "./errors";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, IRequestOpts } from "./interface";
|
||||
import { anySignal, parseErrorResponse, timeoutSignal } from "./utils";
|
||||
import { QueryDict } from "../utils";
|
||||
|
||||
type Body = Record<string, any> | BodyInit;
|
||||
|
||||
interface TypedResponse<T> extends Response {
|
||||
json(): Promise<T>;
|
||||
}
|
||||
|
||||
export type ResponseType<T, O extends IHttpOpts> =
|
||||
O extends undefined ? T :
|
||||
O extends { onlyData: true } ? T :
|
||||
TypedResponse<T>;
|
||||
|
||||
export class FetchHttpApi<O extends IHttpOpts> {
|
||||
private abortController = new AbortController();
|
||||
|
||||
constructor(
|
||||
private eventEmitter: TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>,
|
||||
public readonly opts: O,
|
||||
) {
|
||||
utils.checkObjectHasKeys(opts, ["baseUrl", "prefix"]);
|
||||
opts.onlyData = !!opts.onlyData;
|
||||
opts.useAuthorizationHeader = opts.useAuthorizationHeader ?? true;
|
||||
}
|
||||
|
||||
public abort(): void {
|
||||
this.abortController.abort();
|
||||
this.abortController = new AbortController();
|
||||
}
|
||||
|
||||
public fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
|
||||
if (this.opts.fetchFn) {
|
||||
return this.opts.fetchFn(resource, options);
|
||||
}
|
||||
return global.fetch(resource, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base URL for the identity server
|
||||
* @param {string} url The new base url
|
||||
*/
|
||||
public setIdBaseUrl(url: string): void {
|
||||
this.opts.idBaseUrl = url;
|
||||
}
|
||||
|
||||
public idServerRequest<T extends {}>(
|
||||
method: Method,
|
||||
path: string,
|
||||
params: Record<string, string | string[]>,
|
||||
prefix: string,
|
||||
accessToken?: string,
|
||||
): Promise<ResponseType<T, O>> {
|
||||
if (!this.opts.idBaseUrl) {
|
||||
throw new Error("No identity server base URL set");
|
||||
}
|
||||
|
||||
let queryParams: QueryDict | undefined = undefined;
|
||||
let body: Record<string, string | string[]> | undefined = undefined;
|
||||
if (method === Method.Get) {
|
||||
queryParams = params;
|
||||
} else {
|
||||
body = params;
|
||||
}
|
||||
|
||||
const fullUri = this.getUrl(path, queryParams, prefix, this.opts.idBaseUrl);
|
||||
|
||||
const opts: IRequestOpts = {
|
||||
json: true,
|
||||
headers: {},
|
||||
};
|
||||
if (accessToken) {
|
||||
opts.headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
return this.requestOtherUrl(method, fullUri, body, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform an authorised request to the homeserver.
|
||||
* @param {string} method The HTTP method e.g. "GET".
|
||||
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
|
||||
* "/createRoom".
|
||||
*
|
||||
* @param {Object=} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded). If unspecified, there will be no query params.
|
||||
*
|
||||
* @param {Object} [body] The HTTP JSON body.
|
||||
*
|
||||
* @param {Object|Number=} opts additional options. If a number is specified,
|
||||
* this is treated as `opts.localTimeoutMs`.
|
||||
*
|
||||
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
*
|
||||
* @param {string=} opts.prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
|
||||
*
|
||||
* @param {string=} opts.baseUrl The alternative base url to use.
|
||||
* If not specified, uses this.opts.baseUrl
|
||||
*
|
||||
* @param {Object=} opts.headers map of additional request headers
|
||||
*
|
||||
* @return {Promise} Resolves to <code>{data: {Object},
|
||||
* headers: {Object}, code: {Number}}</code>.
|
||||
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
||||
* object only.
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
public authedRequest<T>(
|
||||
method: Method,
|
||||
path: string,
|
||||
queryParams?: QueryDict,
|
||||
body?: Body,
|
||||
opts: IRequestOpts = {},
|
||||
): Promise<ResponseType<T, O>> {
|
||||
if (!queryParams) queryParams = {};
|
||||
|
||||
if (this.opts.useAuthorizationHeader) {
|
||||
if (!opts.headers) {
|
||||
opts.headers = {};
|
||||
}
|
||||
if (!opts.headers.Authorization) {
|
||||
opts.headers.Authorization = "Bearer " + this.opts.accessToken;
|
||||
}
|
||||
if (queryParams.access_token) {
|
||||
delete queryParams.access_token;
|
||||
}
|
||||
} else if (!queryParams.access_token) {
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
}
|
||||
|
||||
const requestPromise = this.request<T>(method, path, queryParams, body, opts);
|
||||
|
||||
requestPromise.catch((err: MatrixError) => {
|
||||
if (err.errcode == 'M_UNKNOWN_TOKEN' && !opts?.inhibitLogoutEmit) {
|
||||
this.eventEmitter.emit(HttpApiEvent.SessionLoggedOut, err);
|
||||
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
|
||||
this.eventEmitter.emit(HttpApiEvent.NoConsent, err.message, err.data.consent_uri);
|
||||
}
|
||||
});
|
||||
|
||||
// return the original promise, otherwise tests break due to it having to
|
||||
// go around the event loop one more time to process the result of the request
|
||||
return requestPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a request to the homeserver without any credentials.
|
||||
* @param {string} method The HTTP method e.g. "GET".
|
||||
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
|
||||
* "/createRoom".
|
||||
*
|
||||
* @param {Object=} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded). If unspecified, there will be no query params.
|
||||
*
|
||||
* @param {Object} [body] The HTTP JSON body.
|
||||
*
|
||||
* @param {Object=} opts additional options
|
||||
*
|
||||
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
*
|
||||
* @param {string=} opts.prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
|
||||
*
|
||||
* @param {Object=} opts.headers map of additional request headers
|
||||
*
|
||||
* @return {Promise} Resolves to <code>{data: {Object},
|
||||
* headers: {Object}, code: {Number}}</code>.
|
||||
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
||||
* object only.
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
public request<T>(
|
||||
method: Method,
|
||||
path: string,
|
||||
queryParams?: QueryDict,
|
||||
body?: Body,
|
||||
opts?: IRequestOpts,
|
||||
): Promise<ResponseType<T, O>> {
|
||||
const fullUri = this.getUrl(path, queryParams, opts?.prefix, opts?.baseUrl);
|
||||
return this.requestOtherUrl<T>(method, fullUri, body, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a request to an arbitrary URL.
|
||||
* @param {string} method The HTTP method e.g. "GET".
|
||||
* @param {string} url The HTTP URL object.
|
||||
*
|
||||
* @param {Object} [body] The HTTP JSON body.
|
||||
*
|
||||
* @param {Object=} opts additional options
|
||||
*
|
||||
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
*
|
||||
* @param {Object=} opts.headers map of additional request headers
|
||||
*
|
||||
* @return {Promise} Resolves to data unless `onlyData` is specified as false,
|
||||
* where the resolved value will be a fetch Response object.
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
public async requestOtherUrl<T>(
|
||||
method: Method,
|
||||
url: URL | string,
|
||||
body?: Body,
|
||||
opts: Pick<IRequestOpts, "headers" | "json" | "localTimeoutMs" | "abortSignal"> = {},
|
||||
): Promise<ResponseType<T, O>> {
|
||||
const headers = Object.assign({}, opts.headers || {});
|
||||
const json = opts.json ?? true;
|
||||
// We can't use getPrototypeOf here as objects made in other contexts e.g. over postMessage won't have same ref
|
||||
const jsonBody = json && body?.constructor?.name === Object.name;
|
||||
|
||||
if (json) {
|
||||
if (jsonBody && !headers["Content-Type"]) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
if (!headers["Accept"]) {
|
||||
headers["Accept"] = "application/json";
|
||||
}
|
||||
}
|
||||
|
||||
const timeout = opts.localTimeoutMs ?? this.opts.localTimeoutMs;
|
||||
const signals = [
|
||||
this.abortController.signal,
|
||||
];
|
||||
if (timeout !== undefined) {
|
||||
signals.push(timeoutSignal(timeout));
|
||||
}
|
||||
if (opts.abortSignal) {
|
||||
signals.push(opts.abortSignal);
|
||||
}
|
||||
|
||||
let data: BodyInit;
|
||||
if (jsonBody) {
|
||||
data = JSON.stringify(body);
|
||||
} else {
|
||||
data = body as BodyInit;
|
||||
}
|
||||
|
||||
const { signal, cleanup } = anySignal(signals);
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await this.fetch(url, {
|
||||
signal,
|
||||
method,
|
||||
body: data,
|
||||
headers,
|
||||
mode: "cors",
|
||||
redirect: "follow",
|
||||
referrer: "",
|
||||
referrerPolicy: "no-referrer",
|
||||
cache: "no-cache",
|
||||
credentials: "omit", // we send credentials via headers
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.name === "AbortError") {
|
||||
throw e;
|
||||
}
|
||||
throw new ConnectionError("fetch failed", e);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw parseErrorResponse(res, await res.text());
|
||||
}
|
||||
|
||||
if (this.opts.onlyData) {
|
||||
return json ? res.json() : res.text();
|
||||
}
|
||||
return res as ResponseType<T, O>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form and return a homeserver request URL based on the given path params and prefix.
|
||||
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g. "/createRoom".
|
||||
* @param {Object} queryParams A dict of query params (these will NOT be urlencoded).
|
||||
* @param {string} prefix The full prefix to use e.g. "/_matrix/client/v2_alpha", defaulting to this.opts.prefix.
|
||||
* @param {string} baseUrl The baseUrl to use e.g. "https://matrix.org/", defaulting to this.opts.baseUrl.
|
||||
* @return {string} URL
|
||||
*/
|
||||
public getUrl(
|
||||
path: string,
|
||||
queryParams?: QueryDict,
|
||||
prefix?: string,
|
||||
baseUrl?: string,
|
||||
): URL {
|
||||
const url = new URL((baseUrl ?? this.opts.baseUrl) + (prefix ?? this.opts.prefix) + path);
|
||||
if (queryParams) {
|
||||
utils.encodeParams(queryParams, url.searchParams);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
216
src/http-api/index.ts
Normal file
216
src/http-api/index.ts
Normal file
@ -0,0 +1,216 @@
|
||||
/*
|
||||
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 { FetchHttpApi } from "./fetch";
|
||||
import { FileType, IContentUri, IHttpOpts, Upload, UploadOpts, UploadResponse } from "./interface";
|
||||
import { MediaPrefix } from "./prefix";
|
||||
import * as utils from "../utils";
|
||||
import * as callbacks from "../realtime-callbacks";
|
||||
import { Method } from "./method";
|
||||
import { ConnectionError, MatrixError } from "./errors";
|
||||
import { parseErrorResponse } from "./utils";
|
||||
|
||||
export * from "./interface";
|
||||
export * from "./prefix";
|
||||
export * from "./errors";
|
||||
export * from "./method";
|
||||
export * from "./utils";
|
||||
|
||||
export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
private uploads: Upload[] = [];
|
||||
|
||||
/**
|
||||
* Upload content to the homeserver
|
||||
*
|
||||
* @param {object} file The object to upload. On a browser, something that
|
||||
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
|
||||
* a Buffer, String or ReadStream.
|
||||
*
|
||||
* @param {object} opts options object
|
||||
*
|
||||
* @param {string=} opts.name Name to give the file on the server. Defaults
|
||||
* to <tt>file.name</tt>.
|
||||
*
|
||||
* @param {boolean=} opts.includeFilename if false will not send the filename,
|
||||
* e.g for encrypted file uploads where filename leaks are undesirable.
|
||||
* Defaults to true.
|
||||
*
|
||||
* @param {string=} opts.type Content-type for the upload. Defaults to
|
||||
* <tt>file.type</tt>, or <tt>application/octet-stream</tt>.
|
||||
*
|
||||
* @param {boolean=} opts.rawResponse Return the raw body, rather than
|
||||
* parsing the JSON. Defaults to false (except on node.js, where it
|
||||
* defaults to true for backwards compatibility).
|
||||
*
|
||||
* @param {boolean=} opts.onlyContentUri Just return the content URI,
|
||||
* rather than the whole body. Defaults to false (except on browsers,
|
||||
* where it defaults to true for backwards compatibility). Ignored if
|
||||
* opts.rawResponse is true.
|
||||
*
|
||||
* @param {Function=} opts.progressHandler Optional. Called when a chunk of
|
||||
* data has been uploaded, with an object containing the fields `loaded`
|
||||
* (number of bytes transferred) and `total` (total size, if known).
|
||||
*
|
||||
* @return {Promise} Resolves to response object, as
|
||||
* determined by this.opts.onlyData, opts.rawResponse, and
|
||||
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
|
||||
*/
|
||||
public uploadContent(file: FileType, opts: UploadOpts = {}): Promise<UploadResponse> {
|
||||
const includeFilename = opts.includeFilename ?? true;
|
||||
const abortController = opts.abortController ?? new AbortController();
|
||||
|
||||
// If the file doesn't have a mime type, use a default since the HS errors if we don't supply one.
|
||||
const contentType = opts.type ?? (file as File).type ?? 'application/octet-stream';
|
||||
const fileName = opts.name ?? (file as File).name;
|
||||
|
||||
const upload = {
|
||||
loaded: 0,
|
||||
total: 0,
|
||||
abortController,
|
||||
} as Upload;
|
||||
const defer = utils.defer<UploadResponse>();
|
||||
|
||||
if (global.XMLHttpRequest) {
|
||||
const xhr = new global.XMLHttpRequest();
|
||||
|
||||
const timeoutFn = function() {
|
||||
xhr.abort();
|
||||
defer.reject(new Error("Timeout"));
|
||||
};
|
||||
|
||||
// set an initial timeout of 30s; we'll advance it each time we get a progress notification
|
||||
let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
switch (xhr.readyState) {
|
||||
case global.XMLHttpRequest.DONE:
|
||||
callbacks.clearTimeout(timeoutTimer);
|
||||
try {
|
||||
if (xhr.status === 0) {
|
||||
throw new DOMException(xhr.statusText, "AbortError"); // mimic fetch API
|
||||
}
|
||||
if (!xhr.responseText) {
|
||||
throw new Error('No response body.');
|
||||
}
|
||||
|
||||
if (xhr.status >= 400) {
|
||||
defer.reject(parseErrorResponse(xhr, xhr.responseText));
|
||||
} else {
|
||||
defer.resolve(JSON.parse(xhr.responseText));
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name === "AbortError") {
|
||||
defer.reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
(<MatrixError>err).httpStatus = xhr.status;
|
||||
defer.reject(new ConnectionError("request failed", err));
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
xhr.upload.onprogress = (ev: ProgressEvent) => {
|
||||
callbacks.clearTimeout(timeoutTimer);
|
||||
upload.loaded = ev.loaded;
|
||||
upload.total = ev.total;
|
||||
timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
|
||||
opts.progressHandler?.({
|
||||
loaded: ev.loaded,
|
||||
total: ev.total,
|
||||
});
|
||||
};
|
||||
|
||||
const url = this.getUrl("/upload", undefined, MediaPrefix.R0);
|
||||
|
||||
if (includeFilename && fileName) {
|
||||
url.searchParams.set("filename", encodeURIComponent(fileName));
|
||||
}
|
||||
|
||||
if (!this.opts.useAuthorizationHeader && this.opts.accessToken) {
|
||||
url.searchParams.set("access_token", encodeURIComponent(this.opts.accessToken));
|
||||
}
|
||||
|
||||
xhr.open(Method.Post, url.href);
|
||||
if (this.opts.useAuthorizationHeader && this.opts.accessToken) {
|
||||
xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken);
|
||||
}
|
||||
xhr.setRequestHeader("Content-Type", contentType);
|
||||
xhr.send(file);
|
||||
|
||||
abortController.signal.addEventListener("abort", () => {
|
||||
xhr.abort();
|
||||
});
|
||||
} else {
|
||||
const queryParams: utils.QueryDict = {};
|
||||
if (includeFilename && fileName) {
|
||||
queryParams.filename = fileName;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = { "Content-Type": contentType };
|
||||
|
||||
this.authedRequest<UploadResponse>(
|
||||
Method.Post, "/upload", queryParams, file, {
|
||||
prefix: MediaPrefix.R0,
|
||||
headers,
|
||||
abortSignal: abortController.signal,
|
||||
},
|
||||
).then(response => {
|
||||
return this.opts.onlyData ? <UploadResponse>response : response.json();
|
||||
}).then(defer.resolve, defer.reject);
|
||||
}
|
||||
|
||||
// remove the upload from the list on completion
|
||||
upload.promise = defer.promise.finally(() => {
|
||||
utils.removeElement(this.uploads, elem => elem === upload);
|
||||
});
|
||||
abortController.signal.addEventListener("abort", () => {
|
||||
utils.removeElement(this.uploads, elem => elem === upload);
|
||||
defer.reject(new DOMException("Aborted", "AbortError"));
|
||||
});
|
||||
this.uploads.push(upload);
|
||||
return upload.promise;
|
||||
}
|
||||
|
||||
public cancelUpload(promise: Promise<UploadResponse>): boolean {
|
||||
const upload = this.uploads.find(u => u.promise === promise);
|
||||
if (upload) {
|
||||
upload.abortController.abort();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public getCurrentUploads(): Upload[] {
|
||||
return this.uploads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content repository url with query parameters.
|
||||
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
|
||||
* path and query parameters respectively.
|
||||
*/
|
||||
public getContentUri(): IContentUri {
|
||||
return {
|
||||
base: this.opts.baseUrl,
|
||||
path: MediaPrefix.R0 + "/upload",
|
||||
params: {
|
||||
access_token: this.opts.accessToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
93
src/http-api/interface.ts
Normal file
93
src/http-api/interface.ts
Normal file
@ -0,0 +1,93 @@
|
||||
/*
|
||||
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 { MatrixError } from "./errors";
|
||||
|
||||
export interface IHttpOpts {
|
||||
fetchFn?: typeof global.fetch;
|
||||
|
||||
baseUrl: string;
|
||||
idBaseUrl?: string;
|
||||
prefix: string;
|
||||
extraParams?: Record<string, string>;
|
||||
|
||||
accessToken?: string;
|
||||
useAuthorizationHeader?: boolean; // defaults to true
|
||||
|
||||
onlyData?: boolean;
|
||||
localTimeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface IRequestOpts {
|
||||
baseUrl?: string;
|
||||
prefix?: string;
|
||||
|
||||
headers?: Record<string, string>;
|
||||
abortSignal?: AbortSignal;
|
||||
localTimeoutMs?: number;
|
||||
json?: boolean; // defaults to true
|
||||
|
||||
// Set to true to prevent the request function from emitting a Session.logged_out event.
|
||||
// This is intended for use on endpoints where M_UNKNOWN_TOKEN is a valid/notable error response,
|
||||
// such as with token refreshes.
|
||||
inhibitLogoutEmit?: boolean;
|
||||
}
|
||||
|
||||
export interface IContentUri {
|
||||
base: string;
|
||||
path: string;
|
||||
params: {
|
||||
// eslint-disable-next-line camelcase
|
||||
access_token: string;
|
||||
};
|
||||
}
|
||||
|
||||
export enum HttpApiEvent {
|
||||
SessionLoggedOut = "Session.logged_out",
|
||||
NoConsent = "no_consent",
|
||||
}
|
||||
|
||||
export type HttpApiEventHandlerMap = {
|
||||
[HttpApiEvent.SessionLoggedOut]: (err: MatrixError) => void;
|
||||
[HttpApiEvent.NoConsent]: (message: string, consentUri: string) => void;
|
||||
};
|
||||
|
||||
export interface UploadProgress {
|
||||
loaded: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface UploadOpts {
|
||||
name?: string;
|
||||
type?: string;
|
||||
includeFilename?: boolean;
|
||||
progressHandler?(progress: UploadProgress): void;
|
||||
abortController?: AbortController;
|
||||
}
|
||||
|
||||
export interface Upload {
|
||||
loaded: number;
|
||||
total: number;
|
||||
promise: Promise<UploadResponse>;
|
||||
abortController: AbortController;
|
||||
}
|
||||
|
||||
export interface UploadResponse {
|
||||
// eslint-disable-next-line camelcase
|
||||
content_uri: string;
|
||||
}
|
||||
|
||||
export type FileType = XMLHttpRequestBodyInit;
|
22
src/http-api/method.ts
Normal file
22
src/http-api/method.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/*
|
||||
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 enum Method {
|
||||
Get = "GET",
|
||||
Put = "PUT",
|
||||
Post = "POST",
|
||||
Delete = "DELETE",
|
||||
}
|
53
src/http-api/prefix.ts
Normal file
53
src/http-api/prefix.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
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 enum ClientPrefix {
|
||||
/**
|
||||
* A constant representing the URI path for release 0 of the Client-Server HTTP API.
|
||||
*/
|
||||
R0 = "/_matrix/client/r0",
|
||||
/**
|
||||
* A constant representing the URI path for the legacy release v1 of the Client-Server HTTP API.
|
||||
*/
|
||||
V1 = "/_matrix/client/v1",
|
||||
/**
|
||||
* A constant representing the URI path for Client-Server API endpoints versioned at v3.
|
||||
*/
|
||||
V3 = "/_matrix/client/v3",
|
||||
/**
|
||||
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
|
||||
*/
|
||||
Unstable = "/_matrix/client/unstable",
|
||||
}
|
||||
|
||||
export enum IdentityPrefix {
|
||||
/**
|
||||
* URI path for v1 of the identity API
|
||||
* @deprecated Use v2.
|
||||
*/
|
||||
V1 = "/_matrix/identity/api/v1",
|
||||
/**
|
||||
* URI path for the v2 identity API
|
||||
*/
|
||||
V2 = "/_matrix/identity/api/v2",
|
||||
}
|
||||
|
||||
export enum MediaPrefix {
|
||||
/**
|
||||
* URI path for the media repo API
|
||||
*/
|
||||
R0 = "/_matrix/media/r0",
|
||||
}
|
153
src/http-api/utils.ts
Normal file
153
src/http-api/utils.ts
Normal file
@ -0,0 +1,153 @@
|
||||
/*
|
||||
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 { parse as parseContentType, ParsedMediaType } from "content-type";
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { sleep } from "../utils";
|
||||
import { ConnectionError, MatrixError } from "./errors";
|
||||
|
||||
// Ponyfill for https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout
|
||||
export function timeoutSignal(ms: number): AbortSignal {
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => {
|
||||
controller.abort();
|
||||
}, ms);
|
||||
|
||||
return controller.signal;
|
||||
}
|
||||
|
||||
export function anySignal(signals: AbortSignal[]): {
|
||||
signal: AbortSignal;
|
||||
cleanup(): void;
|
||||
} {
|
||||
const controller = new AbortController();
|
||||
|
||||
function cleanup() {
|
||||
for (const signal of signals) {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
}
|
||||
}
|
||||
|
||||
function onAbort() {
|
||||
controller.abort();
|
||||
cleanup();
|
||||
}
|
||||
|
||||
for (const signal of signals) {
|
||||
if (signal.aborted) {
|
||||
onAbort();
|
||||
break;
|
||||
}
|
||||
signal.addEventListener("abort", onAbort);
|
||||
}
|
||||
|
||||
return {
|
||||
signal: controller.signal,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to turn an HTTP error response into a Javascript Error.
|
||||
*
|
||||
* If it is a JSON response, we will parse it into a MatrixError. Otherwise
|
||||
* we return a generic Error.
|
||||
*
|
||||
* @param {XMLHttpRequest|Response} response response object
|
||||
* @param {String} body raw body of the response
|
||||
* @returns {Error}
|
||||
*/
|
||||
export function parseErrorResponse(response: XMLHttpRequest | Response, body?: string): Error {
|
||||
let contentType: ParsedMediaType;
|
||||
try {
|
||||
contentType = getResponseContentType(response);
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
|
||||
if (contentType?.type === "application/json" && body) {
|
||||
return new MatrixError(
|
||||
JSON.parse(body),
|
||||
response.status,
|
||||
isXhr(response) ? response.responseURL : response.url,
|
||||
);
|
||||
}
|
||||
if (contentType?.type === "text/plain") {
|
||||
return new Error(`Server returned ${response.status} error: ${body}`);
|
||||
}
|
||||
return new Error(`Server returned ${response.status} error`);
|
||||
}
|
||||
|
||||
function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest {
|
||||
return "getResponseHeader" in response;
|
||||
}
|
||||
|
||||
/**
|
||||
* extract the Content-Type header from the response object, and
|
||||
* parse it to a `{type, parameters}` object.
|
||||
*
|
||||
* returns null if no content-type header could be found.
|
||||
*
|
||||
* @param {XMLHttpRequest|Response} response response object
|
||||
* @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found
|
||||
*/
|
||||
function getResponseContentType(response: XMLHttpRequest | Response): ParsedMediaType | null {
|
||||
let contentType: string | null;
|
||||
if (isXhr(response)) {
|
||||
contentType = response.getResponseHeader("Content-Type");
|
||||
} else {
|
||||
contentType = response.headers.get("Content-Type");
|
||||
}
|
||||
|
||||
if (!contentType) return null;
|
||||
|
||||
try {
|
||||
return parseContentType(contentType);
|
||||
} catch (e) {
|
||||
throw new Error(`Error parsing Content-Type '${contentType}': ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries a network operation run in a callback.
|
||||
* @param {number} maxAttempts maximum attempts to try
|
||||
* @param {Function} callback callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again.
|
||||
* @return {any} the result of the network operation
|
||||
* @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError
|
||||
*/
|
||||
export async function retryNetworkOperation<T>(maxAttempts: number, callback: () => Promise<T>): Promise<T> {
|
||||
let attempts = 0;
|
||||
let lastConnectionError: ConnectionError | null = null;
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
if (attempts > 0) {
|
||||
const timeout = 1000 * Math.pow(2, attempts);
|
||||
logger.log(`network operation failed ${attempts} times, retrying in ${timeout}ms...`);
|
||||
await sleep(timeout);
|
||||
}
|
||||
return await callback();
|
||||
} catch (err) {
|
||||
if (err instanceof ConnectionError) {
|
||||
attempts += 1;
|
||||
lastConnectionError = err;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastConnectionError;
|
||||
}
|
@ -14,17 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as request from "request";
|
||||
|
||||
import * as matrixcs from "./matrix";
|
||||
import * as utils from "./utils";
|
||||
import { logger } from './logger';
|
||||
|
||||
if (matrixcs.getRequest()) {
|
||||
if (global.__js_sdk_entrypoint) {
|
||||
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
|
||||
}
|
||||
|
||||
matrixcs.request(request);
|
||||
global.__js_sdk_entrypoint = true;
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
|
@ -64,41 +64,6 @@ export {
|
||||
} from "./webrtc/groupCall";
|
||||
export type { GroupCall } from "./webrtc/groupCall";
|
||||
|
||||
// expose the underlying request object so different environments can use
|
||||
// different request libs (e.g. request or browser-request)
|
||||
let requestInstance;
|
||||
|
||||
/**
|
||||
* The function used to perform HTTP requests. Only use this if you want to
|
||||
* use a different HTTP library, e.g. Angular's <code>$http</code>. This should
|
||||
* be set prior to calling {@link createClient}.
|
||||
* @param {requestFunction} r The request function to use.
|
||||
*/
|
||||
export function request(r) {
|
||||
requestInstance = r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the currently-set request function.
|
||||
* @return {requestFunction} The current request function.
|
||||
*/
|
||||
export function getRequest() {
|
||||
return requestInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply wrapping code around the request function. The wrapper function is
|
||||
* installed as the new request handler, and when invoked it is passed the
|
||||
* previous value, along with the options and callback arguments.
|
||||
* @param {requestWrapperFunction} wrapper The wrapping function.
|
||||
*/
|
||||
export function wrapRequest(wrapper) {
|
||||
const origRequest = requestInstance;
|
||||
requestInstance = function(options, callback) {
|
||||
return wrapper(origRequest, options, callback);
|
||||
};
|
||||
}
|
||||
|
||||
let cryptoStoreFactory = () => new MemoryCryptoStore;
|
||||
|
||||
/**
|
||||
@ -111,10 +76,7 @@ export function setCryptoStoreFactory(fac) {
|
||||
cryptoStoreFactory = fac;
|
||||
}
|
||||
|
||||
function amendClientOpts(opts: ICreateClientOpts | string): ICreateClientOpts {
|
||||
if (typeof opts === "string") opts = { baseUrl: opts };
|
||||
|
||||
opts.request = opts.request ?? requestInstance;
|
||||
function amendClientOpts(opts: ICreateClientOpts): ICreateClientOpts {
|
||||
opts.store = opts.store ?? new MemoryStore({
|
||||
localStorage: global.localStorage,
|
||||
});
|
||||
@ -127,15 +89,12 @@ function amendClientOpts(opts: ICreateClientOpts | string): ICreateClientOpts {
|
||||
/**
|
||||
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
|
||||
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
|
||||
* @param {(Object|string)} opts The configuration options for this client. If
|
||||
* this is a string, it is assumed to be the base URL. These configuration
|
||||
* @param {Object} opts The configuration options for this client. These configuration
|
||||
* options will be passed directly to {@link module:client.MatrixClient}.
|
||||
* @param {Object} opts.store If not set, defaults to
|
||||
* {@link module:store/memory.MemoryStore}.
|
||||
* @param {Object} opts.scheduler If not set, defaults to
|
||||
* {@link module:scheduler~MatrixScheduler}.
|
||||
* @param {requestFunction} opts.request If not set, defaults to the function
|
||||
* supplied to {@link request} which defaults to the request module from NPM.
|
||||
*
|
||||
* @param {module:crypto.store.base~CryptoStore=} opts.cryptoStore
|
||||
* crypto store implementation. Calls the factory supplied to
|
||||
@ -147,7 +106,7 @@ function amendClientOpts(opts: ICreateClientOpts | string): ICreateClientOpts {
|
||||
* @see {@link module:client.MatrixClient} for the full list of options for
|
||||
* <code>opts</code>.
|
||||
*/
|
||||
export function createClient(opts: ICreateClientOpts | string): MatrixClient {
|
||||
export function createClient(opts: ICreateClientOpts): MatrixClient {
|
||||
return new MatrixClient(amendClientOpts(opts));
|
||||
}
|
||||
|
||||
@ -155,46 +114,7 @@ export function createRoomWidgetClient(
|
||||
widgetApi: WidgetApi,
|
||||
capabilities: ICapabilities,
|
||||
roomId: string,
|
||||
opts: ICreateClientOpts | string,
|
||||
opts: ICreateClientOpts,
|
||||
): MatrixClient {
|
||||
return new RoomWidgetClient(widgetApi, capabilities, roomId, amendClientOpts(opts));
|
||||
}
|
||||
|
||||
/**
|
||||
* The request function interface for performing HTTP requests. This matches the
|
||||
* API for the {@link https://github.com/request/request#requestoptions-callback|
|
||||
* request NPM module}. The SDK will attempt to call this function in order to
|
||||
* perform an HTTP request.
|
||||
* @callback requestFunction
|
||||
* @param {Object} opts The options for this HTTP request.
|
||||
* @param {string} opts.uri The complete URI.
|
||||
* @param {string} opts.method The HTTP method.
|
||||
* @param {Object} opts.qs The query parameters to append to the URI.
|
||||
* @param {Object} opts.body The JSON-serializable object.
|
||||
* @param {boolean} opts.json True if this is a JSON request.
|
||||
* @param {Object} opts._matrix_opts The underlying options set for
|
||||
* {@link MatrixHttpApi}.
|
||||
* @param {requestCallback} callback The request callback.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A wrapper for the request function interface.
|
||||
* @callback requestWrapperFunction
|
||||
* @param {requestFunction} origRequest The underlying request function being
|
||||
* wrapped
|
||||
* @param {Object} opts The options for this HTTP request, given in the same
|
||||
* form as {@link requestFunction}.
|
||||
* @param {requestCallback} callback The request callback.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The request callback interface for performing HTTP requests. This matches the
|
||||
* API for the {@link https://github.com/request/request#requestoptions-callback|
|
||||
* request NPM module}. The SDK will implement a callback which meets this
|
||||
* interface in order to handle the HTTP response.
|
||||
* @callback requestCallback
|
||||
* @param {Error} err The error if one occurred, else falsey.
|
||||
* @param {Object} response The HTTP response which consists of
|
||||
* <code>{statusCode: {Number}, headers: {Object}}</code>
|
||||
* @param {Object} body The parsed HTTP response body.
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -476,10 +476,8 @@ export class MSC3089TreeSpace {
|
||||
info: Partial<IEncryptedFile>,
|
||||
additionalContent?: IContent,
|
||||
): Promise<ISendEventResponse> {
|
||||
const mxc = await this.client.uploadContent(encryptedContents, {
|
||||
const { content_uri: mxc } = await this.client.uploadContent(encryptedContents, {
|
||||
includeFilename: false,
|
||||
onlyContentUri: true,
|
||||
rawResponse: false, // make this explicit otherwise behaviour is different on browser vs NodeJS
|
||||
});
|
||||
info.url = mxc;
|
||||
|
||||
|
@ -123,12 +123,15 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
* @param {MatrixClient=} client the Matrix client which owns this EventTimelineSet,
|
||||
* can be omitted if room is specified.
|
||||
* @param {Thread=} thread the thread to which this timeline set relates.
|
||||
* @param {boolean} isThreadTimeline Whether this timeline set relates to a thread list timeline
|
||||
* (e.g., All threads or My threads)
|
||||
*/
|
||||
constructor(
|
||||
public readonly room: Room | undefined,
|
||||
opts: IOpts = {},
|
||||
client?: MatrixClient,
|
||||
public readonly thread?: Thread,
|
||||
public readonly isThreadTimeline: boolean = false,
|
||||
) {
|
||||
super();
|
||||
|
||||
|
@ -99,7 +99,7 @@ export class EventTimeline {
|
||||
private endState: RoomState;
|
||||
private prevTimeline?: EventTimeline;
|
||||
private nextTimeline?: EventTimeline;
|
||||
public paginationRequests: Record<Direction, Promise<boolean>> = {
|
||||
public paginationRequests: Record<Direction, Promise<boolean> | null> = {
|
||||
[Direction.Backward]: null,
|
||||
[Direction.Forward]: null,
|
||||
};
|
||||
@ -311,7 +311,7 @@ export class EventTimeline {
|
||||
* token for going backwards in time; EventTimeline.FORWARDS to set the
|
||||
* pagination token for going forwards in time.
|
||||
*/
|
||||
public setPaginationToken(token: string, direction: Direction): void {
|
||||
public setPaginationToken(token: string | null, direction: Direction): void {
|
||||
this.getState(direction).paginationToken = token;
|
||||
}
|
||||
|
||||
|
@ -151,7 +151,7 @@ interface IKeyRequestRecipient {
|
||||
export interface IDecryptOptions {
|
||||
emit?: boolean;
|
||||
isRetry?: boolean;
|
||||
keyTrusted?: boolean;
|
||||
forceRedecryptIfUntrusted?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -678,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.
|
||||
@ -696,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() && !(this.isKeySourceUntrusted() && options.keyTrusted)) {
|
||||
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",
|
||||
|
@ -284,7 +284,7 @@ export abstract class ReadReceipt<
|
||||
const readUpToId = this.getEventReadUpTo(userId, false);
|
||||
if (readUpToId === eventId) return true;
|
||||
|
||||
if (this.timeline.length
|
||||
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
|
||||
@ -292,7 +292,7 @@ export abstract class ReadReceipt<
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = this.timeline.length - 1; i >= 0; --i) {
|
||||
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
|
||||
|
@ -97,7 +97,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
// XXX: Should be read-only
|
||||
public members: Record<string, RoomMember> = {}; // userId: RoomMember
|
||||
public events = new Map<string, Map<string, MatrixEvent>>(); // Map<eventType, Map<stateKey, MatrixEvent>>
|
||||
public paginationToken: string = null;
|
||||
public paginationToken: string | null = null;
|
||||
|
||||
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
private _liveBeaconIds: BeaconIdentifier[] = [];
|
||||
|
@ -57,6 +57,7 @@ import {
|
||||
ReceiptContent,
|
||||
synthesizeReceipt,
|
||||
} from "./read-receipt";
|
||||
import { Feature, ServerSupport } from "../feature";
|
||||
|
||||
// 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
|
||||
@ -96,6 +97,8 @@ export interface IRecommendedVersion {
|
||||
// price to pay to keep matrix-js-sdk responsive.
|
||||
const MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH = 30;
|
||||
|
||||
export type NotificationCount = Partial<Record<NotificationCountType, number>>;
|
||||
|
||||
export enum NotificationCountType {
|
||||
Highlight = "highlight",
|
||||
Total = "total",
|
||||
@ -125,6 +128,7 @@ export enum RoomEvent {
|
||||
OldStateUpdated = "Room.OldStateUpdated",
|
||||
CurrentStateUpdated = "Room.CurrentStateUpdated",
|
||||
HistoryImportedWithinTimeline = "Room.historyImportedWithinTimeline",
|
||||
UnreadNotifications = "Room.UnreadNotifications",
|
||||
}
|
||||
|
||||
type EmittedEvents = RoomEvent
|
||||
@ -162,6 +166,10 @@ export type RoomEventHandlerMap = {
|
||||
markerEvent: MatrixEvent,
|
||||
room: Room,
|
||||
) => void;
|
||||
[RoomEvent.UnreadNotifications]: (
|
||||
unreadNotifications: NotificationCount,
|
||||
threadId?: string,
|
||||
) => void;
|
||||
[RoomEvent.TimelineRefresh]: (room: Room, eventTimelineSet: EventTimelineSet) => void;
|
||||
[ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
|
||||
} & ThreadHandlerMap
|
||||
@ -183,7 +191,9 @@ export type 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 }
|
||||
private notificationCounts: Partial<Record<NotificationCountType, number>> = {};
|
||||
private notificationCounts: NotificationCount = {};
|
||||
private readonly threadNotifications = new Map<string, NotificationCount>();
|
||||
private roomThreadsNotificationType: NotificationCountType | null = null;
|
||||
private readonly timelineSets: EventTimelineSet[];
|
||||
public readonly threadsTimelineSets: EventTimelineSet[] = [];
|
||||
// any filtered timeline sets we're maintaining for this room
|
||||
@ -356,7 +366,7 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
private threadTimelineSetsPromise: Promise<[EventTimelineSet, EventTimelineSet]> | null = null;
|
||||
public async createThreadsTimelineSets(): Promise<[EventTimelineSet, EventTimelineSet]> {
|
||||
public async createThreadsTimelineSets(): Promise<[EventTimelineSet, EventTimelineSet] | null> {
|
||||
if (this.threadTimelineSetsPromise) {
|
||||
return this.threadTimelineSetsPromise;
|
||||
}
|
||||
@ -369,10 +379,13 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
|
||||
]);
|
||||
const timelineSets = await this.threadTimelineSetsPromise;
|
||||
this.threadsTimelineSets.push(...timelineSets);
|
||||
return timelineSets;
|
||||
} catch (e) {
|
||||
this.threadTimelineSetsPromise = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1176,8 +1189,98 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
|
||||
* @return {Number} The notification count, or undefined if there is no count
|
||||
* for this type.
|
||||
*/
|
||||
public getUnreadNotificationCount(type = NotificationCountType.Total): number | undefined {
|
||||
return this.notificationCounts[type];
|
||||
public getUnreadNotificationCount(type = NotificationCountType.Total): number {
|
||||
let count = this.notificationCounts[type] ?? 0;
|
||||
if (this.client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported) {
|
||||
for (const threadNotification of this.threadNotifications.values()) {
|
||||
count += threadNotification[type] ?? 0;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Get one of the notification counts for a thread
|
||||
* @param threadId the root event ID
|
||||
* @param type The type of notification count to get. default: 'total'
|
||||
* @returns The notification count, or undefined if there is no count
|
||||
* for this type.
|
||||
*/
|
||||
public getThreadUnreadNotificationCount(threadId: string, type = NotificationCountType.Total): number {
|
||||
return this.threadNotifications.get(threadId)?.[type] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Checks if the current room has unread thread notifications
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public hasThreadUnreadNotification(): boolean {
|
||||
for (const notification of this.threadNotifications.values()) {
|
||||
if ((notification.highlight ?? 0) > 0 || (notification.total ?? 0) > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Swet one of the notification count for a thread
|
||||
* @param threadId the root event ID
|
||||
* @param type The type of notification count to get. default: 'total'
|
||||
* @returns {void}
|
||||
*/
|
||||
public setThreadUnreadNotificationCount(threadId: string, type: NotificationCountType, count: number): void {
|
||||
const notification: NotificationCount = {
|
||||
highlight: this.threadNotifications.get(threadId)?.highlight,
|
||||
total: this.threadNotifications.get(threadId)?.total,
|
||||
...{
|
||||
[type]: count,
|
||||
},
|
||||
};
|
||||
|
||||
this.threadNotifications.set(threadId, notification);
|
||||
|
||||
// Remember what the global threads notification count type is
|
||||
// for all the threads in the room
|
||||
if (count > 0) {
|
||||
switch (this.roomThreadsNotificationType) {
|
||||
case NotificationCountType.Highlight:
|
||||
break;
|
||||
case NotificationCountType.Total:
|
||||
if (type === NotificationCountType.Highlight) {
|
||||
this.roomThreadsNotificationType = type;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this.roomThreadsNotificationType = type;
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(
|
||||
RoomEvent.UnreadNotifications,
|
||||
notification,
|
||||
threadId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* @returns the notification count type for all the threads in the room
|
||||
*/
|
||||
public getThreadsAggregateNotificationType(): NotificationCountType | null {
|
||||
return this.roomThreadsNotificationType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Resets the thread notifications for this room
|
||||
*/
|
||||
public resetThreadUnreadNotificationCount(): void {
|
||||
this.roomThreadsNotificationType = null;
|
||||
this.threadNotifications.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1187,6 +1290,7 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
|
||||
*/
|
||||
public setUnreadNotificationCount(type: NotificationCountType, count: number): void {
|
||||
this.notificationCounts[type] = count;
|
||||
this.emit(RoomEvent.UnreadNotifications, this.notificationCounts);
|
||||
}
|
||||
|
||||
public setSummary(summary: IRoomSummary): void {
|
||||
@ -1578,7 +1682,14 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
|
||||
|
||||
private async createThreadTimelineSet(filterType?: ThreadFilterType): Promise<EventTimelineSet> {
|
||||
let timelineSet: EventTimelineSet;
|
||||
if (Thread.hasServerSideSupport) {
|
||||
if (Thread.hasServerSideListSupport) {
|
||||
timelineSet =
|
||||
new EventTimelineSet(this, this.opts, undefined, undefined, Boolean(Thread.hasServerSideListSupport));
|
||||
this.reEmitter.reEmit(timelineSet, [
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
} else if (Thread.hasServerSideSupport) {
|
||||
const filter = await this.getThreadListFilter(filterType);
|
||||
|
||||
timelineSet = this.getOrCreateFilteredTimelineSet(
|
||||
@ -1611,13 +1722,42 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
|
||||
return timelineSet;
|
||||
}
|
||||
|
||||
public threadsReady = false;
|
||||
private threadsReady = false;
|
||||
|
||||
/**
|
||||
* Takes the given thread root events and creates threads for them.
|
||||
* @param events
|
||||
* @param toStartOfTimeline
|
||||
*/
|
||||
public processThreadRoots(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
for (const rootEvent of events) {
|
||||
EventTimeline.setEventMetadata(
|
||||
rootEvent,
|
||||
this.currentState,
|
||||
toStartOfTimeline,
|
||||
);
|
||||
if (!this.getThread(rootEvent.getId())) {
|
||||
this.createThread(rootEvent.getId(), rootEvent, [], toStartOfTimeline);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the bare minimum of room threads required for the thread list to work reliably.
|
||||
* With server support that means fetching one page.
|
||||
* Without server support that means fetching as much at once as the server allows us to.
|
||||
*/
|
||||
public async fetchRoomThreads(): Promise<void> {
|
||||
if (this.threadsReady || !this.client.supportsExperimentalThreads()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Thread.hasServerSideListSupport) {
|
||||
await Promise.all([
|
||||
this.fetchRoomThreadList(ThreadFilterType.All),
|
||||
this.fetchRoomThreadList(ThreadFilterType.My),
|
||||
]);
|
||||
} else {
|
||||
const allThreadsFilter = await this.getThreadListFilter();
|
||||
|
||||
const { chunk: events } = await this.client.createMessagesRequest(
|
||||
@ -1641,10 +1781,11 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
|
||||
* is only meant as a short term patch
|
||||
*/
|
||||
const threadAMetadata = eventA
|
||||
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
|
||||
.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||
const threadBMetadata = eventB
|
||||
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
|
||||
return threadAMetadata.latest_event.origin_server_ts - threadBMetadata.latest_event.origin_server_ts;
|
||||
.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||
return threadAMetadata.latest_event.origin_server_ts -
|
||||
threadBMetadata.latest_event.origin_server_ts;
|
||||
});
|
||||
|
||||
let latestMyThreadsRootEvent: MatrixEvent;
|
||||
@ -1657,7 +1798,7 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
|
||||
});
|
||||
|
||||
const threadRelationship = rootEvent
|
||||
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
|
||||
.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||
if (threadRelationship.current_user_participated) {
|
||||
this.threadsTimelineSets[1].addLiveEvent(rootEvent, {
|
||||
duplicateStrategy: DuplicateStrategy.Ignore,
|
||||
@ -1666,26 +1807,63 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
|
||||
});
|
||||
latestMyThreadsRootEvent = rootEvent;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.getThread(rootEvent.getId())) {
|
||||
this.createThread(rootEvent.getId(), rootEvent, [], true);
|
||||
}
|
||||
}
|
||||
this.processThreadRoots(threadRoots, true);
|
||||
|
||||
this.client.decryptEventIfNeeded(threadRoots[threadRoots.length -1]);
|
||||
if (latestMyThreadsRootEvent) {
|
||||
this.client.decryptEventIfNeeded(latestMyThreadsRootEvent);
|
||||
}
|
||||
|
||||
this.threadsReady = true;
|
||||
}
|
||||
|
||||
this.on(ThreadEvent.NewReply, this.onThreadNewReply);
|
||||
this.threadsReady = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single page of threadlist messages for the specific thread filter
|
||||
* @param filter
|
||||
* @private
|
||||
*/
|
||||
private async fetchRoomThreadList(filter?: ThreadFilterType): Promise<void> {
|
||||
const timelineSet = filter === ThreadFilterType.My
|
||||
? this.threadsTimelineSets[1]
|
||||
: this.threadsTimelineSets[0];
|
||||
|
||||
const { chunk: events, end } = await this.client.createThreadListMessagesRequest(
|
||||
this.roomId,
|
||||
null,
|
||||
undefined,
|
||||
Direction.Backward,
|
||||
timelineSet.getFilter(),
|
||||
);
|
||||
|
||||
timelineSet.getLiveTimeline().setPaginationToken(end, Direction.Backward);
|
||||
|
||||
if (!events.length) return;
|
||||
|
||||
const matrixEvents = events.map(this.client.getEventMapper());
|
||||
this.processThreadRoots(matrixEvents, true);
|
||||
const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
for (const rootEvent of matrixEvents) {
|
||||
timelineSet.addLiveEvent(rootEvent, {
|
||||
duplicateStrategy: DuplicateStrategy.Replace,
|
||||
fromCache: false,
|
||||
roomState,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onThreadNewReply(thread: Thread): void {
|
||||
const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
for (const timelineSet of this.threadsTimelineSets) {
|
||||
timelineSet.removeEvent(thread.id);
|
||||
timelineSet.addLiveEvent(thread.rootEvent);
|
||||
timelineSet.addLiveEvent(thread.rootEvent, {
|
||||
duplicateStrategy: DuplicateStrategy.Replace,
|
||||
fromCache: false,
|
||||
roomState,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1831,8 +2009,6 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
|
||||
this.lastThread = thread;
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.New, thread, toStartOfTimeline);
|
||||
|
||||
if (this.threadsReady) {
|
||||
this.threadsTimelineSets.forEach(timelineSet => {
|
||||
if (thread.rootEvent) {
|
||||
@ -1849,6 +2025,8 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
|
||||
});
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.New, thread, toStartOfTimeline);
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
|
@ -52,11 +52,28 @@ interface IThreadOpts {
|
||||
client: MatrixClient;
|
||||
}
|
||||
|
||||
export enum FeatureSupport {
|
||||
None = 0,
|
||||
Experimental = 1,
|
||||
Stable = 2
|
||||
}
|
||||
|
||||
export function determineFeatureSupport(stable: boolean, unstable: boolean): FeatureSupport {
|
||||
if (stable) {
|
||||
return FeatureSupport.Stable;
|
||||
} else if (unstable) {
|
||||
return FeatureSupport.Experimental;
|
||||
} else {
|
||||
return FeatureSupport.None;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
public static hasServerSideSupport: boolean;
|
||||
public static hasServerSideSupport = FeatureSupport.None;
|
||||
public static hasServerSideListSupport = FeatureSupport.None;
|
||||
|
||||
/**
|
||||
* A reference to all the events ID at the bottom of the threads
|
||||
@ -135,15 +152,23 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
|
||||
public static setServerSideSupport(hasServerSideSupport: boolean, useStable: boolean): void {
|
||||
Thread.hasServerSideSupport = hasServerSideSupport;
|
||||
if (!useStable) {
|
||||
public static setServerSideSupport(
|
||||
status: FeatureSupport,
|
||||
): void {
|
||||
Thread.hasServerSideSupport = status;
|
||||
if (status !== FeatureSupport.Stable) {
|
||||
FILTER_RELATED_BY_SENDERS.setPreferUnstable(true);
|
||||
FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true);
|
||||
THREAD_RELATION_TYPE.setPreferUnstable(true);
|
||||
}
|
||||
}
|
||||
|
||||
public static setServerSideListSupport(
|
||||
status: FeatureSupport,
|
||||
): void {
|
||||
Thread.hasServerSideListSupport = status;
|
||||
}
|
||||
|
||||
private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent) => {
|
||||
if (event?.isRelation(THREAD_RELATION_TYPE.name) &&
|
||||
this.room.eventShouldLiveIn(event).threadId === this.id &&
|
||||
@ -382,7 +407,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
return this.timelineSet.getLiveTimeline();
|
||||
}
|
||||
|
||||
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20, direction: Direction.Backward }): Promise<{
|
||||
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20, dir: Direction.Backward }): Promise<{
|
||||
originalEvent: MatrixEvent;
|
||||
events: MatrixEvent[];
|
||||
nextBatch?: string | null;
|
||||
@ -414,7 +439,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
return this.client.decryptEventIfNeeded(event);
|
||||
}));
|
||||
|
||||
const prependEvents = (opts.direction ?? Direction.Backward) === Direction.Backward;
|
||||
const prependEvents = (opts.dir ?? Direction.Backward) === Direction.Backward;
|
||||
|
||||
this.timelineSet.addEventsToTimeline(
|
||||
events,
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { escapeRegExp, globToRegexp, isNullOrUndefined } from "./utils";
|
||||
import { deepCompare, escapeRegExp, globToRegexp, isNullOrUndefined } from "./utils";
|
||||
import { logger } from './logger';
|
||||
import { MatrixClient } from "./client";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
@ -91,6 +91,20 @@ const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
|
||||
],
|
||||
actions: [],
|
||||
},
|
||||
{
|
||||
// For homeservers which don't support MSC3401 yet
|
||||
rule_id: ".org.matrix.msc3401.rule.room.call",
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: "type",
|
||||
pattern: "org.matrix.msc3401.call",
|
||||
},
|
||||
],
|
||||
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Sound, value: "default" }],
|
||||
},
|
||||
];
|
||||
|
||||
export interface IActionsObject {
|
||||
@ -424,7 +438,7 @@ export class PushProcessor {
|
||||
return {} as IActionsObject;
|
||||
}
|
||||
|
||||
const actionObj = PushProcessor.actionListToActionsObject(rule.actions);
|
||||
let actionObj = PushProcessor.actionListToActionsObject(rule.actions);
|
||||
|
||||
// Some actions are implicit in some situations: we add those here
|
||||
if (actionObj.tweaks.highlight === undefined) {
|
||||
@ -433,6 +447,30 @@ export class PushProcessor {
|
||||
actionObj.tweaks.highlight = (rule.kind == PushRuleKind.ContentSpecific);
|
||||
}
|
||||
|
||||
actionObj = this.performCustomEventHandling(ev, actionObj);
|
||||
|
||||
return actionObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Some events require custom event handling e.g. due to missing server support
|
||||
*/
|
||||
private performCustomEventHandling(ev: MatrixEvent, actionObj: IActionsObject): IActionsObject {
|
||||
switch (ev.getType()) {
|
||||
case "m.call":
|
||||
case "org.matrix.msc3401.call":
|
||||
// Since servers don't support properly sending push notification
|
||||
// about MSC3401 call events, we do the handling ourselves
|
||||
if (
|
||||
ev.getContent()["m.intent"] === "m.room"
|
||||
|| ("m.terminated" in ev.getContent())
|
||||
|| !("m.terminated" in ev.getPrevContent()) && !deepCompare(ev.getPrevContent(), {})
|
||||
) {
|
||||
actionObj.notify = false;
|
||||
actionObj.tweaks = {};
|
||||
}
|
||||
}
|
||||
|
||||
return actionObj;
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ import { logger } from './logger';
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { EventType } from "./@types/event";
|
||||
import { IDeferred } from "./utils";
|
||||
import { MatrixError } from "./http-api";
|
||||
import { ConnectionError, MatrixError } from "./http-api";
|
||||
import { ISendEventResponse } from "./@types/requests";
|
||||
|
||||
const DEBUG = false; // set true to enable console logging.
|
||||
@ -68,9 +68,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
|
||||
// client error; no amount of retrying with save you now.
|
||||
return -1;
|
||||
}
|
||||
// we ship with browser-request which returns { cors: rejected } when trying
|
||||
// with no connection, so if we match that, give up since they have no conn.
|
||||
if (err["cors"] === "rejected") {
|
||||
if (err instanceof ConnectionError) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
@ -733,7 +733,7 @@ export class SlidingSyncSdk {
|
||||
member._requestedProfileInfo = true;
|
||||
// try to get a cached copy first.
|
||||
const user = client.getUser(member.userId);
|
||||
let promise;
|
||||
let promise: ReturnType<MatrixClient["getProfileInfo"]>;
|
||||
if (user) {
|
||||
promise = Promise.resolve({
|
||||
avatar_url: user.avatarUrl,
|
||||
|
@ -15,10 +15,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from './logger';
|
||||
import { IAbortablePromise } from "./@types/partials";
|
||||
import { MatrixClient } from "./client";
|
||||
import { IRoomEvent, IStateEvent } from "./sync-accumulator";
|
||||
import { TypedEventEmitter } from "./models//typed-event-emitter";
|
||||
import { TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
import { sleep, IDeferred, defer } from "./utils";
|
||||
|
||||
// /sync requests allow you to set a timeout= but the request may continue
|
||||
@ -353,7 +352,8 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
||||
private desiredRoomSubscriptions = new Set<string>(); // the *desired* room subscriptions
|
||||
private confirmedRoomSubscriptions = new Set<string>();
|
||||
|
||||
private pendingReq?: IAbortablePromise<MSC3575SlidingSyncResponse>;
|
||||
private pendingReq?: Promise<MSC3575SlidingSyncResponse>;
|
||||
private abortController?: AbortController;
|
||||
|
||||
/**
|
||||
* Create a new sliding sync instance
|
||||
@ -700,7 +700,8 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
||||
...d,
|
||||
txnId: this.txnId,
|
||||
});
|
||||
this.pendingReq?.abort();
|
||||
this.abortController?.abort();
|
||||
this.abortController = new AbortController();
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
@ -728,7 +729,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
||||
this.txnIdDefers[i].reject(this.txnIdDefers[i].txnId);
|
||||
}
|
||||
this.txnIdDefers[txnIndex].resolve(txnId);
|
||||
// clear out settled promises, incuding the one we resolved.
|
||||
// clear out settled promises, including the one we resolved.
|
||||
this.txnIdDefers = this.txnIdDefers.slice(txnIndex+1);
|
||||
}
|
||||
|
||||
@ -737,17 +738,40 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
||||
*/
|
||||
public stop(): void {
|
||||
this.terminated = true;
|
||||
this.pendingReq?.abort();
|
||||
this.abortController?.abort();
|
||||
// remove all listeners so things can be GC'd
|
||||
this.removeAllListeners(SlidingSyncEvent.Lifecycle);
|
||||
this.removeAllListeners(SlidingSyncEvent.List);
|
||||
this.removeAllListeners(SlidingSyncEvent.RoomData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-setup this connection e.g in the event of an expired session.
|
||||
*/
|
||||
private resetup(): void {
|
||||
logger.warn("SlidingSync: resetting connection info");
|
||||
// any pending txn ID defers will be forgotten already by the server, so clear them out
|
||||
this.txnIdDefers.forEach((d) => {
|
||||
d.reject(d.txnId);
|
||||
});
|
||||
this.txnIdDefers = [];
|
||||
// resend sticky params and de-confirm all subscriptions
|
||||
this.lists.forEach((l) => {
|
||||
l.setModified(true);
|
||||
});
|
||||
this.confirmedRoomSubscriptions = new Set<string>(); // leave desired ones alone though!
|
||||
// reset the connection as we might be wedged
|
||||
this.needsResend = true;
|
||||
this.abortController?.abort();
|
||||
this.abortController = new AbortController();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start syncing with the server. Blocks until stopped.
|
||||
*/
|
||||
public async start() {
|
||||
this.abortController = new AbortController();
|
||||
|
||||
let currentPos: string;
|
||||
while (!this.terminated) {
|
||||
this.needsResend = false;
|
||||
@ -780,9 +804,8 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
||||
reqBody.txn_id = this.txnId;
|
||||
this.txnId = null;
|
||||
}
|
||||
this.pendingReq = this.client.slidingSync(reqBody, this.proxyBaseUrl);
|
||||
this.pendingReq = this.client.slidingSync(reqBody, this.proxyBaseUrl, this.abortController.signal);
|
||||
resp = await this.pendingReq;
|
||||
logger.debug(resp);
|
||||
currentPos = resp.pos;
|
||||
// update what we think we're subscribed to.
|
||||
for (const roomId of newSubscriptions) {
|
||||
@ -820,16 +843,19 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
||||
null,
|
||||
err,
|
||||
);
|
||||
await sleep(3000);
|
||||
} else if (this.needsResend || err === "aborted") {
|
||||
// don't sleep as we caused this error by abort()ing the request.
|
||||
// we check for 'aborted' because that's the error Jest returns and without it
|
||||
// we get warnings about not exiting fast enough.
|
||||
if (err.httpStatus === 400) {
|
||||
// session probably expired TODO: assign an errcode
|
||||
// so drop state and re-request
|
||||
this.resetup();
|
||||
currentPos = undefined;
|
||||
await sleep(50); // in case the 400 was for something else; don't tightloop
|
||||
continue;
|
||||
} else {
|
||||
logger.error(err);
|
||||
await sleep(3000);
|
||||
} // else fallthrough to generic error handling
|
||||
} else if (this.needsResend || err.name === "AbortError") {
|
||||
continue; // don't sleep as we caused this error by abort()ing the request.
|
||||
}
|
||||
logger.error(err);
|
||||
await sleep(5000);
|
||||
}
|
||||
if (!resp) {
|
||||
continue;
|
||||
|
@ -247,7 +247,7 @@ export interface IStore {
|
||||
/**
|
||||
* Fetches the oldest batch of to-device messages in the queue
|
||||
*/
|
||||
getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch>;
|
||||
getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null>;
|
||||
|
||||
/**
|
||||
* Removes a specific batch of to-device messages from the queue
|
||||
|
@ -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 {
|
||||
@ -33,7 +33,7 @@ export interface IIndexedDBBackend {
|
||||
getClientOptions(): Promise<IStartClientOpts>;
|
||||
storeClientOptions(options: IStartClientOpts): Promise<void>;
|
||||
saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise<void>;
|
||||
getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch>;
|
||||
getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null>;
|
||||
removeToDeviceBatch(id: number): Promise<void>;
|
||||
}
|
||||
|
||||
|
@ -18,14 +18,14 @@ 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;
|
||||
|
||||
function createDatabase(db: IDBDatabase): void {
|
||||
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"] });
|
||||
|
||||
@ -35,24 +35,19 @@ function createDatabase(db: IDBDatabase): void {
|
||||
|
||||
// Make /sync store (sync tokens, room data, etc), always clobber (const key).
|
||||
db.createObjectStore("sync", { keyPath: ["clobber"] });
|
||||
}
|
||||
|
||||
function upgradeSchemaV2(db: IDBDatabase): void {
|
||||
},
|
||||
(db) => {
|
||||
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 });
|
||||
}
|
||||
},
|
||||
(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";
|
||||
|
||||
@ -138,7 +138,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
return this.doCmd('saveToDeviceBatches', [batches]);
|
||||
}
|
||||
|
||||
public async getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch> {
|
||||
public async getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null> {
|
||||
return this.doCmd('getOldestToDeviceBatch');
|
||||
}
|
||||
|
||||
|
@ -357,7 +357,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
return this.backend.saveToDeviceBatches(batches);
|
||||
}
|
||||
|
||||
public getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch> {
|
||||
public getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null> {
|
||||
return this.backend.getOldestToDeviceBatch();
|
||||
}
|
||||
|
||||
|
@ -227,7 +227,7 @@ export class MemoryStore implements IStore {
|
||||
* @param {Filter} filter
|
||||
*/
|
||||
public storeFilter(filter: Filter): void {
|
||||
if (!filter) {
|
||||
if (!filter?.userId) {
|
||||
return;
|
||||
}
|
||||
if (!this.filters[filter.userId]) {
|
||||
|
@ -25,6 +25,7 @@ import { IContent, IUnsigned } from "./models/event";
|
||||
import { IRoomSummary } from "./models/room-summary";
|
||||
import { EventType } from "./@types/event";
|
||||
import { ReceiptType } from "./@types/read_receipts";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from './@types/sync';
|
||||
|
||||
interface IOpts {
|
||||
maxTimelineEntries?: number;
|
||||
@ -41,7 +42,7 @@ export interface IEphemeral {
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IUnreadNotificationCounts {
|
||||
interface UnreadNotificationCounts {
|
||||
highlight_count?: number;
|
||||
notification_count?: number;
|
||||
}
|
||||
@ -75,7 +76,9 @@ export interface IJoinedRoom {
|
||||
timeline: ITimeline;
|
||||
ephemeral: IEphemeral;
|
||||
account_data: IAccountData;
|
||||
unread_notifications: IUnreadNotificationCounts;
|
||||
unread_notifications: UnreadNotificationCounts;
|
||||
unread_thread_notifications?: Record<string, UnreadNotificationCounts>;
|
||||
"org.matrix.msc3773.unread_thread_notifications"?: Record<string, UnreadNotificationCounts>;
|
||||
}
|
||||
|
||||
export interface IStrippedState {
|
||||
@ -153,7 +156,8 @@ interface IRoom {
|
||||
}[];
|
||||
_summary: Partial<IRoomSummary>;
|
||||
_accountData: { [eventType: string]: IMinimalEvent };
|
||||
_unreadNotifications: Partial<IUnreadNotificationCounts>;
|
||||
_unreadNotifications: Partial<UnreadNotificationCounts>;
|
||||
_unreadThreadNotifications?: Record<string, Partial<UnreadNotificationCounts>>;
|
||||
_readReceipts: {
|
||||
[userId: string]: {
|
||||
data: IMinimalEvent;
|
||||
@ -362,6 +366,7 @@ export class SyncAccumulator {
|
||||
_timeline: [],
|
||||
_accountData: Object.create(null),
|
||||
_unreadNotifications: {},
|
||||
_unreadThreadNotifications: {},
|
||||
_summary: {},
|
||||
_readReceipts: {},
|
||||
};
|
||||
@ -379,6 +384,10 @@ export class SyncAccumulator {
|
||||
if (data.unread_notifications) {
|
||||
currentData._unreadNotifications = data.unread_notifications;
|
||||
}
|
||||
currentData._unreadThreadNotifications = data[UNREAD_THREAD_NOTIFICATIONS.stable]
|
||||
?? data[UNREAD_THREAD_NOTIFICATIONS.unstable]
|
||||
?? undefined;
|
||||
|
||||
if (data.summary) {
|
||||
const HEROES_KEY = "m.heroes";
|
||||
const INVITED_COUNT_KEY = "m.invited_member_count";
|
||||
@ -537,6 +546,7 @@ export class SyncAccumulator {
|
||||
prev_batch: null,
|
||||
},
|
||||
unread_notifications: roomData._unreadNotifications,
|
||||
unread_thread_notifications: roomData._unreadThreadNotifications,
|
||||
summary: roomData._summary as IRoomSummary,
|
||||
};
|
||||
// Add account data
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user