Compare commits
31 Commits
v19.3.0
...
v19.4.0-rc.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 528e9343ae | |||
| 6571b6a1ab | |||
| 1df329df7c | |||
| 760eeaeed7 | |||
| 438fc70615 | |||
| de3b3960d2 | |||
| b265d795a4 | |||
| eb79f6246d | |||
| 37f8f736e0 | |||
| 4b1a443f90 | |||
| 3ae974e23e | |||
| 566b4ba56c | |||
| 13291f33d2 | |||
| be3e731499 | |||
| 3f6f5b69c7 | |||
| 478270b225 | |||
| 9eb72908a7 | |||
| 2728d74771 | |||
| edcef9364c | |||
| 1635ac9971 | |||
| 8f13df2dd9 | |||
| fa9f078a75 | |||
| 055af933cc | |||
| 1ba2730e25 | |||
| e29e0d15a5 | |||
| 9ee94c9902 | |||
| 1645867ea6 | |||
| 24d4181a08 | |||
| 2596999cb8 | |||
| a3248c0aa1 | |||
| e05f9b5815 |
@@ -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",
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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";
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user