Compare commits

...

31 Commits

Author SHA1 Message Date
RiotRobot 528e9343ae v19.4.0-rc.1 2022-08-23 10:53:02 +01:00
RiotRobot 6571b6a1ab Prepare changelog for v19.4.0-rc.1 2022-08-23 10:53:01 +01:00
kegsay 1df329df7c Merge pull request #2610 from matrix-org/kegan/ss-bugfix
sliding sync: handle lone DELETE and INSERT operations
2022-08-23 08:42:34 +01:00
kegsay 760eeaeed7 Merge pull request #2612 from matrix-org/kegan/ss-tags
Add tags and not_tags to the list of valid sliding sync filters
2022-08-23 08:42:14 +01:00
renovate[bot] 438fc70615 Update all (#2614)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-23 08:42:56 +02:00
Kegan Dougal de3b3960d2 Add tags and not_tags to the list of valid sliding sync filters 2022-08-22 18:20:51 +01:00
Robin b265d795a4 Re-emit room state events on rooms (#2607)
* Re-emit room state events on rooms

This also fixes some potential memory leaks and abuse of
removeAllListeners in sync.ts.

* Remove some stray whitespace

* Deduplicate some code to appease SonarCloud

* Name helper function more explicitly
2022-08-22 17:04:32 +02:00
Michael Telatynski eb79f6246d Add ability to override built in room name generator for an i18n'able one (#2609)
* Add ability to override built in room name generator for an i18n'able one

* Add tests
2022-08-22 14:39:04 +01:00
Kegan Dougal 37f8f736e0 sliding sync: handle lone DELETE and INSERT operations
If you leave a room you can get a lone DELETE op.
If you join a room you can get a lone INSERT op.

Up until now, we've assumed these operations happen at the ends
of the list (e.g [0] or [length-1]) which is not guaranteed as it
depends on the sort order (e.g sort alphabetically and join a room
called 'D'). In this scenario, the indexes would not be tracked
correctly. Fixed with integration tests.
2022-08-22 14:37:10 +01:00
renovate[bot] 4b1a443f90 Lock file maintenance (#2608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-22 09:30:03 +01:00
Šimon Brandner 3ae974e23e Remove duplicate log of answering a call (#2595) 2022-08-17 10:23:06 +00:00
RiotRobot 566b4ba56c Resetting package fields for development 2022-08-16 15:25:58 +01:00
RiotRobot 13291f33d2 Merge branch 'master' into develop 2022-08-16 15:25:57 +01:00
Kerry be3e731499 Fix: Handle parsing of a beacon info event without asset (#2591)
* test case

* handle missing beacon info asset

* default beacon info asset type to self

* make BeaconLocationState.assetType optional
2022-08-16 15:33:19 +02:00
3nprob 3f6f5b69c7 Improve test coverage and modernize style for interactive-auth (#2574)
* style: address no-mixed-operators errors,minor style improvements

* test: Fix async interactive-auth tests, add test case

* tests: Fix incorrectly stringified mock response

* pushprocessor: style update

* use async primitives in interactive-auth-spec

* lint

* fixup: remove duplicate test

* add test case for no-flow-with-session for interactive-auth

* interactive-auth: handle non-existing error.data

* async test fix

* test: add dummyauth test

* add testing for errcode

* Revert "pushprocessor: style update"

This reverts commit 3ed0fdfb73ae55b725aa7c74d9cab35fb96c9178.

* add testcase for missing error data

* test: move sessionId assignment

* Add tests to improve coverage for interactive-auth

* pushprocessor: style update
2022-08-11 15:29:53 +01:00
Šimon Brandner 478270b225 Fix finding event read up to if stable private read receipts is missing (#2585)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-08-11 13:30:51 +01:00
kegsay 9eb72908a7 Merge pull request #2583 from matrix-org/kegan/sync-v3
sliding sync bugfix: ensure history is treated as history and not live events
2022-08-10 20:00:53 +01:00
Kegan Dougal 2728d74771 Review comments 2022-08-10 19:53:36 +01:00
Kegan Dougal edcef9364c Only add events if there are some; set the pagination token for faster scrollback 2022-08-10 12:43:47 +01:00
Kegan Dougal 1635ac9971 sliding sync bugfix: ensure history is treated as history and not live events
with tests
2022-08-10 12:32:28 +01:00
kegsay 8f13df2dd9 Merge pull request #2567 from matrix-org/kegan/sync-v3
Add txn_id support to sliding sync
2022-08-10 12:16:47 +01:00
Kegan Dougal fa9f078a75 Review comments 2022-08-10 11:53:13 +01:00
Michael Telatynski 055af933cc Update backport.yml 2022-08-10 11:41:44 +01:00
Michael Telatynski 1ba2730e25 Update backport.yml (#2582) 2022-08-10 11:33:51 +01:00
Michael Telatynski e29e0d15a5 Set up basic Backporting action (#2580)
* Create backport.yml

* Update backport.yml
2022-08-10 09:21:39 +01:00
Michael Telatynski 9ee94c9902 Update jsdoc.yml (#2577) 2022-08-10 09:04:38 +01:00
renovate[bot] 1645867ea6 Update babel monorepo to v7.18.10 (#2578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-10 07:27:31 +01:00
renovate[bot] 24d4181a08 Update typescript-eslint monorepo to v5.33.0 (#2579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-09 15:42:40 -04:00
Kegan Dougal 2596999cb8 Review comments 2022-08-08 14:26:24 +01:00
Kegan Dougal a3248c0aa1 Merge branch 'develop' into kegan/sync-v3 2022-08-08 12:51:03 +01:00
Kegan Dougal e05f9b5815 Add txn_id support to sliding sync
This allows clients to know when a request has been applied
on the server. This allows us to change `resend(): void` to
`resend(): Promise<string>` which resolves/rejects with the
transaction ID when it has been applied/not.
2022-08-05 17:28:02 +01:00
21 changed files with 1540 additions and 594 deletions
+4
View File
@@ -57,6 +57,10 @@ module.exports = {
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
// The non-TypeScript rule produces false positives
"func-call-spacing": "off",
"@typescript-eslint/func-call-spacing": ["error"],
"quotes": "off",
// We use a `logger` intermediary module
"no-console": "error",
+30
View File
@@ -0,0 +1,30 @@
name: Backport
on:
pull_request_target:
types:
- closed
- labeled
branches:
- develop
jobs:
backport:
name: Backport
runs-on: ubuntu-latest
# Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: >
github.event.pull_request.merged
&& (
github.event.action == 'closed'
|| (
github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport')
)
)
steps:
- uses: tibdex/backport@v2
with:
labels_template: "<%= JSON.stringify(labels) %>"
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+14
View File
@@ -1,3 +1,17 @@
Changes in [19.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.4.0-rc.1) (2022-08-23)
============================================================================================================
## ✨ Features
* Re-emit room state events on rooms ([\#2607](https://github.com/matrix-org/matrix-js-sdk/pull/2607)).
* Add ability to override built in room name generator for an i18n'able one ([\#2609](https://github.com/matrix-org/matrix-js-sdk/pull/2609)).
* Add txn_id support to sliding sync ([\#2567](https://github.com/matrix-org/matrix-js-sdk/pull/2567)).
## 🐛 Bug Fixes
* Fixed a sliding sync bug which could cause the `roomIndexToRoomId` map to be incorrect when a new room is added in the middle of the list or when an existing room is deleted from the middle of the list. ([\#2610](https://github.com/matrix-org/matrix-js-sdk/pull/2610)).
* Fix: Handle parsing of a beacon info event without asset ([\#2591](https://github.com/matrix-org/matrix-js-sdk/pull/2591)). Fixes vector-im/element-web#23078.
* Fix finding event read up to if stable private read receipts is missing ([\#2585](https://github.com/matrix-org/matrix-js-sdk/pull/2585)). Fixes vector-im/element-web#23027.
* fixed a sliding sync issue where history could be interpreted as live events. ([\#2583](https://github.com/matrix-org/matrix-js-sdk/pull/2583)).
Changes in [19.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.3.0) (2022-08-16)
==================================================================================================
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "19.3.0",
"version": "19.4.0-rc.1",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=12.9.0"
@@ -92,10 +92,10 @@
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"eslint": "8.20.0",
"eslint": "8.22.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-matrix-org": "^0.5.0",
"eslint-plugin-matrix-org": "^0.6.0",
"exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0",
"jest": "^28.0.0",
+31
View File
@@ -28,6 +28,7 @@ import {
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
import { SyncState } from "../../src/sync";
import { IStoredClientOpts } from "../../src/client";
import { logger } from "../../src/logger";
describe("SlidingSyncSdk", () => {
let client: MatrixClient = null;
@@ -372,6 +373,36 @@ describe("SlidingSyncSdk", () => {
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
).toEqual(1);
});
// Regression test for a bug which caused the timeline entries to be out-of-order
// when the same room appears twice with different timeline limits. E.g appears in
// the list with timeline_limit:1 then appears again as a room subscription with
// timeline_limit:50
it("can return history with a larger timeline_limit", async () => {
const timeline = data[roomA].timeline;
const oldTimeline = [
mkOwnEvent(EventType.RoomMessage, { body: "old event A" }),
mkOwnEvent(EventType.RoomMessage, { body: "old event B" }),
mkOwnEvent(EventType.RoomMessage, { body: "old event C" }),
...timeline,
];
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);
expect(gotRoom).toBeDefined();
logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body)));
logger.log("got:", gotRoom.getLiveTimeline().getEvents().map(
(e) => (e.getType() + " : " + e.getContent().body)),
);
// we expect the timeline now to be oldTimeline (so the old events are in fact old)
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), oldTimeline);
});
});
});
});
+366
View File
@@ -558,6 +558,372 @@ describe("SlidingSync", () => {
await httpBackend.flushAllExpected();
await responseProcessed;
await listPromise;
});
// this refers to a set of operations where the end result is no change.
it("should handle net zero operations correctly", async () => {
const indexToRoomId = {
0: roomB,
1: roomC,
};
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId);
httpBackend.when("POST", syncUrl).respond(200, {
pos: "f",
// currently the list is [B,C] so we will insert D then immediately delete it
lists: [{
count: 500,
ops: [
{
op: "DELETE", index: 2,
},
{
op: "INSERT", index: 0, room_id: roomA,
},
{
op: "DELETE", index: 0,
},
],
},
{
count: 50,
}],
});
const listPromise = listenUntil(slidingSync, "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual(indexToRoomId);
return true;
});
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await responseProcessed;
await listPromise;
});
it("should handle deletions correctly", async () => {
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
0: roomB,
1: roomC,
});
httpBackend.when("POST", syncUrl).respond(200, {
pos: "g",
lists: [{
count: 499,
ops: [
{
op: "DELETE", index: 0,
},
],
},
{
count: 50,
}],
});
const listPromise = listenUntil(slidingSync, "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
expect(joinedCount).toEqual(499);
expect(roomIndexToRoomId).toEqual({
0: roomC,
});
return true;
});
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await responseProcessed;
await listPromise;
});
it("should handle insertions correctly", async () => {
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
0: roomC,
});
httpBackend.when("POST", syncUrl).respond(200, {
pos: "h",
lists: [{
count: 500,
ops: [
{
op: "INSERT", index: 1, room_id: roomA,
},
],
},
{
count: 50,
}],
});
let listPromise = listenUntil(slidingSync, "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual({
0: roomC,
1: roomA,
});
return true;
});
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await responseProcessed;
await listPromise;
httpBackend.when("POST", syncUrl).respond(200, {
pos: "h",
lists: [{
count: 501,
ops: [
{
op: "INSERT", index: 1, room_id: roomB,
},
],
},
{
count: 50,
}],
});
listPromise = listenUntil(slidingSync, "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
expect(joinedCount).toEqual(501);
expect(roomIndexToRoomId).toEqual({
0: roomC,
1: roomB,
2: roomA,
});
return true;
});
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await responseProcessed;
await listPromise;
slidingSync.stop();
});
});
describe("transaction IDs", () => {
beforeAll(setupClient);
afterAll(teardownClient);
const roomId = "!foo:bar";
let slidingSync: SlidingSync;
// really this applies to them all but it's easier to just test one
it("should resolve modifyRoomSubscriptions after SlidingSync.start() is called", async () => {
const roomSubInfo = {
timeline_limit: 1,
required_state: [
["m.room.name", ""],
],
};
// add the subscription
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) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeTruthy();
expect(body.room_subscriptions[roomId]).toEqual(roomSubInfo);
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
}).respond(200, function() {
return {
pos: "aaa",
txn_id: txnId,
lists: [],
extensions: {},
rooms: {
[roomId]: {
name: "foo bar",
required_state: [],
timeline: [],
},
},
};
});
slidingSync.start();
await httpBackend.flushAllExpected();
await subscribePromise;
});
it("should resolve setList during a connection", async () => {
const newList = {
ranges: [[0, 20]],
};
const promise = slidingSync.setList(0, newList);
let txnId;
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(newList);
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
}).respond(200, function() {
return {
pos: "bbb",
txn_id: txnId,
lists: [{ count: 5 }],
extensions: {},
};
});
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) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
expect(body.lists[0]).toEqual({
ranges: [[20, 40]],
});
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
}).respond(200, function() {
return {
pos: "ccc",
txn_id: txnId,
lists: [{ count: 5 }],
extensions: {},
};
});
await httpBackend.flushAllExpected();
await promise;
expect(txnId).toBeDefined();
});
it("should resolve modifyRoomSubscriptionInfo during a connection", async () => {
const promise = slidingSync.modifyRoomSubscriptionInfo({
timeline_limit: 99,
});
let txnId;
httpBackend.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeTruthy();
expect(body.room_subscriptions[roomId]).toEqual({
timeline_limit: 99,
});
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
}).respond(200, function() {
return {
pos: "ddd",
txn_id: txnId,
extensions: {},
};
});
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 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();
const failPromise2 = slidingSync.setListRanges(0, [[60, 70]]);
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.
expect(failPromise).rejects.toEqual(gotTxnIds[0]);
expect(failPromise2).rejects.toEqual(gotTxnIds[1]);
const okPromise = slidingSync.setListRanges(0, [[0, 20]]);
let txnId;
httpBackend.when("POST", syncUrl).check((req) => {
txnId = req.data.txn_id;
}).respond(200, () => {
// include the txn_id, earlier requests should now be reject()ed.
return {
pos: "g",
txn_id: txnId,
};
});
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 pushTxn = function(req) {
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();
const B = slidingSync.setListRanges(0, [[60, 70]]);
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.
expect(A).rejects.toEqual(gotTxnIds[0]);
const C = slidingSync.setListRanges(0, [[0, 20]]);
let pendingC = true;
C.finally(() => {
pendingC = false;
});
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();
// A is rejected, see above
expect(B).resolves.toEqual(gotTxnIds[1]); // B is resolved
expect(pendingC).toBe(true); // C is pending still
});
it("should do nothing for unknown txn_ids", async () => {
const promise = slidingSync.setListRanges(0, [[20, 40]]);
let pending = true;
promise.finally(() => {
pending = false;
});
let txnId;
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: [[20, 40]],
});
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
}).respond(200, function() {
return {
pos: "ccc",
txn_id: "bogus transaction id",
lists: [{ count: 5 }],
extensions: {},
};
});
await httpBackend.flushAllExpected();
expect(txnId).toBeDefined();
expect(pending).toBe(true);
slidingSync.stop();
});
});
+17 -18
View File
@@ -153,27 +153,26 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_PROMPT when .well-known returns not-JSON", function() {
it("should return FAIL_PROMPT when .well-known returns not-JSON", async () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc");
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc", true);
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
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);
}),
AutoDiscovery.findClientConfig("example.org").then(
expect(expected).toEqual,
),
]);
});
+293 -25
View File
@@ -32,8 +32,8 @@ class FakeClient {
const getFakeClient = (): MatrixClient => new FakeClient() as unknown as MatrixClient;
describe("InteractiveAuth", function() {
it("should start an auth stage and complete it", function() {
describe("InteractiveAuth", () => {
it("should start an auth stage and complete it", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
@@ -59,7 +59,7 @@ describe("InteractiveAuth", function() {
});
// first we expect a call here
stateUpdated.mockImplementation(function(stage) {
stateUpdated.mockImplementation((stage) => {
logger.log('aaaa');
expect(stage).toEqual(AuthType.Password);
ia.submitAuthDict({
@@ -69,23 +69,130 @@ describe("InteractiveAuth", function() {
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(function(authData) {
doRequest.mockImplementation(async (authData) => {
logger.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return Promise.resolve(requestRes);
return requestRes;
});
return ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should make a request if no authdata is provided", function() {
it("should handle auth errcode presence ", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
authData: {
session: "sessionId",
flows: [
{ stages: [AuthType.Password] },
],
errcode: "MockError0",
params: {
[AuthType.Password]: { param: "aa" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
param: "aa",
});
// first we expect a call here
stateUpdated.mockImplementation((stage) => {
logger.log('aaaa');
expect(stage).toEqual(AuthType.Password);
ia.submitAuthDict({
type: AuthType.Password,
});
});
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(async (authData) => {
logger.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should handle set emailSid for email flow", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
doRequest,
stateUpdated,
requestEmailToken,
matrixClient: getFakeClient(),
emailSid: 'myEmailSid',
authData: {
session: "sessionId",
flows: [
{ stages: [AuthType.Email, AuthType.Password] },
],
params: {
[AuthType.Email]: { param: "aa" },
[AuthType.Password]: { param: "bb" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Email)).toEqual({
param: "aa",
});
// first we expect a call here
stateUpdated.mockImplementation((stage) => {
logger.log('husky');
expect(stage).toEqual(AuthType.Email);
ia.submitAuthDict({
type: AuthType.Email,
});
});
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(async (authData) => {
logger.log('barfoo');
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Email,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
expect(requestEmailToken).toBeCalledTimes(0);
expect(ia.getEmailSid()).toBe("myEmailSid");
});
it("should make a request if no authdata is provided", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
@@ -101,7 +208,7 @@ describe("InteractiveAuth", function() {
expect(ia.getStageParams(AuthType.Password)).toBe(undefined);
// first we expect a call to doRequest
doRequest.mockImplementation(function(authData) {
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
@@ -119,7 +226,7 @@ describe("InteractiveAuth", function() {
// .. which should be followed by a call to stateUpdated
const requestRes = { "a": "b" };
stateUpdated.mockImplementation(function(stage) {
stateUpdated.mockImplementation((stage) => {
expect(stage).toEqual(AuthType.Password);
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
@@ -127,13 +234,13 @@ describe("InteractiveAuth", function() {
});
// submitAuthDict should trigger another call to doRequest
doRequest.mockImplementation(function(authData) {
doRequest.mockImplementation(async (authData) => {
logger.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return Promise.resolve(requestRes);
return requestRes;
});
ia.submitAuthDict({
@@ -141,14 +248,76 @@ describe("InteractiveAuth", function() {
});
});
return ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1);
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should start an auth stage and reject if no auth flow", function() {
it("should make a request if authdata is null", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
authData: null,
matrixClient: getFakeClient(),
stateUpdated,
doRequest,
requestEmailToken,
});
expect(ia.getSessionId()).toBe(undefined);
expect(ia.getStageParams(AuthType.Password)).toBe(undefined);
// first we expect a call to doRequest
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
session: "sessionId",
flows: [
{ stages: [AuthType.Password] },
],
params: {
[AuthType.Password]: { param: "aa" },
},
});
err.httpStatus = 401;
throw err;
});
// .. which should be followed by a call to stateUpdated
const requestRes = { "a": "b" };
stateUpdated.mockImplementation((stage) => {
expect(stage).toEqual(AuthType.Password);
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
param: "aa",
});
// submitAuthDict should trigger another call to doRequest
doRequest.mockImplementation(async (authData) => {
logger.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return requestRes;
});
ia.submitAuthDict({
type: AuthType.Password,
});
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should start an auth stage and reject if no auth flow", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
@@ -160,7 +329,7 @@ describe("InteractiveAuth", function() {
requestEmailToken,
});
doRequest.mockImplementation(function(authData) {
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
@@ -174,9 +343,108 @@ describe("InteractiveAuth", function() {
throw err;
});
return ia.attemptAuth().catch(function(error) {
expect(error.message).toBe('No appropriate authentication flow found');
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new Error('No appropriate authentication flow found'),
);
});
it("should start an auth stage and reject if no auth flow but has session", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
authData: {
},
sessionId: "sessionId",
});
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId
const err = new MatrixError({
session: "sessionId",
flows: [],
params: {
[AuthType.Password]: { param: "aa" },
},
error: "Mock Error 1",
errcode: "MOCKERR1",
});
err.httpStatus = 401;
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new Error('No appropriate authentication flow found'),
);
});
it("should handle unexpected error types without data propery set", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
authData: {
session: "sessionId",
},
});
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId
const err = new Error('myerror');
(err as any).httpStatus = 401;
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new Error("myerror"),
);
});
it("should allow dummy auth", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
authData: {
session: 'sessionId',
flows: [
{ stages: [AuthType.Dummy] },
],
params: {},
},
});
const requestRes = { "a": "b" };
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Dummy,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(0);
});
describe("requestEmailToken", () => {
@@ -247,7 +515,7 @@ describe("InteractiveAuth", function() {
doRequest, stateUpdated, requestEmailToken,
});
expect(async () => await ia.requestEmailToken()).rejects.toThrowError("unspecific network error");
await expect(ia.requestEmailToken.bind(ia)).rejects.toThrowError("unspecific network error");
});
it("only starts one request at a time", async () => {
+12
View File
@@ -2545,4 +2545,16 @@ describe("Room", function() {
});
});
});
describe("roomNameGenerator", () => {
const client = new TestClient(userA).client;
client.roomNameGenerator = jest.fn().mockReturnValue(null);
const room = new Room(roomId, client, userA);
it("should call fn when recalculating room name", () => {
(client.roomNameGenerator as jest.Mock).mockClear();
room.recalculate();
expect(client.roomNameGenerator).toHaveBeenCalled();
});
});
});
+29
View File
@@ -24,7 +24,16 @@ import { ListenerMap, TypedEventEmitter } from "./models/typed-event-emitter";
export class ReEmitter {
constructor(private readonly target: EventEmitter) {}
// Map from emitter to event name to re-emitter
private reEmitters = new Map<EventEmitter, Map<string, (...args: any[]) => void>>();
public reEmit(source: EventEmitter, eventNames: string[]): void {
let reEmittersByEvent = this.reEmitters.get(source);
if (!reEmittersByEvent) {
reEmittersByEvent = new Map();
this.reEmitters.set(source, reEmittersByEvent);
}
for (const eventName of eventNames) {
// We include the source as the last argument for event handlers which may need it,
// such as read receipt listeners on the client class which won't have the context
@@ -44,8 +53,21 @@ export class ReEmitter {
this.target.emit(eventName, ...args, source);
};
source.on(eventName, forSource);
reEmittersByEvent.set(eventName, forSource);
}
}
public stopReEmitting(source: EventEmitter, eventNames: string[]): void {
const reEmittersByEvent = this.reEmitters.get(source);
if (!reEmittersByEvent) return; // We were never re-emitting these events in the first place
for (const eventName of eventNames) {
source.off(eventName, reEmittersByEvent.get(eventName));
reEmittersByEvent.delete(eventName);
}
if (reEmittersByEvent.size === 0) this.reEmitters.delete(source);
}
}
export class TypedReEmitter<
@@ -62,4 +84,11 @@ export class TypedReEmitter<
): void {
super.reEmit(source, eventNames);
}
public stopReEmitting<ReEmittedEvents extends string, T extends Events & ReEmittedEvents>(
source: TypedEventEmitter<ReEmittedEvents, any>,
eventNames: T[],
): void {
super.stopReEmitting(source, eventNames);
}
}
+28 -24
View File
@@ -17,6 +17,8 @@ limitations under the License.
/** @module auto-discovery */
import { ServerResponse } from "http";
import { IClientWellKnown, IWellKnownConfig } from "./client";
import { logger } from './logger';
@@ -409,39 +411,41 @@ export class AutoDiscovery {
* @return {Promise<object>} Resolves to the returned state.
* @private
*/
private static fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
return new Promise(function(resolve) {
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: url, timeout: 5000 },
(err, response, body) => {
if (err || response &&
(response.statusCode < 200 || response.statusCode >= 300)
) {
let action = AutoDiscoveryAction.FAIL_PROMPT;
let reason = (err ? err.message : null) || "General failure";
if (response && response.statusCode === 404) {
action = AutoDiscoveryAction.IGNORE;
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
}
resolve({ raw: {}, action: action, reason: reason, error: err });
return;
{ 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,
action: AutoDiscoveryAction.IGNORE,
reason: AutoDiscovery.ERROR_MISSING_WELLKNOWN,
} : {
...result,
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: error?.message || "General failure",
});
}
try {
resolve({ raw: JSON.parse(body), action: AutoDiscoveryAction.SUCCESS });
} catch (e) {
let reason = AutoDiscovery.ERROR_INVALID;
if (e.name === "SyntaxError") {
reason = AutoDiscovery.ERROR_INVALID_JSON;
}
resolve({
return resolve({
raw: JSON.parse(body),
action: AutoDiscoveryAction.SUCCESS,
});
} catch (err) {
return resolve({
error: err,
raw: {},
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: reason,
error: e,
reason: err?.name === "SyntaxError"
? AutoDiscovery.ERROR_INVALID_JSON
: AutoDiscovery.ERROR_INVALID,
});
}
},
+9 -1
View File
@@ -137,7 +137,7 @@ import { VerificationRequest } from "./crypto/verification/request/VerificationR
import { VerificationBase as Verification } from "./crypto/verification/Base";
import * as ContentHelpers from "./content-helpers";
import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning";
import { Room } from "./models/room";
import { Room, RoomNameState } from "./models/room";
import {
IAddThreePidOnlyBody,
IBindThreePidBody,
@@ -344,6 +344,12 @@ export interface ICreateClientOpts {
fallbackICEServerAllowed?: boolean;
cryptoCallbacks?: ICryptoCallbacks;
/**
* Method to generate room names for empty rooms and rooms names based on membership.
* Defaults to a built-in English handler with basic pluralisation.
*/
roomNameGenerator?: (roomId: string, state: RoomNameState) => string | null;
}
export interface IMatrixClientCreateOpts extends ICreateClientOpts {
@@ -918,6 +924,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
protected fallbackICEServerAllowed = false;
protected roomList: RoomList;
protected syncApi: SlidingSyncSdk | SyncApi;
public roomNameGenerator?: ICreateClientOpts["roomNameGenerator"];
public pushRules: IPushRules;
protected syncLeftRoomsPromise: Promise<Room[]>;
protected syncedLeftRooms = false;
@@ -1041,6 +1048,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// we still want to know which rooms are encrypted even if crypto is disabled:
// we don't want to start sending unencrypted events to them.
this.roomList = new RoomList(this.cryptoStore);
this.roomNameGenerator = opts.roomNameGenerator;
this.toDeviceMessageQueue = new ToDeviceMessageQueue(this);
+13 -15
View File
@@ -250,12 +250,9 @@ export class InteractiveAuth {
if (!this.data?.flows) {
this.busyChangedCallback?.(true);
// use the existing sessionId, if one is present.
let auth = null;
if (this.data.session) {
auth = {
session: this.data.session,
};
}
const auth = this.data.session
? { session: this.data.session }
: null;
this.doRequest(auth).finally(() => {
this.busyChangedCallback?.(false);
});
@@ -312,7 +309,7 @@ export class InteractiveAuth {
* @return {string} session id
*/
public getSessionId(): string {
return this.data ? this.data.session : undefined;
return this.data?.session;
}
/**
@@ -477,6 +474,9 @@ export class InteractiveAuth {
logger.log("Background poll request failed doing UI auth: ignoring", error);
}
}
if (!error.data) {
error.data = {};
}
// if the error didn't come with flows, completed flows or session ID,
// copy over the ones we have. Synapse sometimes sends responses without
// any UI auth data (eg. when polling for email validation, if the email
@@ -539,19 +539,17 @@ export class InteractiveAuth {
return;
}
if (this.data && this.data.errcode || this.data.error) {
if (this.data?.errcode || this.data?.error) {
this.stateUpdatedCallback(nextStage, {
errcode: this.data.errcode || "",
error: this.data.error || "",
errcode: this.data?.errcode || "",
error: this.data?.error || "",
});
return;
}
const stageStatus: IStageStatus = {};
if (nextStage == EMAIL_STAGE_TYPE) {
stageStatus.emailSid = this.emailSid;
}
this.stateUpdatedCallback(nextStage, stageStatus);
this.stateUpdatedCallback(nextStage, nextStage === EMAIL_STAGE_TYPE
? { emailSid: this.emailSid }
: {});
}
/**
+144 -25
View File
@@ -1,5 +1,5 @@
/*
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2015 - 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.
@@ -37,7 +37,8 @@ import {
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
import { Filter, IFilterDefinition } from "../filter";
import { RoomState } from "./room-state";
import { RoomState, RoomStateEvent, RoomStateEventHandlerMap } from "./room-state";
import { BeaconEvent, BeaconEventHandlerMap } from "./beacon";
import {
Thread,
ThreadEvent,
@@ -172,16 +173,19 @@ export enum RoomEvent {
}
type EmittedEvents = RoomEvent
| RoomStateEvent.Events
| RoomStateEvent.Members
| RoomStateEvent.NewMember
| RoomStateEvent.Update
| RoomStateEvent.Marker
| ThreadEvent.New
| ThreadEvent.Update
| ThreadEvent.NewReply
| RoomEvent.Timeline
| RoomEvent.TimelineReset
| RoomEvent.TimelineRefresh
| RoomEvent.HistoryImportedWithinTimeline
| RoomEvent.OldStateUpdated
| RoomEvent.CurrentStateUpdated
| MatrixEventEvent.BeforeRedaction;
| MatrixEventEvent.BeforeRedaction
| BeaconEvent.New
| BeaconEvent.Update
| BeaconEvent.Destroy
| BeaconEvent.LivenessChange;
export type RoomEventHandlerMap = {
[RoomEvent.MyMembership]: (room: Room, membership: string, prevMembership?: string) => void;
@@ -205,7 +209,21 @@ export type RoomEventHandlerMap = {
) => void;
[RoomEvent.TimelineRefresh]: (room: Room, eventTimelineSet: EventTimelineSet) => void;
[ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
} & ThreadHandlerMap & MatrixEventHandlerMap;
} & ThreadHandlerMap
& MatrixEventHandlerMap
& Pick<
RoomStateEventHandlerMap,
RoomStateEvent.Events
| RoomStateEvent.Members
| RoomStateEvent.NewMember
| RoomStateEvent.Update
| RoomStateEvent.Marker
| BeaconEvent.New
>
& Pick<
BeaconEventHandlerMap,
BeaconEvent.Update | BeaconEvent.Destroy | BeaconEvent.LivenessChange
>;
export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap> {
public readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
@@ -1068,6 +1086,32 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
if (previousCurrentState !== this.currentState) {
this.emit(RoomEvent.CurrentStateUpdated, this, previousCurrentState, this.currentState);
// Re-emit various events on the current room state
// TODO: If currentState really only exists for backwards
// compatibility, shouldn't we be doing this some other way?
this.reEmitter.stopReEmitting(previousCurrentState, [
RoomStateEvent.Events,
RoomStateEvent.Members,
RoomStateEvent.NewMember,
RoomStateEvent.Update,
RoomStateEvent.Marker,
BeaconEvent.New,
BeaconEvent.Update,
BeaconEvent.Destroy,
BeaconEvent.LivenessChange,
]);
this.reEmitter.reEmit(this.currentState, [
RoomStateEvent.Events,
RoomStateEvent.Members,
RoomStateEvent.NewMember,
RoomStateEvent.Update,
RoomStateEvent.Marker,
BeaconEvent.New,
BeaconEvent.Update,
BeaconEvent.Destroy,
BeaconEvent.LivenessChange,
]);
}
}
@@ -2914,6 +2958,33 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return this.getType() === RoomType.ElementVideo;
}
private roomNameGenerator(state: RoomNameState): string {
if (this.client.roomNameGenerator) {
const name = this.client.roomNameGenerator(this.roomId, state);
if (name !== null) {
return name;
}
}
switch (state.type) {
case RoomNameType.Actual:
return state.name;
case RoomNameType.Generated:
switch (state.subtype) {
case "Inviting":
return `Inviting ${memberNamesToRoomName(state.names, state.count)}`;
default:
return memberNamesToRoomName(state.names, state.count);
}
case RoomNameType.EmptyRoom:
if (state.oldName) {
return `Empty room (was ${state.oldName})`;
} else {
return "Empty room";
}
}
}
/**
* This is an internal method. Calculates the name of the room from the current
* room state.
@@ -2928,14 +2999,20 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
// check for an alias, if any. for now, assume first alias is the
// official one.
const mRoomName = this.currentState.getStateEvents(EventType.RoomName, "");
if (mRoomName && mRoomName.getContent() && mRoomName.getContent().name) {
return mRoomName.getContent().name;
if (mRoomName?.getContent().name) {
return this.roomNameGenerator({
type: RoomNameType.Actual,
name: mRoomName.getContent().name,
});
}
}
const alias = this.getCanonicalAlias();
if (alias) {
return alias;
return this.roomNameGenerator({
type: RoomNameType.Actual,
name: alias,
});
}
const joinedMemberCount = this.currentState.getJoinedMemberCount();
@@ -2967,8 +3044,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
});
} else {
let otherMembers = this.currentState.getMembers().filter((m) => {
return m.userId !== userId &&
(m.membership === "invite" || m.membership === "join");
return m.userId !== userId && (m.membership === "invite" || m.membership === "join");
});
otherMembers = otherMembers.filter(({ userId }) => {
// filter service members
@@ -2986,24 +3062,33 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
if (inviteJoinCount) {
return memberNamesToRoomName(otherNames, inviteJoinCount);
return this.roomNameGenerator({
type: RoomNameType.Generated,
names: otherNames,
count: inviteJoinCount,
});
}
const myMembership = this.getMyMembership();
// if I have created a room and invited people through
// 3rd party invites
if (myMembership == 'join') {
const thirdPartyInvites =
this.currentState.getStateEvents(EventType.RoomThirdPartyInvite);
const thirdPartyInvites = this.currentState.getStateEvents(EventType.RoomThirdPartyInvite);
if (thirdPartyInvites && thirdPartyInvites.length) {
if (thirdPartyInvites?.length) {
const thirdPartyNames = thirdPartyInvites.map((i) => {
return i.getContent().display_name;
});
return `Inviting ${memberNamesToRoomName(thirdPartyNames)}`;
return this.roomNameGenerator({
type: RoomNameType.Generated,
subtype: "Inviting",
names: thirdPartyNames,
count: thirdPartyNames.length + 1,
});
}
}
// let's try to figure out who was here before
let leftNames = otherNames;
// if we didn't have heroes, try finding them in the room state
@@ -3014,11 +3099,20 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
m.membership !== "join";
}).map((m) => m.name);
}
let oldName: string;
if (leftNames.length) {
return `Empty room (was ${memberNamesToRoomName(leftNames)})`;
} else {
return "Empty room";
oldName = this.roomNameGenerator({
type: RoomNameType.Generated,
names: leftNames,
count: leftNames.length + 1,
});
}
return this.roomNameGenerator({
type: RoomNameType.EmptyRoom,
oldName,
});
}
/**
@@ -3203,8 +3297,33 @@ const ALLOWED_TRANSITIONS: Record<EventStatus, EventStatus[]> = {
[EventStatus.CANCELLED]: [],
};
// TODO i18n
function memberNamesToRoomName(names: string[], count = (names.length + 1)) {
export enum RoomNameType {
EmptyRoom,
Generated,
Actual,
}
export interface EmptyRoomNameState {
type: RoomNameType.EmptyRoom;
oldName?: string;
}
export interface GeneratedRoomNameState {
type: RoomNameType.Generated;
subtype?: "Inviting";
names: string[];
count: number;
}
export interface ActualRoomNameState {
type: RoomNameType.Actual;
name: string;
}
export type RoomNameState = EmptyRoomNameState | GeneratedRoomNameState | ActualRoomNameState;
// Can be overriden by IMatrixClientCreateOpts::memberNamesToRoomNameFn
function memberNamesToRoomName(names: string[], count: number): string {
const countWithoutMe = count - 1;
if (!names.length) {
return "Empty room";
+3 -7
View File
@@ -362,13 +362,9 @@ export class PushProcessor {
return false;
}
let regex;
if (cond.key == 'content.body') {
regex = this.createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)');
} else {
regex = this.createCachedRegex('^', cond.pattern, '$');
}
const regex = cond.key === 'content.body'
? this.createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)')
: this.createCachedRegex('^', cond.pattern, '$');
return !!val.match(regex);
}
+45 -59
View File
@@ -19,13 +19,11 @@ import { logger } from './logger';
import * as utils from "./utils";
import { EventTimeline } from "./models/event-timeline";
import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client";
import { ISyncStateData, SyncState } from "./sync";
import { ISyncStateData, SyncState, _createAndReEmitRoom } from "./sync";
import { MatrixEvent } from "./models/event";
import { Crypto } from "./crypto";
import { IMinimalEvent, IRoomEvent, IStateEvent, IStrippedState } from "./sync-accumulator";
import { MatrixError } from "./http-api";
import { RoomStateEvent } from "./models/room-state";
import { RoomMemberEvent } from "./models/room-member";
import {
Extension,
ExtensionState,
@@ -290,7 +288,7 @@ export class SlidingSyncSdk {
logger.debug("initial flag not set but no stored room exists for room ", roomId, roomData);
return;
}
room = createRoom(this.client, roomId, this.opts);
room = _createAndReEmitRoom(this.client, roomId, this.opts);
}
this.processRoomData(this.client, room, roomData);
}
@@ -406,9 +404,51 @@ export class SlidingSyncSdk {
// this helps large account to speed up faster
// room::decryptCriticalEvent is in charge of decrypting all the events
// required for a client to function properly
const timelineEvents = mapEvents(this.client, room.roomId, roomData.timeline, false);
let timelineEvents = mapEvents(this.client, room.roomId, roomData.timeline, false);
const ephemeralEvents = []; // TODO this.mapSyncEventsFormat(joinObj.ephemeral);
// TODO: handle threaded / beacon events
if (roomData.initial) {
// we should not know about any of these timeline entries if this is a genuinely new room.
// If we do, then we've effectively done scrollback (e.g requesting timeline_limit: 1 for
// this room, then timeline_limit: 50).
const knownEvents = new Set<string>();
room.getLiveTimeline().getEvents().forEach((e) => {
knownEvents.add(e.getId());
});
// all unknown events BEFORE a known event must be scrollback e.g:
// D E <-- what we know
// A B C D E F <-- what we just received
// means:
// A B C <-- scrollback
// D E <-- dupes
// F <-- new event
// We bucket events based on if we have seen a known event yet.
const oldEvents: MatrixEvent[] = [];
const newEvents: MatrixEvent[] = [];
let seenKnownEvent = false;
for (let i = timelineEvents.length-1; i >= 0; i--) {
const recvEvent = timelineEvents[i];
if (knownEvents.has(recvEvent.getId())) {
seenKnownEvent = true;
continue; // don't include this event, it's a dupe
}
if (seenKnownEvent) {
// old -> new
oldEvents.push(recvEvent);
} else {
// old -> new
newEvents.unshift(recvEvent);
}
}
timelineEvents = newEvents;
if (oldEvents.length > 0) {
// old events are scrollback, insert them now
room.addEventsToTimeline(oldEvents, true, room.getLiveTimeline(), roomData.prev_batch);
}
}
const encrypted = this.client.isRoomEncrypted(room.roomId);
// we do this first so it's correct when any of the events fire
if (roomData.notification_count != null) {
@@ -494,7 +534,6 @@ export class SlidingSyncSdk {
}
if (limited) {
deregisterStateListeners(room);
room.resetLiveTimeline(
roomData.prev_batch,
null, // TODO this.opts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken,
@@ -504,7 +543,6 @@ export class SlidingSyncSdk {
// reason to stop incrementally tracking notifications and
// reset the timeline.
this.client.resetNotifTimelineSet();
registerStateListeners(this.client, room);
}
} */
@@ -774,58 +812,6 @@ function ensureNameEvent(client: MatrixClient, roomId: string, roomData: MSC3575
// Helper functions which set up JS SDK structs are below and are identical to the sync v2 counterparts,
// just outside the class.
function createRoom(client: MatrixClient, roomId: string, opts: Partial<IStoredClientOpts>): Room { // XXX cargoculted from sync.ts
const { timelineSupport } = client;
const room = new Room(roomId, client, client.getUserId(), {
lazyLoadMembers: opts.lazyLoadMembers,
pendingEventOrdering: opts.pendingEventOrdering,
timelineSupport,
});
client.reEmitter.reEmit(room, [
RoomEvent.Name,
RoomEvent.Redaction,
RoomEvent.RedactionCancelled,
RoomEvent.Receipt,
RoomEvent.Tags,
RoomEvent.LocalEchoUpdated,
RoomEvent.AccountData,
RoomEvent.MyMembership,
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
registerStateListeners(client, room);
return room;
}
function registerStateListeners(client: MatrixClient, room: Room): void { // XXX cargoculted from sync.ts
// we need to also re-emit room state and room member events, so hook it up
// to the client now. We need to add a listener for RoomState.members in
// order to hook them correctly.
client.reEmitter.reEmit(room.currentState, [
RoomStateEvent.Events,
RoomStateEvent.Members,
RoomStateEvent.NewMember,
RoomStateEvent.Update,
]);
room.currentState.on(RoomStateEvent.NewMember, function(event, state, member) {
member.user = client.getUser(member.userId);
client.reEmitter.reEmit(member, [
RoomMemberEvent.Name,
RoomMemberEvent.Typing,
RoomMemberEvent.PowerLevel,
RoomMemberEvent.Membership,
]);
});
}
/*
function deregisterStateListeners(room: Room): void { // XXX cargoculted from sync.ts
// could do with a better way of achieving this.
room.currentState.removeAllListeners(RoomStateEvent.Events);
room.currentState.removeAllListeners(RoomStateEvent.Members);
room.currentState.removeAllListeners(RoomStateEvent.NewMember);
} */
function mapEvents(client: MatrixClient, roomId: string, events: object[], decrypt = true): MatrixEvent[] {
const mapper = client.getEventMapper({ decrypt });
return (events as Array<IStrippedState | IRoomEvent | IStateEvent | IMinimalEvent>).map(function(e) {
+153 -41
View File
@@ -19,7 +19,7 @@ import { IAbortablePromise } from "./@types/partials";
import { MatrixClient } from "./client";
import { IRoomEvent, IStateEvent } from "./sync-accumulator";
import { TypedEventEmitter } from "./models//typed-event-emitter";
import { sleep } from "./utils";
import { sleep, IDeferred, defer } from "./utils";
// /sync requests allow you to set a timeout= but the request may continue
// beyond that and wedge forever, so we need to track how long we are willing
@@ -47,6 +47,8 @@ export interface MSC3575Filter {
room_types?: string[];
not_room_types?: string[];
spaces?: string[];
tags?: string[];
not_tags?: string[];
}
/**
@@ -68,6 +70,7 @@ export interface MSC3575SlidingSyncRequest {
unsubscribe_rooms?: string[];
room_subscriptions?: Record<string, MSC3575RoomSubscription>;
extensions?: object;
txn_id?: string;
// query params
pos?: string;
@@ -126,6 +129,7 @@ type Operation = DeleteOperation | InsertOperation | InvalidateOperation | SyncO
*/
export interface MSC3575SlidingSyncResponse {
pos: string;
txn_id?: string;
lists: ListResponse[];
rooms: Record<string, MSC3575RoomData>;
extensions: object;
@@ -334,6 +338,11 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
private terminated = false;
// flag set when resend() is called because we cannot rely on detecting AbortError in JS SDK :(
private needsResend = false;
// the txn_id to send with the next request.
private txnId?: string = null;
// a list (in chronological order of when they were sent) of objects containing the txn ID and
// a defer to resolve/reject depending on whether they were successfully sent or not.
private txnIdDefers: (IDeferred<string> & { txnId: string})[] = [];
// map of extension name to req/resp handler
private extensions: Record<string, Extension> = {};
@@ -403,10 +412,13 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
* whereas setList always will.
* @param index The list index to modify
* @param ranges The new ranges to apply.
* @return A promise which resolves to the transaction ID when it has been received down sync
* (or rejects with the transaction ID if the action was not applied e.g the request was cancelled
* immediately after sending, in which case the action will be applied in the subsequent request)
*/
public setListRanges(index: number, ranges: number[][]): void {
public setListRanges(index: number, ranges: number[][]): Promise<string> {
this.lists[index].updateListRange(ranges);
this.resend();
return this.resend();
}
/**
@@ -414,15 +426,18 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
* lists.
* @param index The index to modify
* @param list The new list parameters.
* @return A promise which resolves to the transaction ID when it has been received down sync
* (or rejects with the transaction ID if the action was not applied e.g the request was cancelled
* immediately after sending, in which case the action will be applied in the subsequent request)
*/
public setList(index: number, list: MSC3575List): void {
public setList(index: number, list: MSC3575List): Promise<string> {
if (this.lists[index]) {
this.lists[index].replaceList(list);
} else {
this.lists[index] = new SlidingList(list);
}
this.listModifiedCount += 1;
this.resend();
return this.resend();
}
/**
@@ -438,21 +453,27 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
* /sync request to resend new subscriptions. If the /sync stream has not started, this will
* prepare the room subscriptions for when start() is called.
* @param s The new desired room subscriptions.
* @return A promise which resolves to the transaction ID when it has been received down sync
* (or rejects with the transaction ID if the action was not applied e.g the request was cancelled
* immediately after sending, in which case the action will be applied in the subsequent request)
*/
public modifyRoomSubscriptions(s: Set<string>) {
public modifyRoomSubscriptions(s: Set<string>): Promise<string> {
this.desiredRoomSubscriptions = s;
this.resend();
return this.resend();
}
/**
* Modify which events to retrieve for room subscriptions. Invalidates all room subscriptions
* such that they will be sent up afresh.
* @param rs The new room subscription fields to fetch.
* @return A promise which resolves to the transaction ID when it has been received down sync
* (or rejects with the transaction ID if the action was not applied e.g the request was cancelled
* immediately after sending, in which case the action will be applied in the subsequent request)
*/
public modifyRoomSubscriptionInfo(rs: MSC3575RoomSubscription): void {
public modifyRoomSubscriptionInfo(rs: MSC3575RoomSubscription): Promise<string> {
this.roomSubscriptionInfo = rs;
this.confirmedRoomSubscriptions = new Set<string>();
this.resend();
return this.resend();
}
/**
@@ -511,6 +532,65 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
this.emit(SlidingSyncEvent.Lifecycle, state, resp, err);
}
private shiftRight(listIndex: number, hi: number, low: number) {
// l h
// 0,1,2,3,4 <- before
// 0,1,2,2,3 <- after, hi is deleted and low is duplicated
for (let i = hi; i > low; i--) {
if (this.lists[listIndex].isIndexInRange(i)) {
this.lists[listIndex].roomIndexToRoomId[i] =
this.lists[listIndex].roomIndexToRoomId[
i - 1
];
}
}
}
private shiftLeft(listIndex: number, hi: number, low: number) {
// l h
// 0,1,2,3,4 <- before
// 0,1,3,4,4 <- after, low is deleted and hi is duplicated
for (let i = low; i < hi; i++) {
if (this.lists[listIndex].isIndexInRange(i)) {
this.lists[listIndex].roomIndexToRoomId[i] =
this.lists[listIndex].roomIndexToRoomId[
i + 1
];
}
}
}
private removeEntry(listIndex: number, index: number) {
// work out the max index
let max = -1;
for (const n in this.lists[listIndex].roomIndexToRoomId) {
if (Number(n) > max) {
max = Number(n);
}
}
if (max < 0 || index > max) {
return;
}
// Everything higher than the gap needs to be shifted left.
this.shiftLeft(listIndex, max, index);
delete this.lists[listIndex].roomIndexToRoomId[max];
}
private addEntry(listIndex: number, index: number) {
// work out the max index
let max = -1;
for (const n in this.lists[listIndex].roomIndexToRoomId) {
if (Number(n) > max) {
max = Number(n);
}
}
if (max < 0 || index > max) {
return;
}
// Everything higher than the gap needs to be shifted right, +1 so we don't delete the highest element
this.shiftRight(listIndex, max+1, index);
}
private processListOps(list: ListResponse, listIndex: number): void {
let gapIndex = -1;
list.ops.forEach((op: Operation) => {
@@ -518,6 +598,10 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
case "DELETE": {
logger.debug("DELETE", listIndex, op.index, ";");
delete this.lists[listIndex].roomIndexToRoomId[op.index];
if (gapIndex !== -1) {
// we already have a DELETE operation to process, so process it.
this.removeEntry(listIndex, gapIndex);
}
gapIndex = op.index;
break;
}
@@ -532,20 +616,9 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
if (this.lists[listIndex].roomIndexToRoomId[op.index]) {
// something is in this space, shift items out of the way
if (gapIndex < 0) {
logger.debug(
"cannot work out where gap is, INSERT without previous DELETE! List: ",
listIndex,
);
return;
}
// 0,1,2,3 index
// [A,B,C,D]
// DEL 3
// [A,B,C,_]
// INSERT E 0
// [E,A,B,C]
// gapIndex=3, op.index=0
if (gapIndex > op.index) {
// we haven't been told where to shift from, so make way for a new room entry.
this.addEntry(listIndex, op.index);
} else if (gapIndex > op.index) {
// the gap is further down the list, shift every element to the right
// starting at the gap so we can just shift each element in turn:
// [A,B,C,_] gapIndex=3, op.index=0
@@ -553,26 +626,13 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
// [A,B,B,C] i=2
// [A,A,B,C] i=1
// Terminate. We'll assign into op.index next.
for (let i = gapIndex; i > op.index; i--) {
if (this.lists[listIndex].isIndexInRange(i)) {
this.lists[listIndex].roomIndexToRoomId[i] =
this.lists[listIndex].roomIndexToRoomId[
i - 1
];
}
}
this.shiftRight(listIndex, gapIndex, op.index);
} else if (gapIndex < op.index) {
// the gap is further up the list, shift every element to the left
// starting at the gap so we can just shift each element in turn
for (let i = gapIndex; i < op.index; i++) {
if (this.lists[listIndex].isIndexInRange(i)) {
this.lists[listIndex].roomIndexToRoomId[i] =
this.lists[listIndex].roomIndexToRoomId[
i + 1
];
}
}
this.shiftLeft(listIndex, op.index, gapIndex);
}
gapIndex = -1; // forget the gap, we don't need it anymore.
}
this.lists[listIndex].roomIndexToRoomId[op.index] = op.room_id;
break;
@@ -612,14 +672,60 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
}
}
});
if (gapIndex !== -1) {
// we already have a DELETE operation to process, so process it
// Everything higher than the gap needs to be shifted left.
this.removeEntry(listIndex, gapIndex);
}
}
/**
* Resend a Sliding Sync request. Used when something has changed in the request.
* Resend a Sliding Sync request. Used when something has changed in the request. Resolves with
* the transaction ID of this request on success. Rejects with the transaction ID of this request
* on failure.
*/
public resend(): void {
public resend(): Promise<string> {
if (this.needsResend && this.txnIdDefers.length > 0) {
// we already have a resend queued, so just return the same promise
return this.txnIdDefers[this.txnIdDefers.length-1].promise;
}
this.needsResend = true;
this.txnId = this.client.makeTxnId();
const d = defer<string>();
this.txnIdDefers.push({
...d,
txnId: this.txnId,
});
this.pendingReq?.abort();
return d.promise;
}
private resolveTransactionDefers(txnId?: string) {
if (!txnId) {
return;
}
// find the matching index
let txnIndex = -1;
for (let i = 0; i < this.txnIdDefers.length; i++) {
if (this.txnIdDefers[i].txnId === txnId) {
txnIndex = i;
break;
}
}
if (txnIndex === -1) {
// this shouldn't happen; we shouldn't be seeing txn_ids for things we don't know about,
// whine about it.
logger.warn(`resolveTransactionDefers: seen ${txnId} but it isn't a pending txn, ignoring.`);
return;
}
// This list is sorted in time, so if the input txnId ACKs in the middle of this array,
// then everything before it that hasn't been ACKed yet never will and we should reject them.
for (let i = 0; i < txnIndex; i++) {
this.txnIdDefers[i].reject(this.txnIdDefers[i].txnId);
}
this.txnIdDefers[txnIndex].resolve(txnId);
// clear out settled promises, incuding the one we resolved.
this.txnIdDefers = this.txnIdDefers.slice(txnIndex+1);
}
/**
@@ -666,6 +772,10 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
reqBody.room_subscriptions[roomId] = this.roomSubscriptionInfo;
}
}
if (this.txnId) {
reqBody.txn_id = this.txnId;
this.txnId = null;
}
this.pendingReq = this.client.slidingSync(reqBody, this.proxyBaseUrl);
resp = await this.pendingReq;
logger.debug(resp);
@@ -747,6 +857,8 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
i, this.lists[i].joinedCount, Object.assign({}, this.lists[i].roomIndexToRoomId),
);
});
this.resolveTransactionDefers(resp.txn_id);
}
}
}
+51 -77
View File
@@ -1,5 +1,5 @@
/*
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2015 - 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.
@@ -51,7 +51,7 @@ import { MatrixError, Method } from "./http-api";
import { ISavedSync } from "./store";
import { EventType } from "./@types/event";
import { IPushRules } from "./@types/PushRules";
import { RoomState, RoomStateEvent, IMarkerFoundOptions } from "./models/room-state";
import { RoomStateEvent, IMarkerFoundOptions } from "./models/room-state";
import { RoomMemberEvent } from "./models/room-member";
import { BeaconEvent } from "./models/beacon";
import { IEventsResponse } from "./@types/requests";
@@ -199,85 +199,13 @@ export class SyncApi {
* @return {Room}
*/
public createRoom(roomId: string): Room {
const client = this.client;
const {
timelineSupport,
} = client;
const room = new Room(roomId, client, client.getUserId(), {
lazyLoadMembers: this.opts.lazyLoadMembers,
pendingEventOrdering: this.opts.pendingEventOrdering,
timelineSupport,
});
client.reEmitter.reEmit(room, [
RoomEvent.Name,
RoomEvent.Redaction,
RoomEvent.RedactionCancelled,
RoomEvent.Receipt,
RoomEvent.Tags,
RoomEvent.LocalEchoUpdated,
RoomEvent.AccountData,
RoomEvent.MyMembership,
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
this.registerStateListeners(room);
// Register listeners again after the state reference changes
room.on(RoomEvent.CurrentStateUpdated, (targetRoom, previousCurrentState) => {
if (targetRoom !== room) {
return;
}
const room = _createAndReEmitRoom(this.client, roomId, this.opts);
this.deregisterStateListeners(previousCurrentState);
this.registerStateListeners(room);
});
return room;
}
/**
* @param {Room} room
* @private
*/
private registerStateListeners(room: Room): void {
const client = this.client;
// we need to also re-emit room state and room member events, so hook it up
// to the client now. We need to add a listener for RoomState.members in
// order to hook them correctly. (TODO: find a better way?)
client.reEmitter.reEmit(room.currentState, [
RoomStateEvent.Events,
RoomStateEvent.Members,
RoomStateEvent.NewMember,
RoomStateEvent.Update,
BeaconEvent.New,
BeaconEvent.Update,
BeaconEvent.Destroy,
BeaconEvent.LivenessChange,
]);
room.currentState.on(RoomStateEvent.NewMember, function(event, state, member) {
member.user = client.getUser(member.userId);
client.reEmitter.reEmit(member, [
RoomMemberEvent.Name,
RoomMemberEvent.Typing,
RoomMemberEvent.PowerLevel,
RoomMemberEvent.Membership,
]);
});
room.currentState.on(RoomStateEvent.Marker, (markerEvent, markerFoundOptions) => {
room.on(RoomStateEvent.Marker, (markerEvent, markerFoundOptions) => {
this.onMarkerStateEvent(room, markerEvent, markerFoundOptions);
});
}
/**
* @param {RoomState} roomState The roomState to clear listeners from
* @private
*/
private deregisterStateListeners(roomState: RoomState): void {
// could do with a better way of achieving this.
roomState.removeAllListeners(RoomStateEvent.Events);
roomState.removeAllListeners(RoomStateEvent.Members);
roomState.removeAllListeners(RoomStateEvent.NewMember);
roomState.removeAllListeners(RoomStateEvent.Marker);
return room;
}
/** When we see the marker state change in the room, we know there is some
@@ -1792,3 +1720,49 @@ function createNewUser(client: MatrixClient, userId: string): User {
return user;
}
// /!\ This function is not intended for public use! It's only exported from
// here in order to share some common logic with sliding-sync-sdk.ts.
export function _createAndReEmitRoom(client: MatrixClient, roomId: string, opts: Partial<IStoredClientOpts>): Room {
const { timelineSupport } = client;
const room = new Room(roomId, client, client.getUserId(), {
lazyLoadMembers: opts.lazyLoadMembers,
pendingEventOrdering: opts.pendingEventOrdering,
timelineSupport,
});
client.reEmitter.reEmit(room, [
RoomEvent.Name,
RoomEvent.Redaction,
RoomEvent.RedactionCancelled,
RoomEvent.Receipt,
RoomEvent.Tags,
RoomEvent.LocalEchoUpdated,
RoomEvent.AccountData,
RoomEvent.MyMembership,
RoomEvent.Timeline,
RoomEvent.TimelineReset,
RoomStateEvent.Events,
RoomStateEvent.Members,
RoomStateEvent.NewMember,
RoomStateEvent.Update,
BeaconEvent.New,
BeaconEvent.Update,
BeaconEvent.Destroy,
BeaconEvent.LivenessChange,
]);
// We need to add a listener for RoomState.members in order to hook them
// correctly.
room.on(RoomStateEvent.NewMember, (event, state, member) => {
member.user = client.getUser(member.userId);
client.reEmitter.reEmit(member, [
RoomMemberEvent.Name,
RoomMemberEvent.Typing,
RoomMemberEvent.PowerLevel,
RoomMemberEvent.Membership,
]);
});
return room;
}
+18 -12
View File
@@ -328,22 +328,28 @@ export function escapeRegExp(string: string): string {
}
export function globToRegexp(glob: string, extended?: any): string {
extended = typeof(extended) === 'boolean' ? extended : true;
// From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
// Because micromatch is about 130KB with dependencies,
// and minimatch is not much better.
let pat = escapeRegExp(glob);
pat = pat.replace(/\\\*/g, '.*');
pat = pat.replace(/\?/g, '.');
if (extended) {
pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) {
const first = p1 && '^' || '';
const second = p2.replace(/\\-/, '-');
return '[' + first + second + ']';
});
}
return pat;
const replacements: ([RegExp, string | ((substring: string, ...args: any[]) => string) ])[] = [
[/\\\*/g, '.*'],
[/\?/g, '.'],
extended !== false && [
/\\\[(!|)(.*)\\]/g,
(_match: string, neg: string, pat: string) => [
'[',
neg ? '^' : '',
pat.replace(/\\-/, '-'),
']',
].join(''),
],
];
return replacements.reduce(
// https://github.com/microsoft/TypeScript/issues/30134
(pat, args) => args ? pat.replace(args[0], args[1] as any) : pat,
escapeRegExp(glob),
);
}
export function ensureNoTrailingSlash(url: string): string {
+2 -4
View File
@@ -866,8 +866,6 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
public answerWithCallFeeds(callFeeds: CallFeed[]): void {
if (this.inviteOrAnswerSent) return;
logger.debug(`Answering call ${this.callId}`);
this.gotCallFeedsForAnswer(callFeeds);
}
@@ -1865,10 +1863,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const shouldTerminate = (
// reject events also end the call if it's ringing: it's another of
// our devices rejecting the call.
([CallState.InviteSent, CallState.Ringing].includes(this.state)) ||
[CallState.InviteSent, CallState.Ringing].includes(this.state) ||
// also if we're in the init state and it's an inbound call, since
// this means we just haven't entered the ringing state yet
this.state === CallState.Fledgling && this.direction === CallDirection.Inbound
(this.state === CallState.Fledgling && this.direction === CallDirection.Inbound)
);
if (shouldTerminate) {
+275 -283
View File
File diff suppressed because it is too large Load Diff