Compare commits
56 Commits
v15.5.2
...
v16.0.0-rc.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 3141a7d7c1 | |||
| 1304e811d0 | |||
| 2ce1e7e6ef | |||
| 70efed1a58 | |||
| 9e4f109e80 | |||
| 5d54bf558c | |||
| fc5f0e8047 | |||
| 6bc584ba8b | |||
| ac4e504b8d | |||
| 0abee2a0bf | |||
| 4e4afdb795 | |||
| 9b429c1902 | |||
| b782dee2ef | |||
| 54e815085f | |||
| 0739da4ef4 | |||
| c300a6bdd6 | |||
| 901d53eb38 | |||
| a54471f737 | |||
| 1fae9cb3ef | |||
| 124bfc9328 | |||
| 53aa34fba5 | |||
| 58756a1973 | |||
| 7d9800b817 | |||
| e408590c21 | |||
| 735ccca18b | |||
| 946f47e037 | |||
| 4de1699c49 | |||
| 3d9221f054 | |||
| 1d1d59c757 | |||
| af43687354 | |||
| 2ec5acb55d | |||
| 2128f67dc3 | |||
| 55dda8420c | |||
| d595717e60 | |||
| 4603d4e578 | |||
| 12e525b664 | |||
| 1ac4cc4b11 | |||
| 7b218905fb | |||
| 080426dfdd | |||
| a89c1990d6 | |||
| 4e72290d53 | |||
| 7a7318b636 | |||
| 74d24f38f7 | |||
| e86d8861b9 | |||
| 2910e62bb6 | |||
| 2bb14698a5 | |||
| 2fd08d72dc | |||
| 222f62164c | |||
| cfad8d3614 | |||
| d9c3b880fc | |||
| 25c115739c | |||
| d97c514b8d | |||
| 6b822ccd61 | |||
| 47c5c4645e | |||
| ea0eaff212 | |||
| 41bf8c2d5f |
@@ -31,6 +31,9 @@ module.exports = {
|
||||
"no-async-promise-executor": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
|
||||
// restrict EventEmitters to force callers to use TypedEventEmitter
|
||||
"no-restricted-imports": ["error", "events"],
|
||||
},
|
||||
overrides: [{
|
||||
files: [
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
comment:
|
||||
layout: "diff, files"
|
||||
behavior: default
|
||||
require_changes: false
|
||||
require_base: no
|
||||
require_head: no
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Test coverage
|
||||
on:
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [develop, main, master]
|
||||
jobs:
|
||||
test-coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: "yarn install && yarn build && yarn coverage"
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
verbose: true
|
||||
@@ -1,3 +1,58 @@
|
||||
Changes in [16.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.0-rc.1) (2022-03-08)
|
||||
============================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Improve typing around event emitter handlers ([\#2180](https://github.com/matrix-org/matrix-js-sdk/pull/2180)).
|
||||
|
||||
## ✨ Features
|
||||
* Fix defer not supporting resolving with a Promise<T> ([\#2216](https://github.com/matrix-org/matrix-js-sdk/pull/2216)).
|
||||
* add LocationAssetType enum ([\#2214](https://github.com/matrix-org/matrix-js-sdk/pull/2214)).
|
||||
* Support for mid-call devices changes ([\#2154](https://github.com/matrix-org/matrix-js-sdk/pull/2154)). Contributed by @SimonBrandner.
|
||||
* Add new room state emit RoomStateEvent.Update for lower-frequency hits ([\#2192](https://github.com/matrix-org/matrix-js-sdk/pull/2192)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix wrong event_id being sent for m.in_reply_to of threads ([\#2213](https://github.com/matrix-org/matrix-js-sdk/pull/2213)).
|
||||
* Fix wrongly asserting that PushRule::conditions is non-null ([\#2217](https://github.com/matrix-org/matrix-js-sdk/pull/2217)).
|
||||
* Make createThread more resilient when missing rootEvent ([\#2207](https://github.com/matrix-org/matrix-js-sdk/pull/2207)). Fixes vector-im/element-web#21130.
|
||||
* Fix bug with the /hierarchy API sending invalid requests ([\#2201](https://github.com/matrix-org/matrix-js-sdk/pull/2201)). Fixes vector-im/element-web#21170.
|
||||
* fix relation sender filter ([\#2196](https://github.com/matrix-org/matrix-js-sdk/pull/2196)). Fixes vector-im/element-web#20877.
|
||||
* Fix bug with one-way audio after a transfer ([\#2193](https://github.com/matrix-org/matrix-js-sdk/pull/2193)).
|
||||
|
||||
Changes in [15.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.6.0) (2022-02-28)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Return send event response from MSC3089Branch.createNewVersion() ([\#2186](https://github.com/matrix-org/matrix-js-sdk/pull/2186)).
|
||||
* Add functions to support refresh tokens ([\#2178](https://github.com/matrix-org/matrix-js-sdk/pull/2178)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* [Release] Fix bug with the /hierarchy API sending invalid requests ([\#2202](https://github.com/matrix-org/matrix-js-sdk/pull/2202)).
|
||||
* Fix bug where calls could break if rejected from somewhere else ([\#2189](https://github.com/matrix-org/matrix-js-sdk/pull/2189)).
|
||||
* Fix camera stuck on after call transfer ([\#2188](https://github.com/matrix-org/matrix-js-sdk/pull/2188)).
|
||||
* Fix synthetic read receipt handling ([\#2174](https://github.com/matrix-org/matrix-js-sdk/pull/2174)). Fixes vector-im/element-web#21016.
|
||||
* Revert "Sign backup with cross-signing key when we reset it." ([\#2175](https://github.com/matrix-org/matrix-js-sdk/pull/2175)).
|
||||
* Sign backup with cross-signing key when we reset it. ([\#2170](https://github.com/matrix-org/matrix-js-sdk/pull/2170)).
|
||||
* Fix error in uploadContent() when file is empty under Node.js ([\#2155](https://github.com/matrix-org/matrix-js-sdk/pull/2155)).
|
||||
* Check the backup info against the stored private key when determining trust. ([\#2167](https://github.com/matrix-org/matrix-js-sdk/pull/2167)).
|
||||
* Back up keys before logging out ([\#2158](https://github.com/matrix-org/matrix-js-sdk/pull/2158)). Fixes vector-im/element-web#13151.
|
||||
|
||||
Changes in [15.6.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.6.0-rc.1) (2022-02-22)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Return send event response from MSC3089Branch.createNewVersion() ([\#2186](https://github.com/matrix-org/matrix-js-sdk/pull/2186)).
|
||||
* Add functions to support refresh tokens ([\#2178](https://github.com/matrix-org/matrix-js-sdk/pull/2178)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix bug where calls could break if rejected from somewhere else ([\#2189](https://github.com/matrix-org/matrix-js-sdk/pull/2189)).
|
||||
* Fix camera stuck on after call transfer ([\#2188](https://github.com/matrix-org/matrix-js-sdk/pull/2188)).
|
||||
* Fix synthetic read receipt handling ([\#2174](https://github.com/matrix-org/matrix-js-sdk/pull/2174)). Fixes vector-im/element-web#21016.
|
||||
* Revert "Sign backup with cross-signing key when we reset it." ([\#2175](https://github.com/matrix-org/matrix-js-sdk/pull/2175)).
|
||||
* Sign backup with cross-signing key when we reset it. ([\#2170](https://github.com/matrix-org/matrix-js-sdk/pull/2170)).
|
||||
* Fix error in uploadContent() when file is empty under Node.js ([\#2155](https://github.com/matrix-org/matrix-js-sdk/pull/2155)).
|
||||
* Check the backup info against the stored private key when determining trust. ([\#2167](https://github.com/matrix-org/matrix-js-sdk/pull/2167)).
|
||||
* Back up keys before logging out ([\#2158](https://github.com/matrix-org/matrix-js-sdk/pull/2158)). Fixes vector-im/element-web#13151.
|
||||
|
||||
Changes in [15.5.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.2) (2022-02-17)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "15.5.2",
|
||||
"version": "16.0.0-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
@@ -56,7 +56,7 @@
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "^0.0.1-beta.6",
|
||||
"matrix-events-sdk": "^0.0.1-beta.7",
|
||||
"p-retry": "^4.5.0",
|
||||
"qs": "^6.9.6",
|
||||
"request": "^2.88.2",
|
||||
@@ -89,7 +89,7 @@
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "7.18.0",
|
||||
"eslint": "8.9.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-matrix-org": "^0.4.0",
|
||||
@@ -113,7 +113,8 @@
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text"
|
||||
"text",
|
||||
"json"
|
||||
]
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
|
||||
@@ -3,7 +3,6 @@ import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { Filter, MemoryStore, Room } from "../../src/matrix";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { Thread } from "../../src/models/thread";
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
let client = null;
|
||||
@@ -405,6 +404,11 @@ describe("MatrixClient", function() {
|
||||
|
||||
it("copies pre-thread in-timeline vote events onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
const eventMessageInThread = buildEventMessageInThread();
|
||||
const eventPollResponseReference = buildEventPollResponseReference();
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
|
||||
const events = [
|
||||
eventMessageInThread,
|
||||
eventPollResponseReference,
|
||||
@@ -435,6 +439,11 @@ describe("MatrixClient", function() {
|
||||
|
||||
it("copies pre-thread in-timeline reactions onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
const eventMessageInThread = buildEventMessageInThread();
|
||||
const eventReaction = buildEventReaction();
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
|
||||
const events = [
|
||||
eventMessageInThread,
|
||||
eventReaction,
|
||||
@@ -456,6 +465,11 @@ describe("MatrixClient", function() {
|
||||
|
||||
it("copies post-thread in-timeline vote events onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
const eventPollResponseReference = buildEventPollResponseReference();
|
||||
const eventMessageInThread = buildEventMessageInThread();
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
|
||||
const events = [
|
||||
eventPollResponseReference,
|
||||
eventMessageInThread,
|
||||
@@ -475,119 +489,13 @@ describe("MatrixClient", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies post-thread in-thread vote events onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
// Events for this test only, because we hack around with them
|
||||
const eventMessageInThread2 = new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"m.relates_to": {
|
||||
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
},
|
||||
"rel_type": "io.element.thread",
|
||||
},
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$AAAhKIGYowtBblVLkRimeIg8TcdjETnxhDPGfi6NpDg",
|
||||
"origin_server_ts": 1643815466378,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80098509 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventPollStartThreadRoot2 = new MatrixEvent({
|
||||
"age": 80108647,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"origin_server_ts": 1643815456240,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80108647 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventPollResponseReference2 = new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"m.relates_to": {
|
||||
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"rel_type": "m.reference",
|
||||
},
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$AAAvpezvsF0cKgav3g8W-uEVS4WkDHgxbJZvL3uMR1g",
|
||||
"origin_server_ts": 1643815458650,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80106237 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
// When we react within a thread, sometimes the thread root
|
||||
// has isThreadRelation === true, because thread is set on it,
|
||||
// but threadId is not.
|
||||
eventPollStartThreadRoot2.setThread(
|
||||
new Thread(
|
||||
eventPollStartThreadRoot2,
|
||||
{
|
||||
client,
|
||||
room: new Room(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const events = [
|
||||
eventPollResponseReference2,
|
||||
eventMessageInThread2,
|
||||
eventPollStartThreadRoot2,
|
||||
];
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
eventPollResponseReference2,
|
||||
// eventPollStartThreadRoot2,
|
||||
// This is weird: by hacking the thread root to have an inconsistency
|
||||
// between thread and threadId (which is what I have observed in the
|
||||
// wild), we have persuaded the code that the thread root is actually
|
||||
// within the thread, so it is not provided to the main timeline.
|
||||
//
|
||||
// This should go away when we fix this inconsistency. When that
|
||||
// happens, we should probably delete this test.
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
withThreadId(
|
||||
eventPollResponseReference2, eventPollStartThreadRoot2.getId(),
|
||||
),
|
||||
eventMessageInThread2,
|
||||
eventPollStartThreadRoot2, // See note above for why this appears here.
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies post-thread in-timeline reactions onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
const eventReaction = buildEventReaction();
|
||||
const eventMessageInThread = buildEventMessageInThread();
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
|
||||
const events = [
|
||||
eventReaction,
|
||||
eventMessageInThread,
|
||||
@@ -610,6 +518,19 @@ describe("MatrixClient", function() {
|
||||
it("sends room state events to the main timeline only", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
// This is based on recording the events in a real room:
|
||||
|
||||
const eventMessageInThread = buildEventMessageInThread();
|
||||
const eventPollResponseReference = buildEventPollResponseReference();
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
const eventRoomName = buildEventRoomName();
|
||||
const eventEncryption = buildEventEncryption();
|
||||
const eventGuestAccess = buildEventGuestAccess();
|
||||
const eventHistoryVisibility = buildEventHistoryVisibility();
|
||||
const eventJoinRules = buildEventJoinRules();
|
||||
const eventPowerLevels = buildEventPowerLevels();
|
||||
const eventMember = buildEventMember();
|
||||
const eventCreate = buildEventCreate();
|
||||
|
||||
const events = [
|
||||
eventMessageInThread,
|
||||
eventPollResponseReference,
|
||||
@@ -655,7 +576,7 @@ function withThreadId(event, newThreadId) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
const eventMessageInThread = new MatrixEvent({
|
||||
const buildEventMessageInThread = () => new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -680,7 +601,7 @@ const eventMessageInThread = new MatrixEvent({
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventPollResponseReference = new MatrixEvent({
|
||||
const buildEventPollResponseReference = () => new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -702,7 +623,7 @@ const eventPollResponseReference = new MatrixEvent({
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventReaction = new MatrixEvent({
|
||||
const buildEventReaction = () => new MatrixEvent({
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
@@ -721,7 +642,7 @@ const eventReaction = new MatrixEvent({
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
});
|
||||
|
||||
const eventPollStartThreadRoot = new MatrixEvent({
|
||||
const buildEventPollStartThreadRoot = () => new MatrixEvent({
|
||||
"age": 80108647,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -739,7 +660,7 @@ const eventPollStartThreadRoot = new MatrixEvent({
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventRoomName = new MatrixEvent({
|
||||
const buildEventRoomName = () => new MatrixEvent({
|
||||
"age": 80123249,
|
||||
"content": {
|
||||
"name": "1 poll, 1 vote, 1 thread",
|
||||
@@ -754,7 +675,7 @@ const eventRoomName = new MatrixEvent({
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventEncryption = new MatrixEvent({
|
||||
const buildEventEncryption = () => new MatrixEvent({
|
||||
"age": 80123383,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -769,7 +690,7 @@ const eventEncryption = new MatrixEvent({
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventGuestAccess = new MatrixEvent({
|
||||
const buildEventGuestAccess = () => new MatrixEvent({
|
||||
"age": 80123473,
|
||||
"content": {
|
||||
"guest_access": "can_join",
|
||||
@@ -784,7 +705,7 @@ const eventGuestAccess = new MatrixEvent({
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventHistoryVisibility = new MatrixEvent({
|
||||
const buildEventHistoryVisibility = () => new MatrixEvent({
|
||||
"age": 80123556,
|
||||
"content": {
|
||||
"history_visibility": "shared",
|
||||
@@ -799,7 +720,7 @@ const eventHistoryVisibility = new MatrixEvent({
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventJoinRules = new MatrixEvent({
|
||||
const buildEventJoinRules = () => new MatrixEvent({
|
||||
"age": 80123696,
|
||||
"content": {
|
||||
"join_rule": "invite",
|
||||
@@ -814,7 +735,7 @@ const eventJoinRules = new MatrixEvent({
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventPowerLevels = new MatrixEvent({
|
||||
const buildEventPowerLevels = () => new MatrixEvent({
|
||||
"age": 80124105,
|
||||
"content": {
|
||||
"ban": 50,
|
||||
@@ -849,7 +770,7 @@ const eventPowerLevels = new MatrixEvent({
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventMember = new MatrixEvent({
|
||||
const buildEventMember = () => new MatrixEvent({
|
||||
"age": 80125279,
|
||||
"content": {
|
||||
"avatar_url": "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
|
||||
@@ -866,7 +787,7 @@ const eventMember = new MatrixEvent({
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventCreate = new MatrixEvent({
|
||||
const buildEventCreate = () => new MatrixEvent({
|
||||
"age": 80126105,
|
||||
"content": {
|
||||
"creator": "@andybalaam-test1:matrix.org",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EventStatus } from "../../src/matrix";
|
||||
import { EventStatus, RoomEvent } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { TestClient } from "../TestClient";
|
||||
@@ -95,7 +95,7 @@ describe("MatrixClient retrying", function() {
|
||||
|
||||
// wait for the localecho of ev1 to be updated
|
||||
const p3 = new Promise<void>((resolve, reject) => {
|
||||
room.on("Room.localEchoUpdated", (ev0) => {
|
||||
room.on(RoomEvent.LocalEchoUpdated, (ev0) => {
|
||||
if (ev0 === ev1) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ export function mkEvent(opts) {
|
||||
room_id: opts.room,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: opts.content,
|
||||
unsigned: opts.unsigned,
|
||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||
};
|
||||
if (opts.skey !== undefined) {
|
||||
|
||||
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import '../olm-loader';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { Crypto } from "../../src/crypto";
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function resetCrossSigningKeys(client, {
|
||||
crypto.crossSigningInfo.keys = oldKeys;
|
||||
throw e;
|
||||
}
|
||||
crypto.baseApis.emit("crossSigning.keysChanged", {});
|
||||
crypto.emit("crossSigning.keysChanged", {});
|
||||
await crypto.afterCrossSigningLocalKeyChange();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
|
||||
import { encodeBase64 } from "../../../../src/crypto/olmlib";
|
||||
import { setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { FilterComponent } from "../../src/filter-component";
|
||||
import { mkEvent } from '../test-utils';
|
||||
|
||||
describe("Filter Component", function() {
|
||||
describe("types", function() {
|
||||
it("should filter out events with other types", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.member',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(false);
|
||||
});
|
||||
|
||||
it("should validate events with the same type", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
RelationType,
|
||||
UNSTABLE_FILTER_RELATED_BY_REL_TYPES,
|
||||
UNSTABLE_FILTER_RELATED_BY_SENDERS,
|
||||
} from "../../src";
|
||||
import { FilterComponent } from "../../src/filter-component";
|
||||
import { mkEvent } from '../test-utils';
|
||||
|
||||
describe("Filter Component", function() {
|
||||
describe("types", function() {
|
||||
it("should filter out events with other types", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.member',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(false);
|
||||
});
|
||||
|
||||
it("should validate events with the same type", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter out events by relation participation", function() {
|
||||
const currentUserId = '@me:server.org';
|
||||
const filter = new FilterComponent({
|
||||
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: [currentUserId],
|
||||
}, currentUserId);
|
||||
|
||||
const threadRootNotParticipated = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
room: 'roomId',
|
||||
user: '@someone-else:server.org',
|
||||
event: true,
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
count: 2,
|
||||
current_user_participated: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootNotParticipated)).toBe(false);
|
||||
});
|
||||
|
||||
it("should keep events by relation participation", function() {
|
||||
const currentUserId = '@me:server.org';
|
||||
const filter = new FilterComponent({
|
||||
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: [currentUserId],
|
||||
}, currentUserId);
|
||||
|
||||
const threadRootParticipated = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
user: '@someone-else:server.org',
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootParticipated)).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter out events by relation type", function() {
|
||||
const filter = new FilterComponent({
|
||||
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: [RelationType.Thread],
|
||||
});
|
||||
|
||||
const referenceRelationEvent = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
[RelationType.Reference]: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(filter.check(referenceRelationEvent)).toBe(false);
|
||||
});
|
||||
|
||||
it("should keep events by relation type", function() {
|
||||
const filter = new FilterComponent({
|
||||
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: [RelationType.Thread],
|
||||
});
|
||||
|
||||
const threadRootEvent = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootEvent)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import { makeLocationContent } from "../../src/content-helpers";
|
||||
import {
|
||||
ASSET_NODE_TYPE,
|
||||
ASSET_TYPE_SELF,
|
||||
LocationAssetType,
|
||||
LOCATION_EVENT_TYPE,
|
||||
TIMESTAMP_NODE_TYPE,
|
||||
} from "../../src/@types/location";
|
||||
@@ -33,14 +33,14 @@ describe("Location", function() {
|
||||
uri: "geo:foo",
|
||||
description: undefined,
|
||||
});
|
||||
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: ASSET_TYPE_SELF });
|
||||
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: LocationAssetType.Self });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txt");
|
||||
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235435);
|
||||
});
|
||||
|
||||
it("should create a valid location with explicit properties", function() {
|
||||
const loc = makeLocationContent(
|
||||
"txxt", "geo:bar", 134235436, "desc", "m.something");
|
||||
"txxt", "geo:bar", 134235436, "desc", LocationAssetType.Pin);
|
||||
|
||||
expect(loc.body).toEqual("txxt");
|
||||
expect(loc.msgtype).toEqual("m.location");
|
||||
@@ -49,7 +49,7 @@ describe("Location", function() {
|
||||
uri: "geo:bar",
|
||||
description: "desc",
|
||||
});
|
||||
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: "m.something" });
|
||||
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: LocationAssetType.Pin });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txxt");
|
||||
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235436);
|
||||
});
|
||||
|
||||
@@ -91,11 +91,7 @@ describe("MatrixClient", function() {
|
||||
return pendingLookup.promise;
|
||||
}
|
||||
// >1 pending thing, and they are different, whine.
|
||||
expect(false).toBe(
|
||||
true, ">1 pending request. You should probably handle them. " +
|
||||
"PENDING: " + JSON.stringify(pendingLookup) + " JUST GOT: " +
|
||||
method + " " + path,
|
||||
);
|
||||
expect(false).toBe(true);
|
||||
}
|
||||
pendingLookup = {
|
||||
promise: new Promise(() => {}),
|
||||
@@ -123,6 +119,7 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
|
||||
if (next.error) {
|
||||
// eslint-disable-next-line
|
||||
return Promise.reject({
|
||||
errcode: next.error.errcode,
|
||||
httpStatus: next.error.httpStatus,
|
||||
@@ -133,7 +130,7 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
return Promise.resolve(next.data);
|
||||
}
|
||||
expect(true).toBe(false, "Expected different request. " + logLine);
|
||||
expect(true).toBe(false);
|
||||
return new Promise(() => {});
|
||||
}
|
||||
|
||||
@@ -158,7 +155,7 @@ describe("MatrixClient", function() {
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
accessToken: "my.access.token",
|
||||
request: function() {}, // NOP
|
||||
request: function() {} as any, // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: userId,
|
||||
@@ -379,10 +376,10 @@ describe("MatrixClient", function() {
|
||||
];
|
||||
const filterId = "ehfewf";
|
||||
store.getFilterIdByName.mockReturnValue(filterId);
|
||||
const filter = new Filter(0, filterId);
|
||||
const filter = new Filter("0", filterId);
|
||||
filter.setDefinition({ "room": { "timeline": { "limit": 8 } } });
|
||||
store.getFilter.mockReturnValue(filter);
|
||||
const syncPromise = new Promise((resolve, reject) => {
|
||||
const syncPromise = new Promise<void>((resolve, reject) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "SYNCING") {
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
@@ -403,7 +400,7 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("should return the same sync state as emitted sync events", async function() {
|
||||
const syncingPromise = new Promise((resolve) => {
|
||||
const syncingPromise = new Promise<void>((resolve) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
expect(state).toEqual(client.getSyncState());
|
||||
if (state === "SYNCING") {
|
||||
@@ -423,7 +420,7 @@ describe("MatrixClient", function() {
|
||||
it("should use an existing filter if id is present in localStorage", function() {
|
||||
});
|
||||
it("should handle localStorage filterId missing from the server", function(done) {
|
||||
function getFilterName(userId, suffix) {
|
||||
function getFilterName(userId, suffix?: string) {
|
||||
// scope this on the user ID because people may login on many accounts
|
||||
// and they all need to be stored!
|
||||
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
|
||||
@@ -501,7 +498,7 @@ describe("MatrixClient", function() {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(1);
|
||||
expect(client.retryImmediately()).toBe(
|
||||
true, "retryImmediately returned false",
|
||||
true,
|
||||
);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
|
||||
@@ -584,33 +581,33 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("should transition ERROR -> CATCHUP after /sync if prev failed",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
acceptKeepalives = false;
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH, data: {},
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA,
|
||||
});
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
acceptKeepalives = false;
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH, data: {},
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA,
|
||||
});
|
||||
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition PREPARED -> SYNCING after /sync", function(done) {
|
||||
const expectedStates = [];
|
||||
@@ -640,32 +637,32 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
xit("should transition ERROR -> SYNCING after /sync if prev failed",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
|
||||
acceptKeepalives = false;
|
||||
@@ -948,4 +945,39 @@ describe("MatrixClient", function() {
|
||||
expect(event.status).toBe(EventStatus.SENDING);
|
||||
});
|
||||
});
|
||||
|
||||
describe("threads", () => {
|
||||
it("partitions root events to room timeline and thread timeline", () => {
|
||||
const supportsExperimentalThreads = client.supportsExperimentalThreads;
|
||||
client.supportsExperimentalThreads = () => true;
|
||||
|
||||
const rootEvent = new MatrixEvent({
|
||||
"content": {},
|
||||
"origin_server_ts": 1,
|
||||
"room_id": "!room1:matrix.org",
|
||||
"sender": "@alice:matrix.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"m.relations": {
|
||||
"io.element.thread": {
|
||||
"latest_event": {},
|
||||
"count": 33,
|
||||
"current_user_participated": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
"event_id": "$ev1",
|
||||
"user_id": "@alice:matrix.org",
|
||||
});
|
||||
|
||||
expect(rootEvent.isThreadRoot).toBe(true);
|
||||
|
||||
const [room, threads] = client.partitionThreadedEvents([rootEvent]);
|
||||
expect(room).toHaveLength(1);
|
||||
expect(threads).toHaveLength(1);
|
||||
|
||||
// Restore method
|
||||
client.supportsExperimentalThreads = supportsExperimentalThreads;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -302,4 +302,20 @@ describe('NotificationService', function() {
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
});
|
||||
|
||||
it("a rule with no conditions matches every event.", function() {
|
||||
expect(pushProcessor.ruleMatchesEvent({
|
||||
rule_id: "rule1",
|
||||
actions: [],
|
||||
conditions: [],
|
||||
default: false,
|
||||
enabled: true,
|
||||
}, testEvent)).toBe(true);
|
||||
expect(pushProcessor.ruleMatchesEvent({
|
||||
rule_id: "rule1",
|
||||
actions: [],
|
||||
default: false,
|
||||
enabled: true,
|
||||
}, testEvent)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventTimelineSet } from "../../src/models/event-timeline-set";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { Relations } from "../../src/models/relations";
|
||||
|
||||
@@ -103,7 +103,7 @@ describe("Relations", function() {
|
||||
// Add the target event first, then the relation event
|
||||
{
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once("Event.relationsCreated", resolve);
|
||||
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
|
||||
});
|
||||
|
||||
const timelineSet = new EventTimelineSet(room, {
|
||||
@@ -118,7 +118,7 @@ describe("Relations", function() {
|
||||
// Add the relation event first, then the target event
|
||||
{
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once("Event.relationsCreated", resolve);
|
||||
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
|
||||
});
|
||||
|
||||
const timelineSet = new EventTimelineSet(room, {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -82,17 +82,34 @@ class MockRTCPeerConnection {
|
||||
}
|
||||
close() {}
|
||||
getStats() { return []; }
|
||||
addTrack(track: MockMediaStreamTrack) {return new MockRTCRtpSender(track);}
|
||||
}
|
||||
|
||||
class MockRTCRtpSender {
|
||||
constructor(public track: MockMediaStreamTrack) {}
|
||||
|
||||
replaceTrack(track: MockMediaStreamTrack) {this.track = track;}
|
||||
}
|
||||
|
||||
class MockMediaStreamTrack {
|
||||
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) {}
|
||||
|
||||
stop() {}
|
||||
}
|
||||
|
||||
class MockMediaStream {
|
||||
constructor(
|
||||
public id: string,
|
||||
private tracks: MockMediaStreamTrack[] = [],
|
||||
) {}
|
||||
|
||||
getTracks() { return []; }
|
||||
getAudioTracks() { return [{ enabled: true }]; }
|
||||
getVideoTracks() { return [{ enabled: true }]; }
|
||||
getTracks() { return this.tracks; }
|
||||
getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
|
||||
getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
|
||||
addEventListener() {}
|
||||
removeEventListener() { }
|
||||
addTrack(track: MockMediaStreamTrack) {this.tracks.push(track);}
|
||||
removeTrack(track: MockMediaStreamTrack) {this.tracks.splice(this.tracks.indexOf(track), 1);}
|
||||
}
|
||||
|
||||
class MockMediaDeviceInfo {
|
||||
@@ -102,7 +119,13 @@ class MockMediaDeviceInfo {
|
||||
}
|
||||
|
||||
class MockMediaHandler {
|
||||
getUserMediaStream() { return new MockMediaStream("mock_stream_from_media_handler"); }
|
||||
getUserMediaStream(audio: boolean, video: boolean) {
|
||||
const tracks = [];
|
||||
if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio"));
|
||||
if (video) tracks.push(new MockMediaStreamTrack("video_track", "video"));
|
||||
|
||||
return new MockMediaStream("mock_stream_from_media_handler", tracks);
|
||||
}
|
||||
stopUserMediaStream() {}
|
||||
}
|
||||
|
||||
@@ -350,7 +373,15 @@ describe('Call', function() {
|
||||
},
|
||||
});
|
||||
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream"));
|
||||
call.pushRemoteFeed(
|
||||
new MockMediaStream(
|
||||
"remote_stream",
|
||||
[
|
||||
new MockMediaStreamTrack("remote_audio_track", "audio"),
|
||||
new MockMediaStreamTrack("remote_video_track", "video"),
|
||||
],
|
||||
),
|
||||
);
|
||||
const feed = call.getFeeds().find((feed) => feed.stream.id === "remote_stream");
|
||||
expect(feed?.purpose).toBe(SDPStreamMetadataPurpose.Usermedia);
|
||||
expect(feed?.isAudioMuted()).toBeTruthy();
|
||||
@@ -396,4 +427,82 @@ describe('Call', function() {
|
||||
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(1, true, true);
|
||||
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(2, true, false);
|
||||
});
|
||||
|
||||
it("should handle mid-call device changes", async () => {
|
||||
client.client.mediaHandler.getUserMediaStream = jest.fn().mockReturnValue(
|
||||
new MockMediaStream(
|
||||
"stream", [
|
||||
new MockMediaStreamTrack("audio_track", "audio"),
|
||||
new MockMediaStreamTrack("video_track", "video"),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
const callPromise = call.placeVideoCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await call.updateLocalUsermediaStream(
|
||||
new MockMediaStream(
|
||||
"replacement_stream",
|
||||
[
|
||||
new MockMediaStreamTrack("new_audio_track", "audio"),
|
||||
new MockMediaStreamTrack("video_track", "video"),
|
||||
],
|
||||
),
|
||||
);
|
||||
expect(call.localUsermediaStream.id).toBe("stream");
|
||||
expect(call.localUsermediaStream.getAudioTracks()[0].id).toBe("new_audio_track");
|
||||
expect(call.localUsermediaStream.getVideoTracks()[0].id).toBe("video_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "audio";
|
||||
}).track.id).toBe("new_audio_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "video";
|
||||
}).track.id).toBe("video_track");
|
||||
});
|
||||
|
||||
it("should handle upgrade to video call", async () => {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
[SDPStreamMetadataKey]: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await call.upgradeCall(false, true);
|
||||
|
||||
expect(call.localUsermediaStream.getAudioTracks()[0].id).toBe("audio_track");
|
||||
expect(call.localUsermediaStream.getVideoTracks()[0].id).toBe("video_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "audio";
|
||||
}).track.id).toBe("audio_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "video";
|
||||
}).track.id).toBe("video_track");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// disable lint because these are wire responses
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
/**
|
||||
* Represents a response to the CSAPI `/refresh` endpoint.
|
||||
*/
|
||||
export interface IRefreshTokenResponse {
|
||||
access_token: string;
|
||||
expires_in_ms: number;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
@@ -92,6 +92,7 @@ export enum EventType {
|
||||
export enum RelationType {
|
||||
Annotation = "m.annotation",
|
||||
Replace = "m.replace",
|
||||
Reference = "m.reference",
|
||||
/**
|
||||
* Note, "io.element.thread" is hardcoded
|
||||
* Should be replaced with "m.thread" once MSC3440 lands
|
||||
|
||||
@@ -27,7 +27,10 @@ export const ASSET_NODE_TYPE = new UnstableValue("m.asset", "org.matrix.msc3488.
|
||||
|
||||
export const TIMESTAMP_NODE_TYPE = new UnstableValue("m.ts", "org.matrix.msc3488.ts");
|
||||
|
||||
export const ASSET_TYPE_SELF = "m.self";
|
||||
export enum LocationAssetType {
|
||||
Self = "m.self",
|
||||
Pin = "m.pin",
|
||||
}
|
||||
|
||||
/* From the spec at:
|
||||
* https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
|
||||
@@ -60,7 +63,7 @@ export interface ILocationContent extends IContent {
|
||||
description?: string;
|
||||
};
|
||||
[ASSET_NODE_TYPE.name]: {
|
||||
type: string;
|
||||
type: LocationAssetType;
|
||||
};
|
||||
[TEXT_NODE_TYPE.name]: string;
|
||||
[TIMESTAMP_NODE_TYPE.name]: number;
|
||||
|
||||
@@ -21,28 +21,6 @@ import { IStrippedState } from "../sync-accumulator";
|
||||
// Types relating to Rooms of type `m.space` and related APIs
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
/** @deprecated Use hierarchy instead where possible. */
|
||||
export interface ISpaceSummaryRoom extends IPublicRoomsChunkRoom {
|
||||
num_refs: number;
|
||||
room_type: string;
|
||||
}
|
||||
|
||||
/** @deprecated Use hierarchy instead where possible. */
|
||||
export interface ISpaceSummaryEvent {
|
||||
room_id: string;
|
||||
event_id: string;
|
||||
origin_server_ts: number;
|
||||
type: string;
|
||||
state_key: string;
|
||||
sender: string;
|
||||
content: {
|
||||
order?: string;
|
||||
suggested?: boolean;
|
||||
auto_join?: boolean;
|
||||
via?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IHierarchyRelation extends IStrippedState {
|
||||
origin_server_ts: number;
|
||||
content: {
|
||||
|
||||
+21
-6
@@ -16,16 +16,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { ListenerMap, TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
|
||||
export class ReEmitter {
|
||||
private target: EventEmitter;
|
||||
constructor(private readonly target: EventEmitter) {}
|
||||
|
||||
constructor(target: EventEmitter) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
reEmit(source: EventEmitter, eventNames: string[]) {
|
||||
public reEmit(source: EventEmitter, eventNames: string[]): void {
|
||||
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
|
||||
@@ -48,3 +47,19 @@ export class ReEmitter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TypedReEmitter<
|
||||
Events extends string,
|
||||
Arguments extends ListenerMap<Events>,
|
||||
> extends ReEmitter {
|
||||
constructor(target: TypedEventEmitter<Events, Arguments>) {
|
||||
super(target);
|
||||
}
|
||||
|
||||
public reEmit<ReEmittedEvents extends string, T extends Events & ReEmittedEvents>(
|
||||
source: TypedEventEmitter<ReEmittedEvents, any>,
|
||||
eventNames: T[],
|
||||
): void {
|
||||
super.reEmit(source, eventNames);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@ import queryString from "qs";
|
||||
|
||||
import * as matrixcs from "./matrix";
|
||||
|
||||
if (matrixcs.getRequest()) {
|
||||
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
|
||||
}
|
||||
|
||||
matrixcs.request(function(opts, fn) {
|
||||
// We manually fix the query string for browser-request because
|
||||
// it doesn't correctly handle cases like ?via=one&via=two. Instead
|
||||
|
||||
+307
-141
@@ -19,15 +19,22 @@ limitations under the License.
|
||||
* @module client
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { EmoteEvent, MessageEvent, NoticeEvent, IPartialEvent } from "matrix-events-sdk";
|
||||
import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent } from "matrix-events-sdk";
|
||||
|
||||
import { ISyncStateData, SyncApi, SyncState } from "./sync";
|
||||
import { EventStatus, IContent, IDecryptOptions, IEvent, MatrixEvent } from "./models/event";
|
||||
import {
|
||||
EventStatus,
|
||||
IContent,
|
||||
IDecryptOptions,
|
||||
IEvent,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
MatrixEventHandlerMap,
|
||||
} from "./models/event";
|
||||
import { StubStore } from "./store/stub";
|
||||
import { createNewMatrixCall, MatrixCall } from "./webrtc/call";
|
||||
import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall } from "./webrtc/call";
|
||||
import { Filter, IFilterDefinition } from "./filter";
|
||||
import { CallEventHandler } from './webrtc/callEventHandler';
|
||||
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
|
||||
import * as utils from './utils';
|
||||
import { sleep } from './utils';
|
||||
import { Group } from "./models/group";
|
||||
@@ -37,12 +44,12 @@ import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
|
||||
import * as olmlib from "./crypto/olmlib";
|
||||
import { decodeBase64, encodeBase64 } from "./crypto/olmlib";
|
||||
import { IExportedDevice as IOlmDevice } from "./crypto/OlmDevice";
|
||||
import { ReEmitter } from './ReEmitter';
|
||||
import { TypedReEmitter } from './ReEmitter';
|
||||
import { IRoomEncryption, RoomList } from './crypto/RoomList';
|
||||
import { logger } from './logger';
|
||||
import { SERVICE_TYPES } from './service-types';
|
||||
import {
|
||||
FileType,
|
||||
FileType, HttpApiEvent, HttpApiEventHandlerMap,
|
||||
IHttpOpts,
|
||||
IUpload,
|
||||
MatrixError,
|
||||
@@ -52,11 +59,14 @@ import {
|
||||
PREFIX_MEDIA_R0,
|
||||
PREFIX_R0,
|
||||
PREFIX_UNSTABLE,
|
||||
PREFIX_V1,
|
||||
retryNetworkOperation,
|
||||
UploadContentResponseType,
|
||||
} from "./http-api";
|
||||
import {
|
||||
Crypto,
|
||||
CryptoEvent,
|
||||
CryptoEventHandlerMap,
|
||||
fixBackupKey,
|
||||
IBootstrapCrossSigningOpts,
|
||||
ICheckOwnCrossSigningTrustOpts,
|
||||
@@ -67,7 +77,7 @@ import {
|
||||
import { DeviceInfo, IDevice } from "./crypto/deviceinfo";
|
||||
import { decodeRecoveryKey } from './crypto/recoverykey';
|
||||
import { keyFromAuthData } from './crypto/key_passphrase';
|
||||
import { User } from "./models/user";
|
||||
import { User, UserEvent, UserEventHandlerMap } from "./models/user";
|
||||
import { getHttpUriForMxc } from "./content-repo";
|
||||
import { SearchResult } from "./models/search-result";
|
||||
import {
|
||||
@@ -94,6 +104,12 @@ import {
|
||||
IRoomEvent,
|
||||
IStateEvent,
|
||||
NotificationCountType,
|
||||
RoomEvent,
|
||||
RoomEventHandlerMap,
|
||||
RoomMemberEvent,
|
||||
RoomMemberEventHandlerMap,
|
||||
RoomStateEvent,
|
||||
RoomStateEventHandlerMap,
|
||||
} from "./matrix";
|
||||
import {
|
||||
CrossSigningKey,
|
||||
@@ -155,11 +171,13 @@ import {
|
||||
SearchOrderBy,
|
||||
} from "./@types/search";
|
||||
import { ISynapseAdminDeactivateResponse, ISynapseAdminWhoisResponse } from "./@types/synapse";
|
||||
import { IHierarchyRoom, ISpaceSummaryEvent, ISpaceSummaryRoom } from "./@types/spaces";
|
||||
import { IHierarchyRoom } from "./@types/spaces";
|
||||
import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, RuleId } from "./@types/PushRules";
|
||||
import { IThreepid } from "./@types/threepids";
|
||||
import { CryptoStore } from "./crypto/store/base";
|
||||
import { MediaHandler } from "./webrtc/mediaHandler";
|
||||
import { IRefreshTokenResponse } from "./@types/auth";
|
||||
import { TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
|
||||
export type Store = IStore;
|
||||
export type SessionStore = WebStorageSessionStore;
|
||||
@@ -458,7 +476,7 @@ export interface ISignedKey {
|
||||
}
|
||||
|
||||
export type KeySignatures = Record<string, Record<string, ICrossSigningKey | ISignedKey>>;
|
||||
interface IUploadKeySignaturesResponse {
|
||||
export interface IUploadKeySignaturesResponse {
|
||||
failures: Record<string, Record<string, {
|
||||
errcode: string;
|
||||
error: string;
|
||||
@@ -492,7 +510,7 @@ interface ITurnServer {
|
||||
}
|
||||
|
||||
interface IServerVersions {
|
||||
versions: string;
|
||||
versions: string[];
|
||||
unstable_features: Record<string, boolean>;
|
||||
}
|
||||
|
||||
@@ -752,15 +770,109 @@ interface ITimestampToEventResponse {
|
||||
// Probably not the most graceful solution but does a good enough job for now
|
||||
const EVENT_ID_PREFIX = "$";
|
||||
|
||||
export enum ClientEvent {
|
||||
Sync = "sync",
|
||||
Event = "event",
|
||||
ToDeviceEvent = "toDeviceEvent",
|
||||
AccountData = "accountData",
|
||||
Room = "Room",
|
||||
DeleteRoom = "deleteRoom",
|
||||
SyncUnexpectedError = "sync.unexpectedError",
|
||||
ClientWellKnown = "WellKnown.client",
|
||||
/* @deprecated */
|
||||
Group = "Group",
|
||||
// The following enum members are both deprecated and in the wrong place, Groups haven't been TSified
|
||||
GroupProfile = "Group.profile",
|
||||
GroupMyMembership = "Group.myMembership",
|
||||
}
|
||||
|
||||
type RoomEvents = RoomEvent.Name
|
||||
| RoomEvent.Redaction
|
||||
| RoomEvent.RedactionCancelled
|
||||
| RoomEvent.Receipt
|
||||
| RoomEvent.Tags
|
||||
| RoomEvent.LocalEchoUpdated
|
||||
| RoomEvent.AccountData
|
||||
| RoomEvent.MyMembership
|
||||
| RoomEvent.Timeline
|
||||
| RoomEvent.TimelineReset;
|
||||
|
||||
type RoomStateEvents = RoomStateEvent.Events
|
||||
| RoomStateEvent.Members
|
||||
| RoomStateEvent.NewMember
|
||||
| RoomStateEvent.Update
|
||||
;
|
||||
|
||||
type CryptoEvents = CryptoEvent.KeySignatureUploadFailure
|
||||
| CryptoEvent.KeyBackupStatus
|
||||
| CryptoEvent.KeyBackupFailed
|
||||
| CryptoEvent.KeyBackupSessionsRemaining
|
||||
| CryptoEvent.RoomKeyRequest
|
||||
| CryptoEvent.RoomKeyRequestCancellation
|
||||
| CryptoEvent.VerificationRequest
|
||||
| CryptoEvent.DeviceVerificationChanged
|
||||
| CryptoEvent.UserTrustStatusChanged
|
||||
| CryptoEvent.KeysChanged
|
||||
| CryptoEvent.Warning
|
||||
| CryptoEvent.DevicesUpdated
|
||||
| CryptoEvent.WillUpdateDevices;
|
||||
|
||||
type MatrixEventEvents = MatrixEventEvent.Decrypted | MatrixEventEvent.Replaced | MatrixEventEvent.VisibilityChange;
|
||||
|
||||
type RoomMemberEvents = RoomMemberEvent.Name
|
||||
| RoomMemberEvent.Typing
|
||||
| RoomMemberEvent.PowerLevel
|
||||
| RoomMemberEvent.Membership;
|
||||
|
||||
type UserEvents = UserEvent.AvatarUrl
|
||||
| UserEvent.DisplayName
|
||||
| UserEvent.Presence
|
||||
| UserEvent.CurrentlyActive
|
||||
| UserEvent.LastPresenceTs;
|
||||
|
||||
type EmittedEvents = ClientEvent
|
||||
| RoomEvents
|
||||
| RoomStateEvents
|
||||
| CryptoEvents
|
||||
| MatrixEventEvents
|
||||
| RoomMemberEvents
|
||||
| UserEvents
|
||||
| CallEvent // re-emitted by call.ts using Object.values
|
||||
| CallEventHandlerEvent.Incoming
|
||||
| HttpApiEvent.SessionLoggedOut
|
||||
| HttpApiEvent.NoConsent;
|
||||
|
||||
export type ClientEventHandlerMap = {
|
||||
[ClientEvent.Sync]: (state: SyncState, lastState?: SyncState, data?: ISyncStateData) => void;
|
||||
[ClientEvent.Event]: (event: MatrixEvent) => void;
|
||||
[ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void;
|
||||
[ClientEvent.AccountData]: (event: MatrixEvent, lastEvent?: MatrixEvent) => void;
|
||||
[ClientEvent.Room]: (room: Room) => void;
|
||||
[ClientEvent.DeleteRoom]: (roomId: string) => void;
|
||||
[ClientEvent.SyncUnexpectedError]: (error: Error) => void;
|
||||
[ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void;
|
||||
[ClientEvent.Group]: (group: Group) => void;
|
||||
[ClientEvent.GroupProfile]: (group: Group) => void;
|
||||
[ClientEvent.GroupMyMembership]: (group: Group) => void;
|
||||
} & RoomEventHandlerMap
|
||||
& RoomStateEventHandlerMap
|
||||
& CryptoEventHandlerMap
|
||||
& MatrixEventHandlerMap
|
||||
& RoomMemberEventHandlerMap
|
||||
& UserEventHandlerMap
|
||||
& CallEventHandlerEventHandlerMap
|
||||
& CallEventHandlerMap
|
||||
& HttpApiEventHandlerMap;
|
||||
|
||||
/**
|
||||
* Represents a Matrix Client. Only directly construct this if you want to use
|
||||
* custom modules. Normally, {@link createClient} should be used
|
||||
* as it specifies 'sensible' defaults for these modules.
|
||||
*/
|
||||
export class MatrixClient extends EventEmitter {
|
||||
export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHandlerMap> {
|
||||
public static readonly RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
|
||||
|
||||
public reEmitter = new ReEmitter(this);
|
||||
public reEmitter = new TypedReEmitter<EmittedEvents, ClientEventHandlerMap>(this);
|
||||
public olmVersion: [number, number, number] = null; // populated after initCrypto
|
||||
public usingExternalCrypto = false;
|
||||
public store: Store;
|
||||
@@ -822,7 +934,7 @@ export class MatrixClient extends EventEmitter {
|
||||
protected checkTurnServersIntervalID: number;
|
||||
protected exportedOlmDeviceToImport: IOlmDevice;
|
||||
protected txnCtr = 0;
|
||||
protected mediaHandler = new MediaHandler();
|
||||
protected mediaHandler = new MediaHandler(this);
|
||||
protected pendingEventEncryption = new Map<string, Promise<void>>();
|
||||
|
||||
constructor(opts: IMatrixClientCreateOpts) {
|
||||
@@ -841,7 +953,7 @@ export class MatrixClient extends EventEmitter {
|
||||
const userId = opts.userId || null;
|
||||
this.credentials = { userId };
|
||||
|
||||
this.http = new MatrixHttpApi(this, {
|
||||
this.http = new MatrixHttpApi(this as ConstructorParameters<typeof MatrixHttpApi>[0], {
|
||||
baseUrl: opts.baseUrl,
|
||||
idBaseUrl: opts.idBaseUrl,
|
||||
accessToken: opts.accessToken,
|
||||
@@ -902,7 +1014,7 @@ export class MatrixClient extends EventEmitter {
|
||||
// Start listening for calls after the initial sync is done
|
||||
// We do not need to backfill the call event buffer
|
||||
// with encrypted events that might never get decrypted
|
||||
this.on("sync", this.startCallEventHandler);
|
||||
this.on(ClientEvent.Sync, this.startCallEventHandler);
|
||||
}
|
||||
|
||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||
@@ -927,7 +1039,7 @@ export class MatrixClient extends EventEmitter {
|
||||
// actions for themselves, so we have to kinda help them out when they are encrypted.
|
||||
// We do this so that push rules are correctly executed on events in their decrypted
|
||||
// state, such as highlights when the user's name is mentioned.
|
||||
this.on("Event.decrypted", (event) => {
|
||||
this.on(MatrixEventEvent.Decrypted, (event) => {
|
||||
const oldActions = event.getPushActions();
|
||||
const actions = this.getPushActionsForEvent(event, true);
|
||||
|
||||
@@ -962,7 +1074,7 @@ export class MatrixClient extends EventEmitter {
|
||||
// Like above, we have to listen for read receipts from ourselves in order to
|
||||
// correctly handle notification counts on encrypted rooms.
|
||||
// This fixes https://github.com/vector-im/element-web/issues/9421
|
||||
this.on("Room.receipt", (event, room) => {
|
||||
this.on(RoomEvent.Receipt, (event, room) => {
|
||||
if (room && this.isRoomEncrypted(room.roomId)) {
|
||||
// Figure out if we've read something or if it's just informational
|
||||
const content = event.getContent();
|
||||
@@ -997,7 +1109,7 @@ export class MatrixClient extends EventEmitter {
|
||||
|
||||
// Note: we don't need to handle 'total' notifications because the counts
|
||||
// will come from the server.
|
||||
room.setUnreadNotificationCount("highlight", highlightCount);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1562,16 +1674,16 @@ export class MatrixClient extends EventEmitter {
|
||||
);
|
||||
|
||||
this.reEmitter.reEmit(crypto, [
|
||||
"crypto.keyBackupFailed",
|
||||
"crypto.keyBackupSessionsRemaining",
|
||||
"crypto.roomKeyRequest",
|
||||
"crypto.roomKeyRequestCancellation",
|
||||
"crypto.warning",
|
||||
"crypto.devicesUpdated",
|
||||
"crypto.willUpdateDevices",
|
||||
"deviceVerificationChanged",
|
||||
"userTrustStatusChanged",
|
||||
"crossSigning.keysChanged",
|
||||
CryptoEvent.KeyBackupFailed,
|
||||
CryptoEvent.KeyBackupSessionsRemaining,
|
||||
CryptoEvent.RoomKeyRequest,
|
||||
CryptoEvent.RoomKeyRequestCancellation,
|
||||
CryptoEvent.Warning,
|
||||
CryptoEvent.DevicesUpdated,
|
||||
CryptoEvent.WillUpdateDevices,
|
||||
CryptoEvent.DeviceVerificationChanged,
|
||||
CryptoEvent.UserTrustStatusChanged,
|
||||
CryptoEvent.KeysChanged,
|
||||
]);
|
||||
|
||||
logger.log("Crypto: initialising crypto object...");
|
||||
@@ -1583,9 +1695,8 @@ export class MatrixClient extends EventEmitter {
|
||||
|
||||
this.olmVersion = Crypto.getOlmVersion();
|
||||
|
||||
// if crypto initialisation was successful, tell it to attach its event
|
||||
// handlers.
|
||||
crypto.registerEventHandlers(this);
|
||||
// if crypto initialisation was successful, tell it to attach its event handlers.
|
||||
crypto.registerEventHandlers(this as Parameters<Crypto["registerEventHandlers"]>[0]);
|
||||
this.crypto = crypto;
|
||||
}
|
||||
|
||||
@@ -1825,7 +1936,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @returns {Verification} a verification object
|
||||
* @deprecated Use `requestVerification` instead.
|
||||
*/
|
||||
public beginKeyVerification(method: string, userId: string, deviceId: string): Verification {
|
||||
public beginKeyVerification(method: string, userId: string, deviceId: string): Verification<any, any> {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
@@ -3159,9 +3270,9 @@ export class MatrixClient extends EventEmitter {
|
||||
* has been emitted. Note in particular that other events, eg. RoomState.members
|
||||
* will be emitted for a room before this function will return the given room.
|
||||
* @param {string} roomId The room ID
|
||||
* @return {Room} The Room or null if it doesn't exist or there is no data store.
|
||||
* @return {Room|null} The Room or null if it doesn't exist or there is no data store.
|
||||
*/
|
||||
public getRoom(roomId: string): Room {
|
||||
public getRoom(roomId: string): Room | null {
|
||||
return this.store.getRoom(roomId);
|
||||
}
|
||||
|
||||
@@ -3594,17 +3705,20 @@ export class MatrixClient extends EventEmitter {
|
||||
threadId = null;
|
||||
}
|
||||
|
||||
if (threadId && content["m.relates_to"]?.rel_type !== RelationType.Thread) {
|
||||
// If we expect that an event is part of a thread but is missing the relation
|
||||
// we need to add it manually, as well as the reply fallback
|
||||
if (threadId && !content["m.relates_to"]?.rel_type) {
|
||||
content["m.relates_to"] = {
|
||||
...content["m.relates_to"],
|
||||
"rel_type": RelationType.Thread,
|
||||
"event_id": threadId,
|
||||
};
|
||||
|
||||
const thread = this.getRoom(roomId)?.threads.get(threadId);
|
||||
if (thread) {
|
||||
content["m.relates_to"]["m.in_reply_to"] = {
|
||||
"event_id": thread.replyToEvent.getId(),
|
||||
"event_id": thread.lastReply((ev: MatrixEvent) => {
|
||||
return ev.isThreadRelation && !ev.status;
|
||||
})?.getId(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3652,6 +3766,7 @@ export class MatrixClient extends EventEmitter {
|
||||
const thread = room?.threads.get(threadId);
|
||||
if (thread) {
|
||||
localEvent.setThread(thread);
|
||||
localEvent.setThreadId(thread.id);
|
||||
}
|
||||
|
||||
// if this is a relation or redaction of an event
|
||||
@@ -3659,9 +3774,9 @@ export class MatrixClient extends EventEmitter {
|
||||
// then listen for the remote echo of that event so that by the time
|
||||
// this event does get sent, we have the correct event_id
|
||||
const targetId = localEvent.getAssociatedId();
|
||||
if (targetId && targetId.startsWith("~")) {
|
||||
if (targetId?.startsWith("~")) {
|
||||
const target = room.getPendingEvents().find(e => e.getId() === targetId);
|
||||
target.once("Event.localEventIdReplaced", () => {
|
||||
target.once(MatrixEventEvent.LocalEventIdReplaced, () => {
|
||||
localEvent.updateAssociatedId(target.getId());
|
||||
});
|
||||
}
|
||||
@@ -4759,7 +4874,7 @@ export class MatrixClient extends EventEmitter {
|
||||
}
|
||||
return promise.then((response) => {
|
||||
this.store.removeRoom(roomId);
|
||||
this.emit("deleteRoom", roomId);
|
||||
this.emit(ClientEvent.DeleteRoom, roomId);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
@@ -4912,7 +5027,7 @@ export class MatrixClient extends EventEmitter {
|
||||
const user = this.getUser(this.getUserId());
|
||||
if (user) {
|
||||
user.displayName = name;
|
||||
user.emit("User.displayName", user.events.presence, user);
|
||||
user.emit(UserEvent.DisplayName, user.events.presence, user);
|
||||
}
|
||||
return prom;
|
||||
}
|
||||
@@ -4929,7 +5044,7 @@ export class MatrixClient extends EventEmitter {
|
||||
const user = this.getUser(this.getUserId());
|
||||
if (user) {
|
||||
user.avatarUrl = url;
|
||||
user.emit("User.avatarUrl", user.events.presence, user);
|
||||
user.emit(UserEvent.AvatarUrl, user.events.presence, user);
|
||||
}
|
||||
return prom;
|
||||
}
|
||||
@@ -5082,7 +5197,7 @@ export class MatrixClient extends EventEmitter {
|
||||
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
|
||||
|
||||
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
|
||||
await this.processThreadEvents(room, threadedEvents);
|
||||
await this.processThreadEvents(room, threadedEvents, true);
|
||||
|
||||
room.oldState.paginationToken = res.end;
|
||||
if (res.chunk.length === 0) {
|
||||
@@ -5193,7 +5308,7 @@ export class MatrixClient extends EventEmitter {
|
||||
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
|
||||
|
||||
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
|
||||
await this.processThreadEvents(timelineSet.room, threadedEvents);
|
||||
await this.processThreadEvents(timelineSet.room, threadedEvents, true);
|
||||
|
||||
// there is no guarantee that the event ended up in "timeline" (we
|
||||
// might have switched to a neighbouring timeline) - so check the
|
||||
@@ -5326,7 +5441,7 @@ export class MatrixClient extends EventEmitter {
|
||||
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||
await this.processThreadEvents(timelineSet.room, threadedEvents);
|
||||
await this.processThreadEvents(timelineSet.room, threadedEvents, backwards);
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||
@@ -5364,7 +5479,7 @@ export class MatrixClient extends EventEmitter {
|
||||
|
||||
eventTimeline.getTimelineSet()
|
||||
.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||
await this.processThreadEvents(room, threadedEvents);
|
||||
await this.processThreadEvents(room, threadedEvents, backwards);
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||
@@ -6099,7 +6214,7 @@ export class MatrixClient extends EventEmitter {
|
||||
private startCallEventHandler = (): void => {
|
||||
if (this.isInitialSyncComplete()) {
|
||||
this.callEventHandler.start();
|
||||
this.off("sync", this.startCallEventHandler);
|
||||
this.off(ClientEvent.Sync, this.startCallEventHandler);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6247,7 +6362,7 @@ export class MatrixClient extends EventEmitter {
|
||||
// it absorbs errors and returns `{}`.
|
||||
this.clientWellKnownPromise = AutoDiscovery.getRawClientConfig(this.getDomain());
|
||||
this.clientWellKnown = await this.clientWellKnownPromise;
|
||||
this.emit("WellKnown.client", this.clientWellKnown);
|
||||
this.emit(ClientEvent.ClientWellKnown, this.clientWellKnown);
|
||||
}
|
||||
|
||||
public getClientWellKnown(): IClientWellKnown {
|
||||
@@ -6485,8 +6600,8 @@ export class MatrixClient extends EventEmitter {
|
||||
public async relations(
|
||||
roomId: string,
|
||||
eventId: string,
|
||||
relationType: RelationType | string | null,
|
||||
eventType: EventType | string | null,
|
||||
relationType?: RelationType | string | null,
|
||||
eventType?: EventType | string | null,
|
||||
opts: IRelationsRequestOpts = {},
|
||||
): Promise<{
|
||||
originalEvent: MatrixEvent;
|
||||
@@ -6509,12 +6624,10 @@ export class MatrixClient extends EventEmitter {
|
||||
let events = result.chunk.map(mapper);
|
||||
if (fetchedEventType === EventType.RoomMessageEncrypted) {
|
||||
const allEvents = originalEvent ? events.concat(originalEvent) : events;
|
||||
await Promise.all(allEvents.map(e => {
|
||||
if (e.isEncrypted()) {
|
||||
return new Promise(resolve => e.once("Event.decrypted", resolve));
|
||||
}
|
||||
}));
|
||||
events = events.filter(e => e.getType() === eventType);
|
||||
await Promise.all(allEvents.map(e => this.decryptEventIfNeeded(e)));
|
||||
if (eventType !== null) {
|
||||
events = events.filter(e => e.getType() === eventType);
|
||||
}
|
||||
}
|
||||
if (originalEvent && relationType === RelationType.Replace) {
|
||||
events = events.filter(e => e.getSender() === originalEvent.getSender());
|
||||
@@ -6615,6 +6728,14 @@ export class MatrixClient extends EventEmitter {
|
||||
return this.http.opts.accessToken || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the access token associated with this account.
|
||||
* @param {string} token The new access token.
|
||||
*/
|
||||
public setAccessToken(token: string) {
|
||||
this.http.opts.accessToken = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean} true if there is a valid access_token for this client.
|
||||
*/
|
||||
@@ -6691,6 +6812,7 @@ export class MatrixClient extends EventEmitter {
|
||||
|
||||
const params: any = {
|
||||
auth: auth,
|
||||
refresh_token: true, // always ask for a refresh token - does nothing if unsupported
|
||||
};
|
||||
if (username !== undefined && username !== null) {
|
||||
params.username = username;
|
||||
@@ -6768,6 +6890,31 @@ export class MatrixClient extends EventEmitter {
|
||||
return this.http.request(callback, Method.Post, "/register", params, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes an access token using a provided refresh token. The refresh token
|
||||
* must be valid for the current access token known to the client instance.
|
||||
*
|
||||
* Note that this function will not cause a logout if the token is deemed
|
||||
* unknown by the server - the caller is responsible for managing logout
|
||||
* actions on error.
|
||||
* @param {string} refreshToken The refresh token.
|
||||
* @return {Promise<IRefreshTokenResponse>} Resolves to the new token.
|
||||
* @return {module:http-api.MatrixError} Rejects with an error response.
|
||||
*/
|
||||
public refreshToken(refreshToken: string): Promise<IRefreshTokenResponse> {
|
||||
return this.http.authedRequest(
|
||||
undefined,
|
||||
Method.Post,
|
||||
"/refresh",
|
||||
undefined,
|
||||
{ refresh_token: refreshToken },
|
||||
{
|
||||
prefix: PREFIX_V1,
|
||||
inhibitLogoutEmit: true, // we don't want to cause logout loops
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {Promise} Resolves: TODO
|
||||
@@ -6881,7 +7028,17 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {Promise} Resolves: On success, the empty object
|
||||
*/
|
||||
public logout(callback?: Callback): Promise<{}> {
|
||||
public async logout(callback?: Callback): Promise<{}> {
|
||||
if (this.crypto?.backupManager?.getKeyBackupEnabled()) {
|
||||
try {
|
||||
while (await this.crypto.backupManager.backupPendingKeys(200) > 0);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
"Key backup request failed when logging out. Some keys may be missing from backup",
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
return this.http.authedRequest(
|
||||
callback, Method.Post, '/logout',
|
||||
);
|
||||
@@ -6991,8 +7148,16 @@ export class MatrixClient extends EventEmitter {
|
||||
const queryString = utils.encodeParams(opts as Record<string, string | number>);
|
||||
|
||||
let templatedUrl = "/rooms/$roomId/relations/$eventId";
|
||||
if (relationType !== null) templatedUrl += "/$relationType";
|
||||
if (eventType !== null) templatedUrl += "/$eventType";
|
||||
if (relationType !== null) {
|
||||
templatedUrl += "/$relationType";
|
||||
if (eventType !== null) {
|
||||
templatedUrl += "/$eventType";
|
||||
}
|
||||
} else if (eventType !== null) {
|
||||
logger.warn(`eventType: ${eventType} ignored when fetching
|
||||
relations as relationType is null`);
|
||||
eventType = null;
|
||||
}
|
||||
|
||||
const path = utils.encodeUri(
|
||||
templatedUrl + "?" + queryString, {
|
||||
@@ -8530,40 +8695,6 @@ export class MatrixClient extends EventEmitter {
|
||||
return this.http.authedRequest(undefined, Method.Post, path, null, { score, reason });
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches or paginates a summary of a space as defined by an initial version of MSC2946
|
||||
* @param {string} roomId The ID of the space-room to use as the root of the summary.
|
||||
* @param {number?} maxRoomsPerSpace The maximum number of rooms to return per subspace.
|
||||
* @param {boolean?} suggestedOnly Whether to only return rooms with suggested=true.
|
||||
* @param {boolean?} autoJoinOnly Whether to only return rooms with auto_join=true.
|
||||
* @param {number?} limit The maximum number of rooms to return in total.
|
||||
* @param {string?} batch The opaque token to paginate a previous summary request.
|
||||
* @returns {Promise} the response, with next_token, rooms fields.
|
||||
* @deprecated in favour of `getRoomHierarchy` due to the MSC changing paths.
|
||||
*/
|
||||
public getSpaceSummary(
|
||||
roomId: string,
|
||||
maxRoomsPerSpace?: number,
|
||||
suggestedOnly?: boolean,
|
||||
autoJoinOnly?: boolean,
|
||||
limit?: number,
|
||||
batch?: string,
|
||||
): Promise<{rooms: ISpaceSummaryRoom[], events: ISpaceSummaryEvent[]}> {
|
||||
const path = utils.encodeUri("/rooms/$roomId/spaces", {
|
||||
$roomId: roomId,
|
||||
});
|
||||
|
||||
return this.http.authedRequest(undefined, Method.Post, path, null, {
|
||||
max_rooms_per_space: maxRoomsPerSpace,
|
||||
suggested_only: suggestedOnly,
|
||||
auto_join_only: autoJoinOnly,
|
||||
limit,
|
||||
batch,
|
||||
}, {
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2946",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches or paginates a room hierarchy as defined by MSC2946.
|
||||
* Falls back gracefully to sourcing its data from `getSpaceSummary` if this API is not yet supported by the server.
|
||||
@@ -8585,30 +8716,21 @@ export class MatrixClient extends EventEmitter {
|
||||
$roomId: roomId,
|
||||
});
|
||||
|
||||
return this.http.authedRequest<IRoomHierarchy>(undefined, Method.Get, path, {
|
||||
const queryParams: Record<string, string | string[]> = {
|
||||
suggested_only: String(suggestedOnly),
|
||||
max_depth: maxDepth?.toString(),
|
||||
from: fromToken,
|
||||
limit: limit?.toString(),
|
||||
}, undefined, {
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2946",
|
||||
};
|
||||
|
||||
return this.http.authedRequest<IRoomHierarchy>(undefined, Method.Get, path, queryParams, undefined, {
|
||||
prefix: PREFIX_V1,
|
||||
}).catch(e => {
|
||||
if (e.errcode === "M_UNRECOGNIZED") {
|
||||
// fall back to the older space summary API as it exposes the same data just in a different shape.
|
||||
return this.getSpaceSummary(roomId, undefined, suggestedOnly, undefined, limit)
|
||||
.then(({ rooms, events }) => {
|
||||
// Translate response from `/spaces` to that we expect in this API.
|
||||
const roomMap = new Map(rooms.map(r => {
|
||||
return [r.room_id, <IHierarchyRoom>{ ...r, children_state: [] }];
|
||||
}));
|
||||
events.forEach(e => {
|
||||
roomMap.get(e.room_id)?.children_state.push(e);
|
||||
});
|
||||
|
||||
return {
|
||||
rooms: Array.from(roomMap.values()),
|
||||
};
|
||||
});
|
||||
// fall back to the prefixed hierarchy API.
|
||||
return this.http.authedRequest<IRoomHierarchy>(undefined, Method.Get, path, queryParams, undefined, {
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2946",
|
||||
});
|
||||
}
|
||||
|
||||
throw e;
|
||||
@@ -9079,6 +9201,60 @@ export class MatrixClient extends EventEmitter {
|
||||
return threadRoots;
|
||||
}
|
||||
|
||||
private eventShouldLiveIn(event: MatrixEvent, room: Room, events: MatrixEvent[], roots: Set<string>): {
|
||||
shouldLiveInRoom: boolean;
|
||||
shouldLiveInThread: boolean;
|
||||
threadId?: string;
|
||||
} {
|
||||
if (event.isThreadRoot) {
|
||||
return {
|
||||
shouldLiveInRoom: true,
|
||||
shouldLiveInThread: true,
|
||||
threadId: event.getId(),
|
||||
};
|
||||
}
|
||||
|
||||
// A thread relation is always only shown in a thread
|
||||
if (event.isThreadRelation) {
|
||||
return {
|
||||
shouldLiveInRoom: false,
|
||||
shouldLiveInThread: true,
|
||||
threadId: event.relationEventId,
|
||||
};
|
||||
}
|
||||
|
||||
const parentEventId = event.getAssociatedId();
|
||||
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => (
|
||||
mxEv.getId() === parentEventId
|
||||
));
|
||||
|
||||
// A reaction targetting the thread root needs to be routed to both the
|
||||
// the main timeline and the associated thread
|
||||
const targetingThreadRoot = parentEvent?.isThreadRoot || roots.has(event.relationEventId);
|
||||
if (targetingThreadRoot) {
|
||||
return {
|
||||
shouldLiveInRoom: true,
|
||||
shouldLiveInThread: true,
|
||||
threadId: event.relationEventId,
|
||||
};
|
||||
}
|
||||
|
||||
// If the parent event also has an associated ID we want to re-run the
|
||||
// computation for that parent event.
|
||||
// In the case of the redaction of a reaction that targets a root event
|
||||
// we want that redaction to be pushed to both timeline
|
||||
if (parentEvent?.getAssociatedId()) {
|
||||
return this.eventShouldLiveIn(parentEvent, room, events, roots);
|
||||
} else {
|
||||
// We've exhausted all scenarios, can safely assume that this event
|
||||
// should live in the room timeline
|
||||
return {
|
||||
shouldLiveInRoom: true,
|
||||
shouldLiveInThread: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public partitionThreadedEvents(events: MatrixEvent[]): [MatrixEvent[], MatrixEvent[]] {
|
||||
// Indices to the events array, for readibility
|
||||
const ROOM = 0;
|
||||
@@ -9087,35 +9263,22 @@ export class MatrixClient extends EventEmitter {
|
||||
const threadRoots = this.findThreadRoots(events);
|
||||
return events.reduce((memo, event: MatrixEvent) => {
|
||||
const room = this.getRoom(event.getRoomId());
|
||||
// An event should live in the thread timeline if
|
||||
// - It's a reply in thread event
|
||||
// - It's related to a reply in thread event
|
||||
let shouldLiveInThreadTimeline = event.isThreadRelation;
|
||||
if (!shouldLiveInThreadTimeline) {
|
||||
const parentEventId = event.parentEventId;
|
||||
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => {
|
||||
return mxEv.getId() === parentEventId;
|
||||
});
|
||||
const targetingThreadRoot = parentEvent?.isThreadRoot || threadRoots.has(event.relationEventId);
|
||||
|
||||
if (targetingThreadRoot && !event.isThreadRelation && event.relationEventId) {
|
||||
// If we refer to the thread root, we should be copied
|
||||
// into the thread as well as the main timeline.
|
||||
// This happens for reactions, annotations, poll votes etc.
|
||||
const copiedEvent = event.toSnapshot();
|
||||
const {
|
||||
shouldLiveInRoom,
|
||||
shouldLiveInThread,
|
||||
threadId,
|
||||
} = this.eventShouldLiveIn(event, room, events, threadRoots);
|
||||
|
||||
// The copied event is in this thread:
|
||||
copiedEvent.setThreadId(parentEventId);
|
||||
memo[THREAD].push(copiedEvent);
|
||||
} else if (parentEvent?.isThreadRelation) {
|
||||
// If our parent is in a thread, we are in that
|
||||
// same thread too. (E.g. if I reply within a thread.)
|
||||
shouldLiveInThreadTimeline = true;
|
||||
event.setThreadId(parentEvent.threadRootId);
|
||||
}
|
||||
if (shouldLiveInRoom) {
|
||||
memo[ROOM].push(event);
|
||||
}
|
||||
const targetTimeline = shouldLiveInThreadTimeline ? THREAD : ROOM;
|
||||
memo[targetTimeline].push(event);
|
||||
|
||||
if (shouldLiveInThread) {
|
||||
event.setThreadId(threadId);
|
||||
memo[THREAD].push(event);
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, [[], []]);
|
||||
} else {
|
||||
@@ -9131,10 +9294,13 @@ export class MatrixClient extends EventEmitter {
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public async processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): Promise<void> {
|
||||
threadedEvents.sort((a, b) => a.getTs() - b.getTs());
|
||||
public async processThreadEvents(
|
||||
room: Room,
|
||||
threadedEvents: MatrixEvent[],
|
||||
toStartOfTimeline: boolean,
|
||||
): Promise<void> {
|
||||
for (const event of threadedEvents) {
|
||||
await room.addThreadedEvent(event);
|
||||
await room.addThreadedEvent(event, toStartOfTimeline);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ import { MsgType } from "./@types/event";
|
||||
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
|
||||
import {
|
||||
ASSET_NODE_TYPE,
|
||||
ASSET_TYPE_SELF,
|
||||
ILocationContent,
|
||||
LocationAssetType,
|
||||
LOCATION_EVENT_TYPE,
|
||||
TIMESTAMP_NODE_TYPE,
|
||||
} from "./@types/location";
|
||||
@@ -121,7 +121,7 @@ export function makeLocationContent(
|
||||
uri: string,
|
||||
ts: number,
|
||||
description?: string,
|
||||
assetType?: string,
|
||||
assetType?: LocationAssetType,
|
||||
): ILocationContent {
|
||||
return {
|
||||
"body": text,
|
||||
@@ -132,7 +132,7 @@ export function makeLocationContent(
|
||||
description,
|
||||
},
|
||||
[ASSET_NODE_TYPE.name]: {
|
||||
type: assetType ?? ASSET_TYPE_SELF,
|
||||
type: assetType ?? LocationAssetType.Self,
|
||||
},
|
||||
[TEXT_NODE_TYPE.name]: text,
|
||||
[TIMESTAMP_NODE_TYPE.name]: ts,
|
||||
|
||||
@@ -19,7 +19,6 @@ limitations under the License.
|
||||
* @module crypto/CrossSigning
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { PkSigning } from "@matrix-org/olm";
|
||||
|
||||
import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib';
|
||||
@@ -55,7 +54,7 @@ export interface ICrossSigningInfo {
|
||||
crossSigningVerifiedBefore: boolean;
|
||||
}
|
||||
|
||||
export class CrossSigningInfo extends EventEmitter {
|
||||
export class CrossSigningInfo {
|
||||
public keys: Record<string, ICrossSigningKey> = {};
|
||||
public firstUse = true;
|
||||
// This tracks whether we've ever verified this user with any identity.
|
||||
@@ -79,9 +78,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
public readonly userId: string,
|
||||
private callbacks: ICryptoCallbacks = {},
|
||||
private cacheCallbacks: ICacheCallbacks = {},
|
||||
) {
|
||||
super();
|
||||
}
|
||||
) {}
|
||||
|
||||
public static fromStorage(obj: ICrossSigningInfo, userId: string): CrossSigningInfo {
|
||||
const res = new CrossSigningInfo(userId);
|
||||
|
||||
@@ -20,8 +20,6 @@ limitations under the License.
|
||||
* Manages the list of other users' devices
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { DeviceInfo, IDevice } from './deviceinfo';
|
||||
import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning';
|
||||
@@ -31,6 +29,8 @@ import { chunkPromises, defer, IDeferred, sleep } from '../utils';
|
||||
import { IDownloadKeyResult, MatrixClient } from "../client";
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { CryptoStore } from "./store/base";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { CryptoEvent, CryptoEventHandlerMap } from "./index";
|
||||
|
||||
/* State transition diagram for DeviceList.deviceTrackingStatus
|
||||
*
|
||||
@@ -62,10 +62,12 @@ export enum TrackingStatus {
|
||||
|
||||
export type DeviceInfoMap = Record<string, Record<string, DeviceInfo>>;
|
||||
|
||||
type EmittedEvents = CryptoEvent.WillUpdateDevices | CryptoEvent.DevicesUpdated | CryptoEvent.UserCrossSigningUpdated;
|
||||
|
||||
/**
|
||||
* @alias module:crypto/DeviceList
|
||||
*/
|
||||
export class DeviceList extends EventEmitter {
|
||||
export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHandlerMap> {
|
||||
private devices: { [userId: string]: { [deviceId: string]: IDevice } } = {};
|
||||
|
||||
public crossSigningInfo: { [userId: string]: ICrossSigningInfo } = {};
|
||||
@@ -634,7 +636,7 @@ export class DeviceList extends EventEmitter {
|
||||
});
|
||||
|
||||
const finished = (success: boolean): void => {
|
||||
this.emit("crypto.willUpdateDevices", users, !this.hasFetched);
|
||||
this.emit(CryptoEvent.WillUpdateDevices, users, !this.hasFetched);
|
||||
users.forEach((u) => {
|
||||
this.dirty = true;
|
||||
|
||||
@@ -659,7 +661,7 @@ export class DeviceList extends EventEmitter {
|
||||
}
|
||||
});
|
||||
this.saveIfDirty();
|
||||
this.emit("crypto.devicesUpdated", users, !this.hasFetched);
|
||||
this.emit(CryptoEvent.DevicesUpdated, users, !this.hasFetched);
|
||||
this.hasFetched = true;
|
||||
};
|
||||
|
||||
@@ -867,7 +869,7 @@ class DeviceListUpdateSerialiser {
|
||||
|
||||
// NB. Unlike most events in the js-sdk, this one is internal to the
|
||||
// js-sdk and is not re-emitted
|
||||
this.deviceList.emit('userCrossSigningUpdated', userId);
|
||||
this.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,17 +14,25 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { Method, PREFIX_UNSTABLE } from "../http-api";
|
||||
import { Crypto, IBootstrapCrossSigningOpts } from "./index";
|
||||
import { CrossSigningKeys, ICrossSigningKey, ICryptoCallbacks, ISignedKey, KeySignatures } from "../matrix";
|
||||
import {
|
||||
ClientEvent,
|
||||
CrossSigningKeys,
|
||||
ClientEventHandlerMap,
|
||||
ICrossSigningKey,
|
||||
ICryptoCallbacks,
|
||||
ISignedKey,
|
||||
KeySignatures,
|
||||
} from "../matrix";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { IAccountDataClient } from "./SecretStorage";
|
||||
|
||||
interface ICrossSigningKeys {
|
||||
authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
|
||||
@@ -256,7 +264,10 @@ export class EncryptionSetupOperation {
|
||||
* Catches account data set by SecretStorage during bootstrapping by
|
||||
* implementing the methods related to account data in MatrixClient
|
||||
*/
|
||||
class AccountDataClientAdapter extends EventEmitter {
|
||||
class AccountDataClientAdapter
|
||||
extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap>
|
||||
implements IAccountDataClient {
|
||||
//
|
||||
public readonly values = new Map<string, MatrixEvent>();
|
||||
|
||||
/**
|
||||
@@ -303,7 +314,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
||||
// and it seems to rely on this.
|
||||
return Promise.resolve().then(() => {
|
||||
const event = new MatrixEvent({ type, content });
|
||||
this.emit("accountData", event, lastEvent);
|
||||
this.emit(ClientEvent.AccountData, event, lastEvent);
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'stream';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import * as olmlib from './olmlib';
|
||||
import { encodeBase64 } from './olmlib';
|
||||
import { randomString } from '../randomstring';
|
||||
import { encryptAES, decryptAES, IEncryptedPayload, calculateKeyCheck } from './aes';
|
||||
import { encodeBase64 } from "./olmlib";
|
||||
import { ICryptoCallbacks, MatrixClient, MatrixEvent } from '../matrix';
|
||||
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes';
|
||||
import { ClientEvent, ICryptoCallbacks, MatrixEvent } from '../matrix';
|
||||
import { ClientEventHandlerMap, MatrixClient } from "../client";
|
||||
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
|
||||
import { TypedEventEmitter } from '../models/typed-event-emitter';
|
||||
|
||||
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
|
||||
|
||||
@@ -36,7 +36,7 @@ export interface ISecretRequest {
|
||||
cancel: (reason: string) => void;
|
||||
}
|
||||
|
||||
export interface IAccountDataClient extends EventEmitter {
|
||||
export interface IAccountDataClient extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap> {
|
||||
// Subset of MatrixClient (which also uses any for the event content)
|
||||
getAccountDataFromServer: <T extends {[k: string]: any}>(eventType: string) => Promise<T>;
|
||||
getAccountData: (eventType: string) => MatrixEvent;
|
||||
@@ -98,17 +98,17 @@ export class SecretStorage {
|
||||
ev.getType() === 'm.secret_storage.default_key' &&
|
||||
ev.getContent().key === keyId
|
||||
) {
|
||||
this.accountDataAdapter.removeListener('accountData', listener);
|
||||
this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
this.accountDataAdapter.on('accountData', listener);
|
||||
this.accountDataAdapter.on(ClientEvent.AccountData, listener);
|
||||
|
||||
this.accountDataAdapter.setAccountData(
|
||||
'm.secret_storage.default_key',
|
||||
{ key: keyId },
|
||||
).catch(e => {
|
||||
this.accountDataAdapter.removeListener('accountData', listener);
|
||||
this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
|
||||
+28
-16
@@ -26,14 +26,13 @@ import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { DeviceTrustLevel } from './CrossSigning';
|
||||
import { keyFromPassphrase } from './key_passphrase';
|
||||
import { sleep } from "../utils";
|
||||
import { getCrypto, sleep } from "../utils";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { encodeRecoveryKey } from './recoverykey';
|
||||
import { encryptAES, decryptAES, calculateKeyCheck } from './aes';
|
||||
import { getCrypto } from '../utils';
|
||||
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
|
||||
import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
|
||||
import { IAes256AuthData, ICurve25519AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { IMegolmSessionData } from "./index";
|
||||
import { CryptoEvent, IMegolmSessionData } from "./index";
|
||||
|
||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
|
||||
@@ -155,7 +154,7 @@ export class BackupManager {
|
||||
|
||||
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
|
||||
|
||||
this.baseApis.emit('crypto.keyBackupStatus', true);
|
||||
this.baseApis.emit(CryptoEvent.KeyBackupStatus, true);
|
||||
|
||||
// There may be keys left over from a partially completed backup, so
|
||||
// schedule a send to check.
|
||||
@@ -173,7 +172,7 @@ export class BackupManager {
|
||||
|
||||
this.backupInfo = undefined;
|
||||
|
||||
this.baseApis.emit('crypto.keyBackupStatus', false);
|
||||
this.baseApis.emit(CryptoEvent.KeyBackupStatus, false);
|
||||
}
|
||||
|
||||
public getKeyBackupEnabled(): boolean | null {
|
||||
@@ -313,11 +312,24 @@ export class BackupManager {
|
||||
return ret;
|
||||
}
|
||||
|
||||
const trustedPubkey = this.baseApis.crypto.sessionStore.getLocalTrustedBackupPubKey();
|
||||
const privKey = await this.baseApis.crypto.getSessionBackupPrivateKey();
|
||||
if (privKey) {
|
||||
let algorithm;
|
||||
try {
|
||||
algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => privKey);
|
||||
|
||||
if ("public_key" in backupInfo.auth_data && backupInfo.auth_data.public_key === trustedPubkey) {
|
||||
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
|
||||
ret.trusted_locally = true;
|
||||
if (await algorithm.keyMatches(privKey)) {
|
||||
logger.info("Backup is trusted locally");
|
||||
ret.trusted_locally = true;
|
||||
}
|
||||
} catch {
|
||||
// do nothing -- if we have an error, then we don't mark it as
|
||||
// locally trusted
|
||||
} finally {
|
||||
if (algorithm) {
|
||||
algorithm.free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || {};
|
||||
@@ -445,7 +457,7 @@ export class BackupManager {
|
||||
await this.checkKeyBackup();
|
||||
// Backup version has changed or this backup version
|
||||
// has been deleted
|
||||
this.baseApis.crypto.emit("crypto.keyBackupFailed", err.data.errcode);
|
||||
this.baseApis.crypto.emit(CryptoEvent.KeyBackupFailed, err.data.errcode);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -467,14 +479,14 @@ export class BackupManager {
|
||||
* @param {integer} limit Maximum number of keys to back up
|
||||
* @returns {integer} Number of sessions backed up
|
||||
*/
|
||||
private async backupPendingKeys(limit: number): Promise<number> {
|
||||
public async backupPendingKeys(limit: number): Promise<number> {
|
||||
const sessions = await this.baseApis.crypto.cryptoStore.getSessionsNeedingBackup(limit);
|
||||
if (!sessions.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
|
||||
|
||||
const rooms: IKeyBackup["rooms"] = {};
|
||||
for (const session of sessions) {
|
||||
@@ -511,7 +523,7 @@ export class BackupManager {
|
||||
|
||||
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
||||
remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
|
||||
|
||||
return sessions.length;
|
||||
}
|
||||
@@ -567,7 +579,7 @@ export class BackupManager {
|
||||
);
|
||||
|
||||
const remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
this.baseApis.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
|
||||
return remaining;
|
||||
}
|
||||
|
||||
|
||||
+116
-60
@@ -22,27 +22,36 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { ReEmitter } from '../ReEmitter';
|
||||
import { TypedReEmitter } from '../ReEmitter';
|
||||
import { logger } from '../logger';
|
||||
import { IExportedDevice, OlmDevice } from "./OlmDevice";
|
||||
import * as olmlib from "./olmlib";
|
||||
import { DeviceInfoMap, DeviceList } from "./DeviceList";
|
||||
import { DeviceInfo, IDevice } from "./deviceinfo";
|
||||
import type { DecryptionAlgorithm, EncryptionAlgorithm } from "./algorithms";
|
||||
import * as algorithms from "./algorithms";
|
||||
import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning';
|
||||
import { EncryptionSetupBuilder } from "./EncryptionSetup";
|
||||
import {
|
||||
IAccountDataClient,
|
||||
ISecretRequest,
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
SecretStorage,
|
||||
SecretStorageKeyTuple,
|
||||
ISecretRequest,
|
||||
SecretStorageKeyObject,
|
||||
SecretStorageKeyTuple,
|
||||
} from './SecretStorage';
|
||||
import { IAddSecretStorageKeyOpts, ICreateSecretStorageOpts, IImportRoomKeysOpts, ISecretStorageKeyInfo } from "./api";
|
||||
import {
|
||||
IAddSecretStorageKeyOpts,
|
||||
ICreateSecretStorageOpts,
|
||||
IEncryptedEventInfo,
|
||||
IImportRoomKeysOpts,
|
||||
IRecoveryKey,
|
||||
ISecretStorageKeyInfo,
|
||||
} from "./api";
|
||||
import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager';
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { VerificationBase } from "./verification/Base";
|
||||
import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode';
|
||||
import { SAS as SASVerification } from './verification/SAS';
|
||||
import { keyFromPassphrase } from './key_passphrase';
|
||||
@@ -52,21 +61,28 @@ import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChan
|
||||
import { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel";
|
||||
import { IllegalMethod } from "./verification/IllegalMethod";
|
||||
import { KeySignatureUploadError } from "../errors";
|
||||
import { decryptAES, encryptAES, calculateKeyCheck } from './aes';
|
||||
import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
|
||||
import { DehydrationManager, IDeviceKeys, IOneTimeKey } from './dehydration';
|
||||
import { BackupManager } from "./backup";
|
||||
import { IStore } from "../store";
|
||||
import { Room } from "../models/room";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { MatrixEvent, EventStatus, IClearEvent, IEvent } from "../models/event";
|
||||
import { MatrixClient, IKeysUploadResponse, SessionStore, ISignedKey, ICrossSigningKey } from "../client";
|
||||
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
|
||||
import { Room, RoomEvent } from "../models/room";
|
||||
import { RoomMember, RoomMemberEvent } from "../models/room-member";
|
||||
import { EventStatus, IClearEvent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event";
|
||||
import {
|
||||
ClientEvent,
|
||||
ICrossSigningKey,
|
||||
IKeysUploadResponse,
|
||||
ISignedKey,
|
||||
IUploadKeySignaturesResponse,
|
||||
MatrixClient,
|
||||
SessionStore,
|
||||
} from "../client";
|
||||
import type { IRoomEncryption, RoomList } from "./RoomList";
|
||||
import { IRecoveryKey, IEncryptedEventInfo } from "./api";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { ISyncStateData } from "../sync";
|
||||
import { CryptoStore } from "./store/base";
|
||||
import { IVerificationChannel } from "./verification/request/Channel";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
@@ -186,7 +202,45 @@ export interface IRequestsMap {
|
||||
setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void;
|
||||
}
|
||||
|
||||
export class Crypto extends EventEmitter {
|
||||
export enum CryptoEvent {
|
||||
DeviceVerificationChanged = "deviceVerificationChanged",
|
||||
UserTrustStatusChanged = "userTrustStatusChanged",
|
||||
UserCrossSigningUpdated = "userCrossSigningUpdated",
|
||||
RoomKeyRequest = "crypto.roomKeyRequest",
|
||||
RoomKeyRequestCancellation = "crypto.roomKeyRequestCancellation",
|
||||
KeyBackupStatus = "crypto.keyBackupStatus",
|
||||
KeyBackupFailed = "crypto.keyBackupFailed",
|
||||
KeyBackupSessionsRemaining = "crypto.keyBackupSessionsRemaining",
|
||||
KeySignatureUploadFailure = "crypto.keySignatureUploadFailure",
|
||||
VerificationRequest = "crypto.verification.request",
|
||||
Warning = "crypto.warning",
|
||||
WillUpdateDevices = "crypto.willUpdateDevices",
|
||||
DevicesUpdated = "crypto.devicesUpdated",
|
||||
KeysChanged = "crossSigning.keysChanged",
|
||||
}
|
||||
|
||||
export type CryptoEventHandlerMap = {
|
||||
[CryptoEvent.DeviceVerificationChanged]: (userId: string, deviceId: string, device: DeviceInfo) => void;
|
||||
[CryptoEvent.UserTrustStatusChanged]: (userId: string, trustLevel: UserTrustLevel) => void;
|
||||
[CryptoEvent.RoomKeyRequest]: (request: IncomingRoomKeyRequest) => void;
|
||||
[CryptoEvent.RoomKeyRequestCancellation]: (request: IncomingRoomKeyRequestCancellation) => void;
|
||||
[CryptoEvent.KeyBackupStatus]: (enabled: boolean) => void;
|
||||
[CryptoEvent.KeyBackupFailed]: (errcode: string) => void;
|
||||
[CryptoEvent.KeyBackupSessionsRemaining]: (remaining: number) => void;
|
||||
[CryptoEvent.KeySignatureUploadFailure]: (
|
||||
failures: IUploadKeySignaturesResponse["failures"],
|
||||
source: "checkOwnCrossSigningTrust" | "afterCrossSigningLocalKeyChange" | "setDeviceVerification",
|
||||
upload: (opts: { shouldEmit: boolean }) => Promise<void>
|
||||
) => void;
|
||||
[CryptoEvent.VerificationRequest]: (request: VerificationRequest<any>) => void;
|
||||
[CryptoEvent.Warning]: (type: string) => void;
|
||||
[CryptoEvent.KeysChanged]: (data: {}) => void;
|
||||
[CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
|
||||
[CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
|
||||
[CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void;
|
||||
};
|
||||
|
||||
export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap> {
|
||||
/**
|
||||
* @return {string} The version of Olm.
|
||||
*/
|
||||
@@ -201,8 +255,8 @@ export class Crypto extends EventEmitter {
|
||||
public readonly dehydrationManager: DehydrationManager;
|
||||
public readonly secretStorage: SecretStorage;
|
||||
|
||||
private readonly reEmitter: ReEmitter;
|
||||
private readonly verificationMethods: any; // TODO types
|
||||
private readonly reEmitter: TypedReEmitter<CryptoEvent, CryptoEventHandlerMap>;
|
||||
private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>;
|
||||
public readonly supportedAlgorithms: string[];
|
||||
private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager;
|
||||
private readonly toDeviceVerificationRequests: ToDeviceRequests;
|
||||
@@ -295,10 +349,10 @@ export class Crypto extends EventEmitter {
|
||||
private readonly clientStore: IStore,
|
||||
public readonly cryptoStore: CryptoStore,
|
||||
private readonly roomList: RoomList,
|
||||
verificationMethods: any[], // TODO types
|
||||
verificationMethods: Array<keyof typeof defaultVerificationMethods | typeof VerificationBase>,
|
||||
) {
|
||||
super();
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
this.reEmitter = new TypedReEmitter(this);
|
||||
|
||||
if (verificationMethods) {
|
||||
this.verificationMethods = new Map();
|
||||
@@ -307,20 +361,21 @@ export class Crypto extends EventEmitter {
|
||||
if (defaultVerificationMethods[method]) {
|
||||
this.verificationMethods.set(
|
||||
method,
|
||||
defaultVerificationMethods[method],
|
||||
<typeof VerificationBase>defaultVerificationMethods[method],
|
||||
);
|
||||
}
|
||||
} else if (method.NAME) {
|
||||
} else if (method["NAME"]) {
|
||||
this.verificationMethods.set(
|
||||
method.NAME,
|
||||
method,
|
||||
method["NAME"],
|
||||
method as typeof VerificationBase,
|
||||
);
|
||||
} else {
|
||||
logger.warn(`Excluding unknown verification method ${method}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.verificationMethods = defaultVerificationMethods;
|
||||
this.verificationMethods =
|
||||
new Map(Object.entries(defaultVerificationMethods)) as Map<VerificationMethod, typeof VerificationBase>;
|
||||
}
|
||||
|
||||
this.backupManager = new BackupManager(baseApis, async () => {
|
||||
@@ -358,8 +413,8 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
// XXX: This isn't removed at any point, but then none of the event listeners
|
||||
// this class sets seem to be removed at any point... :/
|
||||
this.deviceList.on('userCrossSigningUpdated', this.onDeviceListUserCrossSigningUpdated);
|
||||
this.reEmitter.reEmit(this.deviceList, ["crypto.devicesUpdated", "crypto.willUpdateDevices"]);
|
||||
this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated);
|
||||
this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]);
|
||||
|
||||
this.supportedAlgorithms = Object.keys(algorithms.DECRYPTION_CLASSES);
|
||||
|
||||
@@ -375,7 +430,7 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks);
|
||||
// Yes, we pass the client twice here: see SecretStorage
|
||||
this.secretStorage = new SecretStorage(baseApis, cryptoCallbacks, baseApis);
|
||||
this.secretStorage = new SecretStorage(baseApis as IAccountDataClient, cryptoCallbacks, baseApis);
|
||||
this.dehydrationManager = new DehydrationManager(this);
|
||||
|
||||
// Assuming no app-supplied callback, default to getting from SSSS.
|
||||
@@ -487,7 +542,7 @@ export class Crypto extends EventEmitter {
|
||||
deviceTrust.isCrossSigningVerified()
|
||||
) {
|
||||
const deviceObj = this.deviceList.getStoredDevice(userId, deviceId);
|
||||
this.emit("deviceVerificationChanged", userId, deviceId, deviceObj);
|
||||
this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1165,7 +1220,7 @@ export class Crypto extends EventEmitter {
|
||||
if (Object.keys(failures || []).length > 0) {
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit(
|
||||
"crypto.keySignatureUploadFailure",
|
||||
CryptoEvent.KeySignatureUploadFailure,
|
||||
failures,
|
||||
"afterCrossSigningLocalKeyChange",
|
||||
upload, // continuation
|
||||
@@ -1391,11 +1446,10 @@ export class Crypto extends EventEmitter {
|
||||
// that reset the keys
|
||||
this.storeTrustedSelfKeys(null);
|
||||
// emit cross-signing has been disabled
|
||||
this.emit("crossSigning.keysChanged", {});
|
||||
this.emit(CryptoEvent.KeysChanged, {});
|
||||
// as the trust for our own user has changed,
|
||||
// also emit an event for this
|
||||
this.emit("userTrustStatusChanged",
|
||||
this.userId, this.checkUserTrust(userId));
|
||||
this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
|
||||
}
|
||||
} else {
|
||||
await this.checkDeviceVerifications(userId);
|
||||
@@ -1410,7 +1464,7 @@ export class Crypto extends EventEmitter {
|
||||
this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage());
|
||||
}
|
||||
|
||||
this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId));
|
||||
this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1567,7 +1621,7 @@ export class Crypto extends EventEmitter {
|
||||
if (Object.keys(failures || []).length > 0) {
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit(
|
||||
"crypto.keySignatureUploadFailure",
|
||||
CryptoEvent.KeySignatureUploadFailure,
|
||||
failures,
|
||||
"checkOwnCrossSigningTrust",
|
||||
upload,
|
||||
@@ -1585,10 +1639,10 @@ export class Crypto extends EventEmitter {
|
||||
upload({ shouldEmit: true });
|
||||
}
|
||||
|
||||
this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId));
|
||||
this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId));
|
||||
|
||||
if (masterChanged) {
|
||||
this.baseApis.emit("crossSigning.keysChanged", {});
|
||||
this.emit(CryptoEvent.KeysChanged, {});
|
||||
await this.afterCrossSigningLocalKeyChange();
|
||||
}
|
||||
|
||||
@@ -1675,18 +1729,14 @@ export class Crypto extends EventEmitter {
|
||||
* @param {external:EventEmitter} eventEmitter event source where we can register
|
||||
* for event notifications
|
||||
*/
|
||||
public registerEventHandlers(eventEmitter: EventEmitter): void {
|
||||
eventEmitter.on("RoomMember.membership", (event: MatrixEvent, member: RoomMember, oldMembership?: string) => {
|
||||
try {
|
||||
this.onRoomMembership(event, member, oldMembership);
|
||||
} catch (e) {
|
||||
logger.error("Error handling membership change:", e);
|
||||
}
|
||||
});
|
||||
|
||||
eventEmitter.on("toDeviceEvent", this.onToDeviceEvent);
|
||||
eventEmitter.on("Room.timeline", this.onTimelineEvent);
|
||||
eventEmitter.on("Event.decrypted", this.onTimelineEvent);
|
||||
public registerEventHandlers(eventEmitter: TypedEventEmitter<
|
||||
RoomMemberEvent.Membership | ClientEvent.ToDeviceEvent | RoomEvent.Timeline | MatrixEventEvent.Decrypted,
|
||||
any
|
||||
>): void {
|
||||
eventEmitter.on(RoomMemberEvent.Membership, this.onMembership);
|
||||
eventEmitter.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
||||
eventEmitter.on(RoomEvent.Timeline, this.onTimelineEvent);
|
||||
eventEmitter.on(MatrixEventEvent.Decrypted, this.onTimelineEvent);
|
||||
}
|
||||
|
||||
/** Start background processes related to crypto */
|
||||
@@ -2070,9 +2120,7 @@ export class Crypto extends EventEmitter {
|
||||
if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) {
|
||||
this.storeTrustedSelfKeys(xsk.keys);
|
||||
// This will cause our own user trust to change, so emit the event
|
||||
this.emit(
|
||||
"userTrustStatusChanged", this.userId, this.checkUserTrust(userId),
|
||||
);
|
||||
this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
|
||||
}
|
||||
|
||||
// Now sign the master key with our user signing key (unless it's ourself)
|
||||
@@ -2094,7 +2142,7 @@ export class Crypto extends EventEmitter {
|
||||
if (Object.keys(failures || []).length > 0) {
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit(
|
||||
"crypto.keySignatureUploadFailure",
|
||||
CryptoEvent.KeySignatureUploadFailure,
|
||||
failures,
|
||||
"setDeviceVerification",
|
||||
upload,
|
||||
@@ -2178,7 +2226,7 @@ export class Crypto extends EventEmitter {
|
||||
if (Object.keys(failures || []).length > 0) {
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit(
|
||||
"crypto.keySignatureUploadFailure",
|
||||
CryptoEvent.KeySignatureUploadFailure,
|
||||
failures,
|
||||
"setDeviceVerification",
|
||||
upload, // continuation
|
||||
@@ -2193,7 +2241,7 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
|
||||
const deviceObj = DeviceInfo.fromStorage(dev, deviceId);
|
||||
this.emit("deviceVerificationChanged", userId, deviceId, deviceObj);
|
||||
this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
|
||||
return deviceObj;
|
||||
}
|
||||
|
||||
@@ -3045,6 +3093,14 @@ export class Crypto extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string) => {
|
||||
try {
|
||||
this.onRoomMembership(event, member, oldMembership);
|
||||
} catch (e) {
|
||||
logger.error("Error handling membership change:", e);
|
||||
}
|
||||
};
|
||||
|
||||
private onToDeviceEvent = (event: MatrixEvent): void => {
|
||||
try {
|
||||
logger.log(`received to_device ${event.getType()} from: ` +
|
||||
@@ -3070,7 +3126,7 @@ export class Crypto extends EventEmitter {
|
||||
event.attemptDecryption(this);
|
||||
}
|
||||
// once the event has been decrypted, try again
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||
this.onToDeviceEvent(ev);
|
||||
});
|
||||
}
|
||||
@@ -3219,15 +3275,15 @@ export class Crypto extends EventEmitter {
|
||||
reject(new Error("Event status set to CANCELLED."));
|
||||
}
|
||||
};
|
||||
event.once("Event.localEventIdReplaced", eventIdListener);
|
||||
event.on("Event.status", statusListener);
|
||||
event.once(MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
|
||||
event.on(MatrixEventEvent.Status, statusListener);
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("error while waiting for the verification event to be sent: " + err.message);
|
||||
return;
|
||||
} finally {
|
||||
event.removeListener("Event.localEventIdReplaced", eventIdListener);
|
||||
event.removeListener("Event.status", statusListener);
|
||||
event.removeListener(MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
|
||||
event.removeListener(MatrixEventEvent.Status, statusListener);
|
||||
}
|
||||
}
|
||||
let request = requestsMap.getRequest(event);
|
||||
@@ -3254,7 +3310,7 @@ export class Crypto extends EventEmitter {
|
||||
!request.invalid && // check it has enough events to pass the UNSENT stage
|
||||
!request.observeOnly;
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit("crypto.verification.request", request);
|
||||
this.baseApis.emit(CryptoEvent.VerificationRequest, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3555,7 +3611,7 @@ export class Crypto extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit("crypto.roomKeyRequest", req);
|
||||
this.emit(CryptoEvent.RoomKeyRequest, req);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3574,7 +3630,7 @@ export class Crypto extends EventEmitter {
|
||||
// we should probably only notify the app of cancellations we told it
|
||||
// about, but we don't currently have a record of that, so we just pass
|
||||
// everything through.
|
||||
this.emit("crypto.roomKeyRequestCancellation", cancellation);
|
||||
this.emit(CryptoEvent.RoomKeyRequestCancellation, cancellation);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,8 +20,6 @@ limitations under the License.
|
||||
* @module crypto/verification/Base
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { MatrixEvent } from '../../models/event';
|
||||
import { logger } from '../../logger';
|
||||
import { DeviceInfo } from '../deviceinfo';
|
||||
@@ -30,6 +28,7 @@ import { KeysDuringVerification, requestKeysDuringVerification } from "../CrossS
|
||||
import { IVerificationChannel } from "./request/Channel";
|
||||
import { MatrixClient } from "../../client";
|
||||
import { VerificationRequest } from "./request/VerificationRequest";
|
||||
import { ListenerMap, TypedEventEmitter } from "../../models/typed-event-emitter";
|
||||
|
||||
const timeoutException = new Error("Verification timed out");
|
||||
|
||||
@@ -41,7 +40,18 @@ export class SwitchStartEventError extends Error {
|
||||
|
||||
export type KeyVerifier = (keyId: string, device: DeviceInfo, keyInfo: string) => void;
|
||||
|
||||
export class VerificationBase extends EventEmitter {
|
||||
export enum VerificationEvent {
|
||||
Cancel = "cancel",
|
||||
}
|
||||
|
||||
export type VerificationEventHandlerMap = {
|
||||
[VerificationEvent.Cancel]: (e: Error | MatrixEvent) => void;
|
||||
};
|
||||
|
||||
export class VerificationBase<
|
||||
Events extends string,
|
||||
Arguments extends ListenerMap<Events | VerificationEvent>,
|
||||
> extends TypedEventEmitter<Events | VerificationEvent, Arguments, VerificationEventHandlerMap> {
|
||||
private cancelled = false;
|
||||
private _done = false;
|
||||
private promise: Promise<void> = null;
|
||||
@@ -261,7 +271,7 @@ export class VerificationBase extends EventEmitter {
|
||||
}
|
||||
// Also emit a 'cancel' event that the app can listen for to detect cancellation
|
||||
// before calling verify()
|
||||
this.emit('cancel', e);
|
||||
this.emit(VerificationEvent.Cancel, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ limitations under the License.
|
||||
* @module crypto/verification/IllegalMethod
|
||||
*/
|
||||
|
||||
import { VerificationBase as Base } from "./Base";
|
||||
import { VerificationBase as Base, VerificationEvent, VerificationEventHandlerMap } from "./Base";
|
||||
import { IVerificationChannel } from "./request/Channel";
|
||||
import { MatrixClient } from "../../client";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
@@ -30,7 +30,7 @@ import { VerificationRequest } from "./request/VerificationRequest";
|
||||
* @class crypto/verification/IllegalMethod/IllegalMethod
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class IllegalMethod extends Base {
|
||||
export class IllegalMethod extends Base<VerificationEvent, VerificationEventHandlerMap> {
|
||||
public static factory(
|
||||
channel: IVerificationChannel,
|
||||
baseApis: MatrixClient,
|
||||
|
||||
@@ -19,7 +19,7 @@ limitations under the License.
|
||||
* @module crypto/verification/QRCode
|
||||
*/
|
||||
|
||||
import { VerificationBase as Base } from "./Base";
|
||||
import { VerificationBase as Base, VerificationEventHandlerMap } from "./Base";
|
||||
import { newKeyMismatchError, newUserCancelledError } from './Error';
|
||||
import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib";
|
||||
import { logger } from '../../logger';
|
||||
@@ -31,15 +31,25 @@ import { MatrixEvent } from "../../models/event";
|
||||
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
|
||||
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
|
||||
|
||||
interface IReciprocateQr {
|
||||
confirm(): void;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export enum QrCodeEvent {
|
||||
ShowReciprocateQr = "show_reciprocate_qr",
|
||||
}
|
||||
|
||||
type EventHandlerMap = {
|
||||
[QrCodeEvent.ShowReciprocateQr]: (qr: IReciprocateQr) => void;
|
||||
} & VerificationEventHandlerMap;
|
||||
|
||||
/**
|
||||
* @class crypto/verification/QRCode/ReciprocateQRCode
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class ReciprocateQRCode extends Base {
|
||||
public reciprocateQREvent: {
|
||||
confirm(): void;
|
||||
cancel(): void;
|
||||
};
|
||||
export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> {
|
||||
public reciprocateQREvent: IReciprocateQr;
|
||||
|
||||
public static factory(
|
||||
channel: IVerificationChannel,
|
||||
@@ -76,7 +86,7 @@ export class ReciprocateQRCode extends Base {
|
||||
confirm: resolve,
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
};
|
||||
this.emit("show_reciprocate_qr", this.reciprocateQREvent);
|
||||
this.emit(QrCodeEvent.ShowReciprocateQr, this.reciprocateQREvent);
|
||||
});
|
||||
|
||||
// 3. determine key to sign / mark as trusted
|
||||
|
||||
@@ -22,7 +22,7 @@ limitations under the License.
|
||||
import anotherjson from 'another-json';
|
||||
import { Utility, SAS as OlmSAS } from "@matrix-org/olm";
|
||||
|
||||
import { VerificationBase as Base, SwitchStartEventError } from "./Base";
|
||||
import { VerificationBase as Base, SwitchStartEventError, VerificationEventHandlerMap } from "./Base";
|
||||
import {
|
||||
errorFactory,
|
||||
newInvalidMessageError,
|
||||
@@ -232,11 +232,19 @@ function intersection<T>(anArray: T[], aSet: Set<T>): T[] {
|
||||
return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : [];
|
||||
}
|
||||
|
||||
export enum SasEvent {
|
||||
ShowSas = "show_sas",
|
||||
}
|
||||
|
||||
type EventHandlerMap = {
|
||||
[SasEvent.ShowSas]: (sas: ISasEvent) => void;
|
||||
} & VerificationEventHandlerMap;
|
||||
|
||||
/**
|
||||
* @alias module:crypto/verification/SAS
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class SAS extends Base {
|
||||
export class SAS extends Base<SasEvent, EventHandlerMap> {
|
||||
private waitingForAccept: boolean;
|
||||
public ourSASPubKey: string;
|
||||
public theirSASPubKey: string;
|
||||
@@ -371,7 +379,7 @@ export class SAS extends Base {
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
mismatch: () => reject(newMismatchedSASError()),
|
||||
};
|
||||
this.emit("show_sas", this.sasEvent);
|
||||
this.emit(SasEvent.ShowSas, this.sasEvent);
|
||||
});
|
||||
|
||||
[e] = await Promise.all([
|
||||
@@ -447,7 +455,7 @@ export class SAS extends Base {
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
mismatch: () => reject(newMismatchedSASError()),
|
||||
};
|
||||
this.emit("show_sas", this.sasEvent);
|
||||
this.emit(SasEvent.ShowSas, this.sasEvent);
|
||||
});
|
||||
|
||||
[e] = await Promise.all([
|
||||
|
||||
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { logger } from '../../../logger';
|
||||
import {
|
||||
errorFactory,
|
||||
@@ -29,6 +27,7 @@ import { MatrixClient } from "../../../client";
|
||||
import { MatrixEvent } from "../../../models/event";
|
||||
import { VerificationBase } from "../Base";
|
||||
import { VerificationMethod } from "../../index";
|
||||
import { TypedEventEmitter } from "../../../models/typed-event-emitter";
|
||||
|
||||
// How long after the event's timestamp that the request times out
|
||||
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
|
||||
@@ -76,13 +75,23 @@ interface ITransition {
|
||||
event?: MatrixEvent;
|
||||
}
|
||||
|
||||
export enum VerificationRequestEvent {
|
||||
Change = "change",
|
||||
}
|
||||
|
||||
type EventHandlerMap = {
|
||||
[VerificationRequestEvent.Change]: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* State machine for verification requests.
|
||||
* Things that differ based on what channel is used to
|
||||
* send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
|
||||
* @event "change" whenever the state of the request object has changed.
|
||||
*/
|
||||
export class VerificationRequest<C extends IVerificationChannel = IVerificationChannel> extends EventEmitter {
|
||||
export class VerificationRequest<
|
||||
C extends IVerificationChannel = IVerificationChannel,
|
||||
> extends TypedEventEmitter<VerificationRequestEvent, EventHandlerMap> {
|
||||
private eventsByUs = new Map<string, MatrixEvent>();
|
||||
private eventsByThem = new Map<string, MatrixEvent>();
|
||||
private _observeOnly = false;
|
||||
@@ -104,7 +113,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
private commonMethods: VerificationMethod[] = [];
|
||||
private _phase: Phase;
|
||||
private _cancellingUserId: string;
|
||||
private _verifier: VerificationBase;
|
||||
private _verifier: VerificationBase<any, any>;
|
||||
|
||||
constructor(
|
||||
public readonly channel: C,
|
||||
@@ -236,7 +245,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
}
|
||||
|
||||
/** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */
|
||||
public get verifier(): VerificationBase {
|
||||
public get verifier(): VerificationBase<any, any> {
|
||||
return this._verifier;
|
||||
}
|
||||
|
||||
@@ -410,7 +419,10 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
* @param {string?} targetDevice.deviceId the id of the device to direct this request to
|
||||
* @returns {VerifierBase} the verifier of the given method
|
||||
*/
|
||||
public beginKeyVerification(method: VerificationMethod, targetDevice: ITargetDevice = null): VerificationBase {
|
||||
public beginKeyVerification(
|
||||
method: VerificationMethod,
|
||||
targetDevice: ITargetDevice = null,
|
||||
): VerificationBase<any, any> {
|
||||
// need to allow also when unsent in case of to_device
|
||||
if (!this.observeOnly && !this._verifier) {
|
||||
const validStartPhase =
|
||||
@@ -453,7 +465,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
public async cancel({ reason = "User declined", code = "m.user" } = {}): Promise<void> {
|
||||
if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
|
||||
this._declining = true;
|
||||
this.emit("change");
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
if (this._verifier) {
|
||||
return this._verifier.cancel(errorFactory(code, reason)());
|
||||
} else {
|
||||
@@ -471,7 +483,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
|
||||
const methods = [...this.verificationMethods.keys()];
|
||||
this._accepting = true;
|
||||
this.emit("change");
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
await this.channel.send(READY_TYPE, { methods });
|
||||
}
|
||||
}
|
||||
@@ -495,12 +507,12 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
handled = true;
|
||||
}
|
||||
if (handled) {
|
||||
this.off("change", check);
|
||||
this.off(VerificationRequestEvent.Change, check);
|
||||
}
|
||||
return handled;
|
||||
};
|
||||
if (!check()) {
|
||||
this.on("change", check);
|
||||
this.on(VerificationRequestEvent.Change, check);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -508,7 +520,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
private setPhase(phase: Phase, notify = true): void {
|
||||
this._phase = phase;
|
||||
if (notify) {
|
||||
this.emit("change");
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -768,7 +780,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
// set phase as last thing as this emits the "change" event
|
||||
this.setPhase(phase);
|
||||
} else if (this._observeOnly !== wasObserveOnly) {
|
||||
this.emit("change");
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
}
|
||||
} finally {
|
||||
// log events we processed so we can see from rageshakes what events were added to a request
|
||||
@@ -880,7 +892,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
method: VerificationMethod,
|
||||
startEvent: MatrixEvent = null,
|
||||
targetDevice: ITargetDevice = null,
|
||||
): VerificationBase {
|
||||
): VerificationBase<any, any> {
|
||||
if (!targetDevice) {
|
||||
targetDevice = this.targetDevice;
|
||||
}
|
||||
|
||||
+12
-3
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "./client";
|
||||
import { IEvent, MatrixEvent } from "./models/event";
|
||||
import { IEvent, MatrixEvent, MatrixEventEvent } from "./models/event";
|
||||
|
||||
export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
|
||||
|
||||
@@ -30,10 +30,16 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
||||
|
||||
function mapper(plainOldJsObject: Partial<IEvent>) {
|
||||
const event = new MatrixEvent(plainOldJsObject);
|
||||
|
||||
const room = client.getRoom(event.getRoomId());
|
||||
if (room?.threads.has(event.getId())) {
|
||||
event.setThread(room.threads.get(event.getId()));
|
||||
}
|
||||
|
||||
if (event.isEncrypted()) {
|
||||
if (!preventReEmit) {
|
||||
client.reEmitter.reEmit(event, [
|
||||
"Event.decrypted",
|
||||
MatrixEventEvent.Decrypted,
|
||||
]);
|
||||
}
|
||||
if (decrypt) {
|
||||
@@ -41,7 +47,10 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
||||
}
|
||||
}
|
||||
if (!preventReEmit) {
|
||||
client.reEmitter.reEmit(event, ["Event.replaced", "Event.visibilityChange"]);
|
||||
client.reEmitter.reEmit(event, [
|
||||
MatrixEventEvent.Replaced,
|
||||
MatrixEventEvent.VisibilityChange,
|
||||
]);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
+12
-7
@@ -15,7 +15,10 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { RelationType } from "./@types/event";
|
||||
import { UNSTABLE_FILTER_RELATION_SENDERS, UNSTABLE_FILTER_RELATION_TYPES } from "./filter";
|
||||
import {
|
||||
UNSTABLE_FILTER_RELATED_BY_REL_TYPES,
|
||||
UNSTABLE_FILTER_RELATED_BY_SENDERS,
|
||||
} from "./filter";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
|
||||
/**
|
||||
@@ -48,7 +51,8 @@ export interface IFilterComponent {
|
||||
not_senders?: string[];
|
||||
contains_url?: boolean;
|
||||
limit?: number;
|
||||
[UNSTABLE_FILTER_RELATION_TYPES.name]?: Array<RelationType | string>;
|
||||
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]?: string[];
|
||||
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]?: Array<RelationType | string>;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -80,9 +84,10 @@ export class FilterComponent {
|
||||
// of performance
|
||||
// This should be improved when bundled relationships solve that problem
|
||||
const relationSenders = [];
|
||||
if (this.userId && relations?.[RelationType.Thread]?.current_user_participated) {
|
||||
if (this.userId && bundledRelationships?.[RelationType.Thread]?.current_user_participated) {
|
||||
relationSenders.push(this.userId);
|
||||
}
|
||||
|
||||
return this.checkFields(
|
||||
event.getRoomId(),
|
||||
event.getSender(),
|
||||
@@ -105,8 +110,8 @@ export class FilterComponent {
|
||||
senders: this.filterJson.senders || null,
|
||||
not_senders: this.filterJson.not_senders || [],
|
||||
contains_url: this.filterJson.contains_url || null,
|
||||
[UNSTABLE_FILTER_RELATION_SENDERS.name]: UNSTABLE_FILTER_RELATION_SENDERS.findIn(this.filterJson),
|
||||
[UNSTABLE_FILTER_RELATION_TYPES.name]: UNSTABLE_FILTER_RELATION_TYPES.findIn(this.filterJson),
|
||||
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: UNSTABLE_FILTER_RELATED_BY_SENDERS.findIn(this.filterJson),
|
||||
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: UNSTABLE_FILTER_RELATED_BY_REL_TYPES.findIn(this.filterJson),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -160,14 +165,14 @@ export class FilterComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
const relationTypesFilter = this.filterJson[UNSTABLE_FILTER_RELATION_TYPES.name];
|
||||
const relationTypesFilter = this.filterJson[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name];
|
||||
if (relationTypesFilter !== undefined) {
|
||||
if (!this.arrayMatchesFilter(relationTypesFilter, relationTypes)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const relationSendersFilter = this.filterJson[UNSTABLE_FILTER_RELATION_SENDERS.name];
|
||||
const relationSendersFilter = this.filterJson[UNSTABLE_FILTER_RELATED_BY_SENDERS.name];
|
||||
if (relationSendersFilter !== undefined) {
|
||||
if (!this.arrayMatchesFilter(relationSendersFilter, relationSenders)) {
|
||||
return false;
|
||||
|
||||
+6
-6
@@ -26,13 +26,13 @@ import { FilterComponent, IFilterComponent } from "./filter-component";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { UnstableValue } from "./NamespacedValue";
|
||||
|
||||
export const UNSTABLE_FILTER_RELATION_SENDERS = new UnstableValue(
|
||||
"relation_senders",
|
||||
export const UNSTABLE_FILTER_RELATED_BY_SENDERS = new UnstableValue(
|
||||
"related_by_senders",
|
||||
"io.element.relation_senders",
|
||||
);
|
||||
|
||||
export const UNSTABLE_FILTER_RELATION_TYPES = new UnstableValue(
|
||||
"relation_types",
|
||||
export const UNSTABLE_FILTER_RELATED_BY_REL_TYPES = new UnstableValue(
|
||||
"related_by_rel_types",
|
||||
"io.element.relation_types",
|
||||
);
|
||||
|
||||
@@ -66,8 +66,8 @@ export interface IRoomEventFilter extends IFilterComponent {
|
||||
lazy_load_members?: boolean;
|
||||
include_redundant_members?: boolean;
|
||||
types?: Array<EventType | string>;
|
||||
[UNSTABLE_FILTER_RELATION_TYPES.name]?: Array<RelationType | string>;
|
||||
[UNSTABLE_FILTER_RELATION_SENDERS.name]?: string[];
|
||||
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]?: Array<RelationType | string>;
|
||||
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]?: string[];
|
||||
}
|
||||
|
||||
interface IStateFilter extends IRoomEventFilter {}
|
||||
|
||||
+35
-10
@@ -21,7 +21,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { parse as parseContentType, ParsedMediaType } from "content-type";
|
||||
import EventEmitter from "events";
|
||||
|
||||
import type { IncomingHttpHeaders, IncomingMessage } from "http";
|
||||
import type { Request as _Request, CoreOptions } from "request";
|
||||
@@ -35,6 +34,7 @@ import { IDeferred } from "./utils";
|
||||
import { Callback } from "./client";
|
||||
import * as utils from "./utils";
|
||||
import { logger } from './logger';
|
||||
import { TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
|
||||
/*
|
||||
TODO:
|
||||
@@ -111,6 +111,12 @@ interface IRequestOpts<T> {
|
||||
json?: boolean; // defaults to true
|
||||
qsStringifyOptions?: CoreOptions["qsStringifyOptions"];
|
||||
bodyParser?(body: string): T;
|
||||
|
||||
// Set to true to prevent the request function from emitting
|
||||
// a Session.logged_out event. This is intended for use on
|
||||
// endpoints where M_UNKNOWN_TOKEN is a valid/notable error
|
||||
// response, such as with token refreshes.
|
||||
inhibitLogoutEmit?: boolean;
|
||||
}
|
||||
|
||||
export interface IUpload {
|
||||
@@ -158,6 +164,16 @@ export enum Method {
|
||||
|
||||
export type FileType = Document | XMLHttpRequestBodyInit;
|
||||
|
||||
export enum HttpApiEvent {
|
||||
SessionLoggedOut = "Session.logged_out",
|
||||
NoConsent = "no_consent",
|
||||
}
|
||||
|
||||
export type HttpApiEventHandlerMap = {
|
||||
[HttpApiEvent.SessionLoggedOut]: (err: MatrixError) => void;
|
||||
[HttpApiEvent.NoConsent]: (message: string, consentUri: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a MatrixHttpApi.
|
||||
* @constructor
|
||||
@@ -186,7 +202,10 @@ export type FileType = Document | XMLHttpRequestBodyInit;
|
||||
export class MatrixHttpApi {
|
||||
private uploads: IUpload[] = [];
|
||||
|
||||
constructor(private eventEmitter: EventEmitter, public readonly opts: IHttpOpts) {
|
||||
constructor(
|
||||
private eventEmitter: TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>,
|
||||
public readonly opts: IHttpOpts,
|
||||
) {
|
||||
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
|
||||
opts.onlyData = !!opts.onlyData;
|
||||
opts.useAuthorizationHeader = !!opts.useAuthorizationHeader;
|
||||
@@ -437,10 +456,20 @@ export class MatrixHttpApi {
|
||||
queryParams.filename = fileName;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = { "Content-Type": contentType };
|
||||
|
||||
// authedRequest uses `request` which is no longer maintained.
|
||||
// `request` has a bug where if the body is zero bytes then you get an error: `Argument error, options.body`.
|
||||
// See https://github.com/request/request/issues/920
|
||||
// if body looks like a byte array and empty then set the Content-Length explicitly as a workaround:
|
||||
if ((body as unknown as ArrayLike<number>).length === 0) {
|
||||
headers["Content-Length"] = "0";
|
||||
}
|
||||
|
||||
promise = this.authedRequest(
|
||||
opts.callback, Method.Post, "/upload", queryParams, body, {
|
||||
prefix: "/_matrix/media/r0",
|
||||
headers: { "Content-Type": contentType },
|
||||
headers,
|
||||
json: false,
|
||||
bodyParser,
|
||||
},
|
||||
@@ -586,14 +615,10 @@ export class MatrixHttpApi {
|
||||
const requestPromise = this.request<T, O>(callback, method, path, queryParams, data, requestOpts);
|
||||
|
||||
requestPromise.catch((err: MatrixError) => {
|
||||
if (err.errcode == 'M_UNKNOWN_TOKEN') {
|
||||
this.eventEmitter.emit("Session.logged_out", err);
|
||||
if (err.errcode == 'M_UNKNOWN_TOKEN' && !requestOpts?.inhibitLogoutEmit) {
|
||||
this.eventEmitter.emit(HttpApiEvent.SessionLoggedOut, err);
|
||||
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
|
||||
this.eventEmitter.emit(
|
||||
"no_consent",
|
||||
err.message,
|
||||
err.data.consent_uri,
|
||||
);
|
||||
this.eventEmitter.emit(HttpApiEvent.NoConsent, err.message, err.data.consent_uri);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ import * as matrixcs from "./matrix";
|
||||
import * as utils from "./utils";
|
||||
import { logger } from './logger';
|
||||
|
||||
if (matrixcs.getRequest()) {
|
||||
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
|
||||
}
|
||||
|
||||
matrixcs.request(request);
|
||||
|
||||
try {
|
||||
|
||||
@@ -48,6 +48,7 @@ export * from "./crypto/store/indexeddb-crypto-store";
|
||||
export * from "./content-repo";
|
||||
export * from './@types/event';
|
||||
export * from './@types/PushRules';
|
||||
export * from './@types/partials';
|
||||
export * from './@types/requests';
|
||||
export * from './@types/search';
|
||||
export * from './models/room-summary';
|
||||
|
||||
@@ -20,6 +20,7 @@ import { IContent, MatrixEvent } from "./event";
|
||||
import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
|
||||
import { EventTimeline } from "./event-timeline";
|
||||
import { FileType } from "../http-api";
|
||||
import type { ISendEventResponse } from "..";
|
||||
|
||||
/**
|
||||
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference
|
||||
@@ -164,14 +165,14 @@ export class MSC3089Branch {
|
||||
* @param {File | String | Buffer | ReadStream | Blob} encryptedContents The encrypted contents.
|
||||
* @param {Partial<IEncryptedFile>} info The encrypted file information.
|
||||
* @param {IContent} additionalContent Optional event content fields to include in the message.
|
||||
* @returns {Promise<void>} Resolves when uploaded.
|
||||
* @returns {Promise<ISendEventResponse>} Resolves to the file event's sent response.
|
||||
*/
|
||||
public async createNewVersion(
|
||||
name: string,
|
||||
encryptedContents: FileType,
|
||||
info: Partial<IEncryptedFile>,
|
||||
additionalContent?: IContent,
|
||||
): Promise<void> {
|
||||
): Promise<ISendEventResponse> {
|
||||
const fileEventResponse = await this.directory.createFile(name, encryptedContents, info, {
|
||||
...(additionalContent ?? {}),
|
||||
"m.new_content": true,
|
||||
@@ -193,6 +194,8 @@ export class MSC3089Branch {
|
||||
...(this.indexEvent.getContent()),
|
||||
active: false,
|
||||
}, this.id);
|
||||
|
||||
return fileEventResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Enum for event statuses.
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum EventStatus {
|
||||
/** The event was not sent and will no longer be retried. */
|
||||
NOT_SENT = "not_sent",
|
||||
|
||||
/** The message is being encrypted */
|
||||
ENCRYPTING = "encrypting",
|
||||
|
||||
/** The event is in the process of being sent. */
|
||||
SENDING = "sending",
|
||||
|
||||
/** The event is in a queue waiting to be sent. */
|
||||
QUEUED = "queued",
|
||||
|
||||
/** The event has been sent to the server, but we have not yet received the echo. */
|
||||
SENT = "sent",
|
||||
|
||||
/** The event was cancelled before it was successfully sent. */
|
||||
CANCELLED = "cancelled",
|
||||
}
|
||||
@@ -18,17 +18,15 @@ limitations under the License.
|
||||
* @module models/event-timeline-set
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { EventTimeline } from "./event-timeline";
|
||||
import { EventStatus, MatrixEvent } from "./event";
|
||||
import { EventStatus, MatrixEvent, MatrixEventEvent } from "./event";
|
||||
import { logger } from '../logger';
|
||||
import { Relations } from './relations';
|
||||
import { Room } from "./room";
|
||||
import { Room, RoomEvent } from "./room";
|
||||
import { Filter } from "../filter";
|
||||
import { EventType, RelationType } from "../@types/event";
|
||||
import { RoomState } from "./room-state";
|
||||
import { Thread } from "./thread";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
|
||||
// var DEBUG = false;
|
||||
const DEBUG = true;
|
||||
@@ -58,7 +56,15 @@ export interface IRoomTimelineData {
|
||||
liveEvent?: boolean;
|
||||
}
|
||||
|
||||
export class EventTimelineSet extends EventEmitter {
|
||||
type EmittedEvents = RoomEvent.Timeline | RoomEvent.TimelineReset;
|
||||
|
||||
export type EventTimelineSetHandlerMap = {
|
||||
[RoomEvent.Timeline]:
|
||||
(event: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean, data: IRoomTimelineData) => void;
|
||||
[RoomEvent.TimelineReset]: (room: Room, eventTimelineSet: EventTimelineSet, resetAllTimelines: boolean) => void;
|
||||
};
|
||||
|
||||
export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTimelineSetHandlerMap> {
|
||||
private readonly timelineSupport: boolean;
|
||||
private unstableClientRelationAggregation: boolean;
|
||||
private displayPendingEvents: boolean;
|
||||
@@ -159,17 +165,12 @@ export class EventTimelineSet extends EventEmitter {
|
||||
*
|
||||
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
|
||||
*/
|
||||
public getPendingEvents(thread?: Thread): MatrixEvent[] {
|
||||
public getPendingEvents(): MatrixEvent[] {
|
||||
if (!this.room || !this.displayPendingEvents) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pendingEvents = this.room.getPendingEvents(thread);
|
||||
if (this.filter) {
|
||||
return this.filter.filterRoomTimeline(pendingEvents);
|
||||
} else {
|
||||
return pendingEvents;
|
||||
}
|
||||
return this.room.getPendingEvents();
|
||||
}
|
||||
/**
|
||||
* Get the live timeline for this room.
|
||||
@@ -253,7 +254,7 @@ export class EventTimelineSet extends EventEmitter {
|
||||
|
||||
// Now we can swap the live timeline to the new one.
|
||||
this.liveTimeline = newTimeline;
|
||||
this.emit("Room.timelineReset", this.room, this, resetAllTimelines);
|
||||
this.emit(RoomEvent.TimelineReset, this.room, this, resetAllTimelines);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -603,8 +604,7 @@ export class EventTimelineSet extends EventEmitter {
|
||||
timeline: timeline,
|
||||
liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache,
|
||||
};
|
||||
this.emit("Room.timeline", event, this.room,
|
||||
Boolean(toStartOfTimeline), false, data);
|
||||
this.emit(RoomEvent.Timeline, event, this.room, Boolean(toStartOfTimeline), false, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -658,7 +658,7 @@ export class EventTimelineSet extends EventEmitter {
|
||||
const data = {
|
||||
timeline: timeline,
|
||||
};
|
||||
this.emit("Room.timeline", removed, this.room, undefined, true, data);
|
||||
this.emit(RoomEvent.Timeline, removed, this.room, undefined, true, data);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
@@ -756,7 +756,7 @@ export class EventTimelineSet extends EventEmitter {
|
||||
*/
|
||||
public getRelationsForEvent(
|
||||
eventId: string,
|
||||
relationType: RelationType,
|
||||
relationType: RelationType | string,
|
||||
eventType: EventType | string,
|
||||
): Relations | undefined {
|
||||
if (!this.unstableClientRelationAggregation) {
|
||||
@@ -774,6 +774,17 @@ export class EventTimelineSet extends EventEmitter {
|
||||
return relationsWithRelType[eventType];
|
||||
}
|
||||
|
||||
public getAllRelationsEventForEvent(eventId: string): MatrixEvent[] {
|
||||
const relationsForEvent = this.relations[eventId] || {};
|
||||
const events = [];
|
||||
for (const relationsRecord of Object.values(relationsForEvent)) {
|
||||
for (const relations of Object.values(relationsRecord)) {
|
||||
events.push(...relations.getRelations());
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an event as the target event if any Relations exist for it already
|
||||
*
|
||||
@@ -814,7 +825,7 @@ export class EventTimelineSet extends EventEmitter {
|
||||
|
||||
// If the event is currently encrypted, wait until it has been decrypted.
|
||||
if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
|
||||
event.once("Event.decrypted", () => {
|
||||
event.once(MatrixEventEvent.Decrypted, () => {
|
||||
this.aggregateRelations(event);
|
||||
});
|
||||
return;
|
||||
|
||||
+45
-52
@@ -20,49 +20,22 @@ limitations under the License.
|
||||
* @module models/event
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk";
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
|
||||
import {
|
||||
EventType,
|
||||
MsgType,
|
||||
RelationType,
|
||||
EVENT_VISIBILITY_CHANGE_TYPE,
|
||||
} from "../@types/event";
|
||||
import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "../@types/event";
|
||||
import { Crypto, IEventDecryptionResult } from "../crypto";
|
||||
import { deepSortedObjectEntries } from "../utils";
|
||||
import { RoomMember } from "./room-member";
|
||||
import { Thread, ThreadEvent } from "./thread";
|
||||
import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap } from "./thread";
|
||||
import { IActionsObject } from '../pushprocessor';
|
||||
import { ReEmitter } from '../ReEmitter';
|
||||
import { TypedReEmitter } from '../ReEmitter';
|
||||
import { MatrixError } from "../http-api";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { EventStatus } from "./event-status";
|
||||
|
||||
/**
|
||||
* Enum for event statuses.
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum EventStatus {
|
||||
/** The event was not sent and will no longer be retried. */
|
||||
NOT_SENT = "not_sent",
|
||||
|
||||
/** The message is being encrypted */
|
||||
ENCRYPTING = "encrypting",
|
||||
|
||||
/** The event is in the process of being sent. */
|
||||
SENDING = "sending",
|
||||
|
||||
/** The event is in a queue waiting to be sent. */
|
||||
QUEUED = "queued",
|
||||
|
||||
/** The event has been sent to the server, but we have not yet received the echo. */
|
||||
SENT = "sent",
|
||||
|
||||
/** The event was cancelled before it was successfully sent. */
|
||||
CANCELLED = "cancelled",
|
||||
}
|
||||
export { EventStatus } from "./event-status";
|
||||
|
||||
const interns: Record<string, string> = {};
|
||||
function intern(str: string): string {
|
||||
@@ -209,7 +182,29 @@ export interface IMessageVisibilityHidden {
|
||||
// A singleton implementing `IMessageVisibilityVisible`.
|
||||
const MESSAGE_VISIBLE: IMessageVisibilityVisible = Object.freeze({ visible: true });
|
||||
|
||||
export class MatrixEvent extends EventEmitter {
|
||||
export enum MatrixEventEvent {
|
||||
Decrypted = "Event.decrypted",
|
||||
BeforeRedaction = "Event.beforeRedaction",
|
||||
VisibilityChange = "Event.visibilityChange",
|
||||
LocalEventIdReplaced = "Event.localEventIdReplaced",
|
||||
Status = "Event.status",
|
||||
Replaced = "Event.replaced",
|
||||
RelationsCreated = "Event.relationsCreated",
|
||||
}
|
||||
|
||||
type EmittedEvents = MatrixEventEvent | ThreadEvent.Update;
|
||||
|
||||
export type MatrixEventHandlerMap = {
|
||||
[MatrixEventEvent.Decrypted]: (event: MatrixEvent, err?: Error) => void;
|
||||
[MatrixEventEvent.BeforeRedaction]: (event: MatrixEvent, redactionEvent: MatrixEvent) => void;
|
||||
[MatrixEventEvent.VisibilityChange]: (event: MatrixEvent, visible: boolean) => void;
|
||||
[MatrixEventEvent.LocalEventIdReplaced]: (event: MatrixEvent) => void;
|
||||
[MatrixEventEvent.Status]: (event: MatrixEvent, status: EventStatus) => void;
|
||||
[MatrixEventEvent.Replaced]: (event: MatrixEvent) => void;
|
||||
[MatrixEventEvent.RelationsCreated]: (relationType: string, eventType: string) => void;
|
||||
} & ThreadEventHandlerMap;
|
||||
|
||||
export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHandlerMap> {
|
||||
private pushActions: IActionsObject = null;
|
||||
private _replacingEvent: MatrixEvent = null;
|
||||
private _localRedactionEvent: MatrixEvent = null;
|
||||
@@ -292,7 +287,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
*/
|
||||
public verificationRequest: VerificationRequest = null;
|
||||
|
||||
private readonly reEmitter: ReEmitter;
|
||||
private readonly reEmitter: TypedReEmitter<EmittedEvents, MatrixEventHandlerMap>;
|
||||
|
||||
/**
|
||||
* Construct a Matrix Event object
|
||||
@@ -343,7 +338,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
|
||||
this.txnId = event.txn_id || null;
|
||||
this.localTimestamp = Date.now() - (this.getAge() ?? 0);
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
this.reEmitter = new TypedReEmitter(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -512,8 +507,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
if (relatesTo?.rel_type === RelationType.Thread) {
|
||||
return relatesTo.event_id;
|
||||
} else {
|
||||
return this.threadId
|
||||
|| this.getThread()?.id;
|
||||
return this.getThread()?.id || this.threadId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,10 +531,6 @@ export class MatrixEvent extends EventEmitter {
|
||||
return !!threadDetails || (this.getThread()?.id === this.getId());
|
||||
}
|
||||
|
||||
public get parentEventId(): string {
|
||||
return this.replyEventId || this.relationEventId;
|
||||
}
|
||||
|
||||
public get replyEventId(): string {
|
||||
// We're prefer ev.getContent() over ev.getWireContent() to make sure
|
||||
// we grab the latest edit with potentially new relations. But we also
|
||||
@@ -876,7 +866,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
this.setPushActions(null);
|
||||
|
||||
if (options.emit !== false) {
|
||||
this.emit("Event.decrypted", this, err);
|
||||
this.emit(MatrixEventEvent.Decrypted, this, err);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -1035,7 +1025,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
|
||||
public markLocallyRedacted(redactionEvent: MatrixEvent): void {
|
||||
if (this._localRedactionEvent) return;
|
||||
this.emit("Event.beforeRedaction", this, redactionEvent);
|
||||
this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent);
|
||||
this._localRedactionEvent = redactionEvent;
|
||||
if (!this.event.unsigned) {
|
||||
this.event.unsigned = {};
|
||||
@@ -1073,7 +1063,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
});
|
||||
}
|
||||
if (change) {
|
||||
this.emit("Event.visibilityChange", this, visible);
|
||||
this.emit(MatrixEventEvent.VisibilityChange, this, visible);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1105,7 +1095,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
|
||||
this._localRedactionEvent = null;
|
||||
|
||||
this.emit("Event.beforeRedaction", this, redactionEvent);
|
||||
this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent);
|
||||
|
||||
this._replacingEvent = null;
|
||||
// we attempt to replicate what we would see from the server if
|
||||
@@ -1268,7 +1258,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
this.setStatus(null);
|
||||
if (this.getId() !== oldId) {
|
||||
// emit the event if it changed
|
||||
this.emit("Event.localEventIdReplaced", this);
|
||||
this.emit(MatrixEventEvent.LocalEventIdReplaced, this);
|
||||
}
|
||||
|
||||
this.localTimestamp = Date.now() - this.getAge();
|
||||
@@ -1291,12 +1281,12 @@ export class MatrixEvent extends EventEmitter {
|
||||
*/
|
||||
public setStatus(status: EventStatus): void {
|
||||
this.status = status;
|
||||
this.emit("Event.status", this, status);
|
||||
this.emit(MatrixEventEvent.Status, this, status);
|
||||
}
|
||||
|
||||
public replaceLocalEventId(eventId: string): void {
|
||||
this.event.event_id = eventId;
|
||||
this.emit("Event.localEventIdReplaced", this);
|
||||
this.emit(MatrixEventEvent.LocalEventIdReplaced, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1345,7 +1335,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
}
|
||||
if (this._replacingEvent !== newEvent) {
|
||||
this._replacingEvent = newEvent;
|
||||
this.emit("Event.replaced", this);
|
||||
this.emit(MatrixEventEvent.Replaced, this);
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
}
|
||||
@@ -1427,7 +1417,9 @@ export class MatrixEvent extends EventEmitter {
|
||||
*/
|
||||
public getAssociatedId(): string | undefined {
|
||||
const relation = this.getRelation();
|
||||
if (relation) {
|
||||
if (this.replyEventId) {
|
||||
return this.replyEventId;
|
||||
} else if (relation) {
|
||||
return relation.event_id;
|
||||
} else if (this.isRedaction()) {
|
||||
return this.event.redacts;
|
||||
@@ -1561,7 +1553,8 @@ export class MatrixEvent extends EventEmitter {
|
||||
*/
|
||||
public setThread(thread: Thread): void {
|
||||
this.thread = thread;
|
||||
this.reEmitter.reEmit(thread, [ThreadEvent.Ready, ThreadEvent.Update]);
|
||||
this.setThreadId(thread.id);
|
||||
this.reEmitter.reEmit(thread, [ThreadEvent.Update]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,7 @@ limitations under the License.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import * as utils from "../utils";
|
||||
|
||||
@@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Relations } from "./relations";
|
||||
import { Relations, RelationsEvent, EventHandlerMap } from "./relations";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { Listener } from "./typed-event-emitter";
|
||||
|
||||
export class RelatedRelations {
|
||||
private relations: Relations[];
|
||||
@@ -28,11 +29,11 @@ export class RelatedRelations {
|
||||
return this.relations.reduce((c, p) => [...c, ...p.getRelations()], []);
|
||||
}
|
||||
|
||||
public on(ev: string, fn: (...params) => void) {
|
||||
public on<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>) {
|
||||
this.relations.forEach(r => r.on(ev, fn));
|
||||
}
|
||||
|
||||
public off(ev: string, fn: (...params) => void) {
|
||||
public off<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>) {
|
||||
this.relations.forEach(r => r.off(ev, fn));
|
||||
}
|
||||
}
|
||||
|
||||
+28
-17
@@ -14,12 +14,23 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { EventStatus, MatrixEvent, IAggregatedRelation } from './event';
|
||||
import { EventStatus, IAggregatedRelation, MatrixEvent, MatrixEventEvent } from './event';
|
||||
import { Room } from './room';
|
||||
import { logger } from '../logger';
|
||||
import { RelationType } from "../@types/event";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
|
||||
export enum RelationsEvent {
|
||||
Add = "Relations.add",
|
||||
Remove = "Relations.remove",
|
||||
Redaction = "Relations.redaction",
|
||||
}
|
||||
|
||||
export type EventHandlerMap = {
|
||||
[RelationsEvent.Add]: (event: MatrixEvent) => void;
|
||||
[RelationsEvent.Remove]: (event: MatrixEvent) => void;
|
||||
[RelationsEvent.Redaction]: (event: MatrixEvent) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A container for relation events that supports easy access to common ways of
|
||||
@@ -29,7 +40,7 @@ import { RelationType } from "../@types/event";
|
||||
* The typical way to get one of these containers is via
|
||||
* EventTimelineSet#getRelationsForEvent.
|
||||
*/
|
||||
export class Relations extends EventEmitter {
|
||||
export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap> {
|
||||
private relationEventIds = new Set<string>();
|
||||
private relations = new Set<MatrixEvent>();
|
||||
private annotationsByKey: Record<string, Set<MatrixEvent>> = {};
|
||||
@@ -84,7 +95,7 @@ export class Relations extends EventEmitter {
|
||||
// If the event is in the process of being sent, listen for cancellation
|
||||
// so we can remove the event from the collection.
|
||||
if (event.isSending()) {
|
||||
event.on("Event.status", this.onEventStatus);
|
||||
event.on(MatrixEventEvent.Status, this.onEventStatus);
|
||||
}
|
||||
|
||||
this.relations.add(event);
|
||||
@@ -97,9 +108,9 @@ export class Relations extends EventEmitter {
|
||||
this.targetEvent.makeReplaced(lastReplacement);
|
||||
}
|
||||
|
||||
event.on("Event.beforeRedaction", this.onBeforeRedaction);
|
||||
event.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
|
||||
|
||||
this.emit("Relations.add", event);
|
||||
this.emit(RelationsEvent.Add, event);
|
||||
|
||||
this.maybeEmitCreated();
|
||||
}
|
||||
@@ -138,7 +149,7 @@ export class Relations extends EventEmitter {
|
||||
this.targetEvent.makeReplaced(lastReplacement);
|
||||
}
|
||||
|
||||
this.emit("Relations.remove", event);
|
||||
this.emit(RelationsEvent.Remove, event);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,14 +161,14 @@ export class Relations extends EventEmitter {
|
||||
private onEventStatus = (event: MatrixEvent, status: EventStatus) => {
|
||||
if (!event.isSending()) {
|
||||
// Sending is done, so we don't need to listen anymore
|
||||
event.removeListener("Event.status", this.onEventStatus);
|
||||
event.removeListener(MatrixEventEvent.Status, this.onEventStatus);
|
||||
return;
|
||||
}
|
||||
if (status !== EventStatus.CANCELLED) {
|
||||
return;
|
||||
}
|
||||
// Event was cancelled, remove from the collection
|
||||
event.removeListener("Event.status", this.onEventStatus);
|
||||
event.removeListener(MatrixEventEvent.Status, this.onEventStatus);
|
||||
this.removeEvent(event);
|
||||
};
|
||||
|
||||
@@ -171,11 +182,11 @@ export class Relations extends EventEmitter {
|
||||
* @return {Array}
|
||||
* Relation events in insertion order.
|
||||
*/
|
||||
public getRelations() {
|
||||
public getRelations(): MatrixEvent[] {
|
||||
return [...this.relations];
|
||||
}
|
||||
|
||||
private addAnnotationToAggregation(event: MatrixEvent) {
|
||||
private addAnnotationToAggregation(event: MatrixEvent): void {
|
||||
const { key } = event.getRelation();
|
||||
if (!key) {
|
||||
return;
|
||||
@@ -204,7 +215,7 @@ export class Relations extends EventEmitter {
|
||||
eventsFromSender.add(event);
|
||||
}
|
||||
|
||||
private removeAnnotationFromAggregation(event: MatrixEvent) {
|
||||
private removeAnnotationFromAggregation(event: MatrixEvent): void {
|
||||
const { key } = event.getRelation();
|
||||
if (!key) {
|
||||
return;
|
||||
@@ -240,7 +251,7 @@ export class Relations extends EventEmitter {
|
||||
* @param {MatrixEvent} redactedEvent
|
||||
* The original relation event that is about to be redacted.
|
||||
*/
|
||||
private onBeforeRedaction = async (redactedEvent: MatrixEvent) => {
|
||||
private onBeforeRedaction = async (redactedEvent: MatrixEvent): Promise<void> => {
|
||||
if (!this.relations.has(redactedEvent)) {
|
||||
return;
|
||||
}
|
||||
@@ -255,9 +266,9 @@ export class Relations extends EventEmitter {
|
||||
this.targetEvent.makeReplaced(lastReplacement);
|
||||
}
|
||||
|
||||
redactedEvent.removeListener("Event.beforeRedaction", this.onBeforeRedaction);
|
||||
redactedEvent.removeListener(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
|
||||
|
||||
this.emit("Relations.redaction", redactedEvent);
|
||||
this.emit(RelationsEvent.Redaction, redactedEvent);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -375,6 +386,6 @@ export class Relations extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
this.creationEmitted = true;
|
||||
this.targetEvent.emit("Event.relationsCreated", this.relationType, this.eventType);
|
||||
this.targetEvent.emit(MatrixEventEvent.RelationsCreated, this.relationType, this.eventType);
|
||||
}
|
||||
}
|
||||
|
||||
+34
-10
@@ -18,15 +18,30 @@ limitations under the License.
|
||||
* @module models/room-member
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { getHttpUriForMxc } from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
import { User } from "./user";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { RoomState } from "./room-state";
|
||||
import { logger } from "../logger";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { EventType } from "../@types/event";
|
||||
|
||||
export class RoomMember extends EventEmitter {
|
||||
export enum RoomMemberEvent {
|
||||
Membership = "RoomMember.membership",
|
||||
Name = "RoomMember.name",
|
||||
PowerLevel = "RoomMember.powerLevel",
|
||||
Typing = "RoomMember.typing",
|
||||
}
|
||||
|
||||
export type RoomMemberEventHandlerMap = {
|
||||
[RoomMemberEvent.Membership]: (event: MatrixEvent, member: RoomMember, oldMembership: string | null) => void;
|
||||
[RoomMemberEvent.Name]: (event: MatrixEvent, member: RoomMember, oldName: string | null) => void;
|
||||
[RoomMemberEvent.PowerLevel]: (event: MatrixEvent, member: RoomMember) => void;
|
||||
[RoomMemberEvent.Typing]: (event: MatrixEvent, member: RoomMember) => void;
|
||||
};
|
||||
|
||||
export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEventHandlerMap> {
|
||||
private _isOutOfBand = false;
|
||||
private _modified: number;
|
||||
public _requestedProfileInfo: boolean; // used by sync.ts
|
||||
@@ -43,8 +58,8 @@ export class RoomMember extends EventEmitter {
|
||||
public events: {
|
||||
member?: MatrixEvent;
|
||||
} = {
|
||||
member: null,
|
||||
};
|
||||
member: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a new room member.
|
||||
@@ -106,7 +121,7 @@ export class RoomMember extends EventEmitter {
|
||||
public setMembershipEvent(event: MatrixEvent, roomState?: RoomState): void {
|
||||
const displayName = event.getDirectionalContent().displayname;
|
||||
|
||||
if (event.getType() !== "m.room.member") {
|
||||
if (event.getType() !== EventType.RoomMember) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,6 +131,15 @@ export class RoomMember extends EventEmitter {
|
||||
|
||||
const oldMembership = this.membership;
|
||||
this.membership = event.getDirectionalContent().membership;
|
||||
if (this.membership === undefined) {
|
||||
// logging to diagnose https://github.com/vector-im/element-web/issues/20962
|
||||
// (logs event content, although only of membership events)
|
||||
logger.trace(
|
||||
`membership event with membership undefined (forwardLooking: ${event.forwardLooking})!`,
|
||||
event.getContent(),
|
||||
`prevcontent is `, event.getPrevContent(),
|
||||
);
|
||||
}
|
||||
|
||||
this.disambiguate = shouldDisambiguate(
|
||||
this.userId,
|
||||
@@ -140,11 +164,11 @@ export class RoomMember extends EventEmitter {
|
||||
|
||||
if (oldMembership !== this.membership) {
|
||||
this.updateModifiedTime();
|
||||
this.emit("RoomMember.membership", event, this, oldMembership);
|
||||
this.emit(RoomMemberEvent.Membership, event, this, oldMembership);
|
||||
}
|
||||
if (oldName !== this.name) {
|
||||
this.updateModifiedTime();
|
||||
this.emit("RoomMember.name", event, this, oldName);
|
||||
this.emit(RoomMemberEvent.Name, event, this, oldName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +210,7 @@ export class RoomMember extends EventEmitter {
|
||||
// redraw everyone's level if the max has changed)
|
||||
if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) {
|
||||
this.updateModifiedTime();
|
||||
this.emit("RoomMember.powerLevel", powerLevelEvent, this);
|
||||
this.emit(RoomMemberEvent.PowerLevel, powerLevelEvent, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +236,7 @@ export class RoomMember extends EventEmitter {
|
||||
}
|
||||
if (oldTyping !== this.typing) {
|
||||
this.updateModifiedTime();
|
||||
this.emit("RoomMember.typing", event, this);
|
||||
this.emit(RoomMemberEvent.Typing, event, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+25
-10
@@ -18,8 +18,6 @@ limitations under the License.
|
||||
* @module models/room-state
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { RoomMember } from "./room-member";
|
||||
import { logger } from '../logger';
|
||||
import * as utils from "../utils";
|
||||
@@ -27,6 +25,7 @@ import { EventType } from "../@types/event";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { MatrixClient } from "../client";
|
||||
import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
|
||||
// possible statuses for out-of-band member loading
|
||||
enum OobStatus {
|
||||
@@ -35,7 +34,21 @@ enum OobStatus {
|
||||
Finished,
|
||||
}
|
||||
|
||||
export class RoomState extends EventEmitter {
|
||||
export enum RoomStateEvent {
|
||||
Events = "RoomState.events",
|
||||
Members = "RoomState.members",
|
||||
NewMember = "RoomState.newMember",
|
||||
Update = "RoomState.update", // signals batches of updates without specificity
|
||||
}
|
||||
|
||||
export type RoomStateEventHandlerMap = {
|
||||
[RoomStateEvent.Events]: (event: MatrixEvent, state: RoomState, lastStateEvent: MatrixEvent | null) => void;
|
||||
[RoomStateEvent.Members]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void;
|
||||
[RoomStateEvent.NewMember]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void;
|
||||
[RoomStateEvent.Update]: (state: RoomState) => void;
|
||||
};
|
||||
|
||||
export class RoomState extends TypedEventEmitter<RoomStateEvent, RoomStateEventHandlerMap> {
|
||||
private sentinels: Record<string, RoomMember> = {}; // userId: RoomMember
|
||||
// stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
|
||||
private displayNameToUserIds: Record<string, string[]> = {};
|
||||
@@ -307,7 +320,7 @@ export class RoomState extends EventEmitter {
|
||||
this.updateDisplayNameCache(event.getStateKey(), event.getContent().displayname);
|
||||
this.updateThirdPartyTokenCache(event);
|
||||
}
|
||||
this.emit("RoomState.events", event, this, lastStateEvent);
|
||||
this.emit(RoomStateEvent.Events, event, this, lastStateEvent);
|
||||
});
|
||||
|
||||
// update higher level data structures. This needs to be done AFTER the
|
||||
@@ -342,7 +355,7 @@ export class RoomState extends EventEmitter {
|
||||
member.setMembershipEvent(event, this);
|
||||
|
||||
this.updateMember(member);
|
||||
this.emit("RoomState.members", event, this, member);
|
||||
this.emit(RoomStateEvent.Members, event, this, member);
|
||||
} else if (event.getType() === EventType.RoomPowerLevels) {
|
||||
// events with unknown state keys should be ignored
|
||||
// and should not aggregate onto members power levels
|
||||
@@ -357,7 +370,7 @@ export class RoomState extends EventEmitter {
|
||||
const oldLastModified = member.getLastModifiedTime();
|
||||
member.setPowerLevelEvent(event);
|
||||
if (oldLastModified !== member.getLastModifiedTime()) {
|
||||
this.emit("RoomState.members", event, this, member);
|
||||
this.emit(RoomStateEvent.Members, event, this, member);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -365,6 +378,8 @@ export class RoomState extends EventEmitter {
|
||||
this.sentinels = {};
|
||||
}
|
||||
});
|
||||
|
||||
this.emit(RoomStateEvent.Update, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,7 +399,7 @@ export class RoomState extends EventEmitter {
|
||||
// add member to members before emitting any events,
|
||||
// as event handlers often lookup the member
|
||||
this.members[userId] = member;
|
||||
this.emit("RoomState.newMember", event, this, member);
|
||||
this.emit(RoomStateEvent.NewMember, event, this, member);
|
||||
}
|
||||
return member;
|
||||
}
|
||||
@@ -397,8 +412,7 @@ export class RoomState extends EventEmitter {
|
||||
}
|
||||
|
||||
private getStateEventMatching(event: MatrixEvent): MatrixEvent | null {
|
||||
if (!this.events.has(event.getType())) return null;
|
||||
return this.events.get(event.getType()).get(event.getStateKey());
|
||||
return this.events.get(event.getType())?.get(event.getStateKey()) ?? null;
|
||||
}
|
||||
|
||||
private updateMember(member: RoomMember): void {
|
||||
@@ -475,6 +489,7 @@ export class RoomState extends EventEmitter {
|
||||
logger.log(`LL: RoomState put in finished state ...`);
|
||||
this.oobMemberFlags.status = OobStatus.Finished;
|
||||
stateEvents.forEach((e) => this.setOutOfBandMember(e));
|
||||
this.emit(RoomStateEvent.Update, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -503,7 +518,7 @@ export class RoomState extends EventEmitter {
|
||||
|
||||
this.setStateEvent(stateEvent);
|
||||
this.updateMember(member);
|
||||
this.emit("RoomState.members", stateEvent, this, member);
|
||||
this.emit(RoomStateEvent.Members, stateEvent, this, member);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+101
-63
@@ -18,18 +18,17 @@ limitations under the License.
|
||||
* @module models/room
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { EventTimelineSet, DuplicateStrategy } from "./event-timeline-set";
|
||||
import { Direction, EventTimeline } from "./event-timeline";
|
||||
import { getHttpUriForMxc } from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
import { normalize } from "../utils";
|
||||
import { EventStatus, IEvent, MatrixEvent } from "./event";
|
||||
import { IEvent, MatrixEvent } from "./event";
|
||||
import { EventStatus } from "./event-status";
|
||||
import { RoomMember } from "./room-member";
|
||||
import { IRoomSummary, RoomSummary } from "./room-summary";
|
||||
import { logger } from '../logger';
|
||||
import { ReEmitter } from '../ReEmitter';
|
||||
import { TypedReEmitter } from '../ReEmitter';
|
||||
import {
|
||||
EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
|
||||
EVENT_VISIBILITY_CHANGE_TYPE,
|
||||
@@ -38,8 +37,9 @@ import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersio
|
||||
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
|
||||
import { Filter } from "../filter";
|
||||
import { RoomState } from "./room-state";
|
||||
import { Thread, ThreadEvent } from "./thread";
|
||||
import { Thread, ThreadEvent, EventHandlerMap as ThreadHandlerMap } from "./thread";
|
||||
import { Method } from "../http-api";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
|
||||
// These constants are used as sane defaults when the homeserver doesn't support
|
||||
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||
@@ -143,15 +143,51 @@ export interface ICreateFilterOpts {
|
||||
prepopulateTimeline?: boolean;
|
||||
}
|
||||
|
||||
export class Room extends EventEmitter {
|
||||
private readonly reEmitter: ReEmitter;
|
||||
export enum RoomEvent {
|
||||
MyMembership = "Room.myMembership",
|
||||
Tags = "Room.tags",
|
||||
AccountData = "Room.accountData",
|
||||
Receipt = "Room.receipt",
|
||||
Name = "Room.name",
|
||||
Redaction = "Room.redaction",
|
||||
RedactionCancelled = "Room.redactionCancelled",
|
||||
LocalEchoUpdated = "Room.localEchoUpdated",
|
||||
Timeline = "Room.timeline",
|
||||
TimelineReset = "Room.timelineReset",
|
||||
}
|
||||
|
||||
type EmittedEvents = RoomEvent
|
||||
| ThreadEvent.New
|
||||
| ThreadEvent.Update
|
||||
| RoomEvent.Timeline
|
||||
| RoomEvent.TimelineReset;
|
||||
|
||||
export type RoomEventHandlerMap = {
|
||||
[RoomEvent.MyMembership]: (room: Room, membership: string, prevMembership?: string) => void;
|
||||
[RoomEvent.Tags]: (event: MatrixEvent, room: Room) => void;
|
||||
[RoomEvent.AccountData]: (event: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => void;
|
||||
[RoomEvent.Receipt]: (event: MatrixEvent, room: Room) => void;
|
||||
[RoomEvent.Name]: (room: Room) => void;
|
||||
[RoomEvent.Redaction]: (event: MatrixEvent, room: Room) => void;
|
||||
[RoomEvent.RedactionCancelled]: (event: MatrixEvent, room: Room) => void;
|
||||
[RoomEvent.LocalEchoUpdated]: (
|
||||
event: MatrixEvent,
|
||||
room: Room,
|
||||
oldEventId?: string,
|
||||
oldStatus?: EventStatus,
|
||||
) => void;
|
||||
[ThreadEvent.New]: (thread: Thread) => void;
|
||||
} & ThreadHandlerMap;
|
||||
|
||||
export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap> {
|
||||
private readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
|
||||
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
|
||||
// receipts should clobber based on receipt_type and user_id pairs hence
|
||||
// the form of this structure. This is sub-optimal for the exposed APIs
|
||||
// which pass in an event ID and get back some receipts, so we also store
|
||||
// a pre-cached list for this purpose.
|
||||
private receipts: Receipts = {}; // { receipt_type: { user_id: IReceipt } }
|
||||
private receiptCacheByEventId: ReceiptCache = {}; // { event_id: IReceipt2[] }
|
||||
private receiptCacheByEventId: ReceiptCache = {}; // { event_id: ICachedReceipt[] }
|
||||
private notificationCounts: Partial<Record<NotificationCountType, number>> = {};
|
||||
private readonly timelineSets: EventTimelineSet[];
|
||||
// any filtered timeline sets we're maintaining for this room
|
||||
@@ -287,7 +323,7 @@ export class Room extends EventEmitter {
|
||||
// In some cases, we add listeners for every displayed Matrix event, so it's
|
||||
// common to have quite a few more than the default limit.
|
||||
this.setMaxListeners(100);
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
this.reEmitter = new TypedReEmitter(this);
|
||||
|
||||
opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological;
|
||||
|
||||
@@ -297,7 +333,8 @@ export class Room extends EventEmitter {
|
||||
// the subsequent ones are the filtered ones in no particular order.
|
||||
this.timelineSets = [new EventTimelineSet(this, opts)];
|
||||
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), [
|
||||
"Room.timeline", "Room.timelineReset",
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
|
||||
this.fixUpLegacyTimelineFields();
|
||||
@@ -513,16 +550,14 @@ export class Room extends EventEmitter {
|
||||
*
|
||||
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
|
||||
*/
|
||||
public getPendingEvents(thread?: Thread): MatrixEvent[] {
|
||||
public getPendingEvents(): MatrixEvent[] {
|
||||
if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) {
|
||||
throw new Error(
|
||||
"Cannot call getPendingEvents with pendingEventOrdering == " +
|
||||
this.opts.pendingEventOrdering);
|
||||
}
|
||||
|
||||
return this.pendingEventList.filter(event => {
|
||||
return !thread || thread.id === event.threadRootId;
|
||||
});
|
||||
return this.pendingEventList;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -714,7 +749,7 @@ export class Room extends EventEmitter {
|
||||
if (membership === "leave") {
|
||||
this.cleanupAfterLeaving();
|
||||
}
|
||||
this.emit("Room.myMembership", this, membership, prevMembership);
|
||||
this.emit(RoomEvent.MyMembership, this, membership, prevMembership);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1287,7 +1322,10 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
const opts = Object.assign({ filter: filter }, this.opts);
|
||||
const timelineSet = new EventTimelineSet(this, opts);
|
||||
this.reEmitter.reEmit(timelineSet, ["Room.timeline", "Room.timelineReset"]);
|
||||
this.reEmitter.reEmit(timelineSet, [
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
this.filteredTimelineSets[filter.filterId] = timelineSet;
|
||||
this.timelineSets.push(timelineSet);
|
||||
|
||||
@@ -1359,7 +1397,7 @@ export class Room extends EventEmitter {
|
||||
} else if (event.isThreadRoot) {
|
||||
return this.threads.get(event.getId());
|
||||
} else {
|
||||
const parentEvent = this.findEventById(event.parentEventId);
|
||||
const parentEvent = this.findEventById(event.getAssociatedId());
|
||||
return this.findThreadForEvent(parentEvent);
|
||||
}
|
||||
}
|
||||
@@ -1368,10 +1406,10 @@ export class Room extends EventEmitter {
|
||||
* Add an event to a thread's timeline. Will fire "Thread.update"
|
||||
* @experimental
|
||||
*/
|
||||
public async addThreadedEvent(event: MatrixEvent): Promise<void> {
|
||||
public async addThreadedEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
|
||||
let thread = this.findThreadForEvent(event);
|
||||
if (thread) {
|
||||
thread.addEvent(event);
|
||||
thread.addEvent(event, toStartOfTimeline);
|
||||
} else {
|
||||
const events = [event];
|
||||
let rootEvent = this.findEventById(event.threadRootId);
|
||||
@@ -1397,19 +1435,18 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
if (event.getUnsigned().transaction_id) {
|
||||
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
|
||||
if (existingEvent) {
|
||||
// remote echo of an event we sent earlier
|
||||
this.handleRemoteEcho(event, existingEvent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.Update, thread);
|
||||
}
|
||||
|
||||
public createThread(rootEvent: MatrixEvent, events?: MatrixEvent[]): Thread | undefined {
|
||||
public createThread(rootEvent: MatrixEvent | undefined, events: MatrixEvent[] = []): Thread | undefined {
|
||||
if (rootEvent) {
|
||||
const tl = this.getTimelineForEvent(rootEvent.getId());
|
||||
const relatedEvents = tl?.getTimelineSet().getAllRelationsEventForEvent(rootEvent.getId());
|
||||
if (relatedEvents) {
|
||||
events = events.concat(relatedEvents);
|
||||
}
|
||||
}
|
||||
|
||||
const thread = new Thread(rootEvent, {
|
||||
initialEvents: events,
|
||||
room: this,
|
||||
@@ -1421,12 +1458,11 @@ export class Room extends EventEmitter {
|
||||
this.threads.set(thread.id, thread);
|
||||
this.reEmitter.reEmit(thread, [
|
||||
ThreadEvent.Update,
|
||||
ThreadEvent.Ready,
|
||||
"Room.timeline",
|
||||
"Room.timelineReset",
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
|
||||
if (!this.lastThread || this.lastThread.rootEvent.localTimestamp < rootEvent.localTimestamp) {
|
||||
if (!this.lastThread || this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp) {
|
||||
this.lastThread = thread;
|
||||
}
|
||||
|
||||
@@ -1465,7 +1501,7 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
this.emit("Room.redaction", event, this);
|
||||
this.emit(RoomEvent.Redaction, event, this);
|
||||
|
||||
// TODO: we stash user displaynames (among other things) in
|
||||
// RoomMember objects which are then attached to other events
|
||||
@@ -1565,8 +1601,7 @@ export class Room extends EventEmitter {
|
||||
EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS), false);
|
||||
|
||||
this.txnToEvent[txnId] = event;
|
||||
const thread = this.findThreadForEvent(event);
|
||||
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached && !thread) {
|
||||
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached) {
|
||||
if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
|
||||
logger.warn("Setting event as NOT_SENT due to messages in the same state");
|
||||
event.setStatus(EventStatus.NOT_SENT);
|
||||
@@ -1582,36 +1617,31 @@ export class Room extends EventEmitter {
|
||||
|
||||
if (event.isRedaction()) {
|
||||
const redactId = event.event.redacts;
|
||||
let redactedEvent = this.pendingEventList &&
|
||||
this.pendingEventList.find(e => e.getId() === redactId);
|
||||
let redactedEvent = this.pendingEventList?.find(e => e.getId() === redactId);
|
||||
if (!redactedEvent) {
|
||||
redactedEvent = this.findEventById(redactId);
|
||||
}
|
||||
if (redactedEvent) {
|
||||
redactedEvent.markLocallyRedacted(event);
|
||||
this.emit("Room.redaction", event, this);
|
||||
this.emit(RoomEvent.Redaction, event, this);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (thread) {
|
||||
thread.addEvent(event, false);
|
||||
} else {
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
const timelineSet = this.timelineSets[i];
|
||||
if (timelineSet.getFilter()) {
|
||||
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
|
||||
timelineSet.addEventToTimeline(event,
|
||||
timelineSet.getLiveTimeline(), false);
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
const timelineSet = this.timelineSets[i];
|
||||
if (timelineSet.getFilter()) {
|
||||
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
|
||||
timelineSet.addEventToTimeline(event,
|
||||
timelineSet.getLiveTimeline(), false);
|
||||
}
|
||||
} else {
|
||||
timelineSet.addEventToTimeline(event,
|
||||
timelineSet.getLiveTimeline(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.emit("Room.localEchoUpdated", event, this, null, null);
|
||||
this.emit(RoomEvent.LocalEchoUpdated, event, this, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1667,7 +1697,9 @@ export class Room extends EventEmitter {
|
||||
const thread = this.findThreadForEvent(event);
|
||||
if (thread) {
|
||||
thread.timelineSet.aggregateRelations(event);
|
||||
} else {
|
||||
}
|
||||
|
||||
if (thread?.id === event.getAssociatedId() || !thread) {
|
||||
// TODO: We should consider whether this means it would be a better
|
||||
// design to lift the relations handling up to the room instead.
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
@@ -1683,6 +1715,10 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
public getEventForTxnId(txnId: string): MatrixEvent {
|
||||
return this.txnToEvent[txnId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deal with the echo of a message we sent.
|
||||
*
|
||||
@@ -1697,7 +1733,7 @@ export class Room extends EventEmitter {
|
||||
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
|
||||
* @private
|
||||
*/
|
||||
private handleRemoteEcho(remoteEvent: MatrixEvent, localEvent: MatrixEvent): void {
|
||||
public handleRemoteEcho(remoteEvent: MatrixEvent, localEvent: MatrixEvent): void {
|
||||
const oldEventId = localEvent.getId();
|
||||
const newEventId = remoteEvent.getId();
|
||||
const oldStatus = localEvent.status;
|
||||
@@ -1722,7 +1758,9 @@ export class Room extends EventEmitter {
|
||||
const thread = this.findThreadForEvent(remoteEvent);
|
||||
if (thread) {
|
||||
thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
|
||||
} else {
|
||||
}
|
||||
|
||||
if (thread?.id === remoteEvent.getAssociatedId() || !thread) {
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
const timelineSet = this.timelineSets[i];
|
||||
|
||||
@@ -1731,8 +1769,7 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
this.emit("Room.localEchoUpdated", localEvent, this,
|
||||
oldEventId, oldStatus);
|
||||
this.emit(RoomEvent.LocalEchoUpdated, localEvent, this, oldEventId, oldStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1792,7 +1829,8 @@ export class Room extends EventEmitter {
|
||||
const thread = this.findThreadForEvent(event);
|
||||
if (thread) {
|
||||
thread.timelineSet.replaceEventId(oldEventId, newEventId);
|
||||
} else {
|
||||
}
|
||||
if (thread?.id === event.getAssociatedId() || !thread) {
|
||||
// if the event was already in the timeline (which will be the case if
|
||||
// opts.pendingEventOrdering==chronological), we need to update the
|
||||
// timeline map.
|
||||
@@ -1815,7 +1853,7 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
this.savePendingEvents();
|
||||
|
||||
this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus);
|
||||
this.emit(RoomEvent.LocalEchoUpdated, event, this, oldEventId, oldStatus);
|
||||
}
|
||||
|
||||
private revertRedactionLocalEcho(redactionEvent: MatrixEvent): void {
|
||||
@@ -1828,7 +1866,7 @@ export class Room extends EventEmitter {
|
||||
if (redactedEvent) {
|
||||
redactedEvent.unmarkLocallyRedacted();
|
||||
// re-render after undoing redaction
|
||||
this.emit("Room.redactionCancelled", redactionEvent, this);
|
||||
this.emit(RoomEvent.RedactionCancelled, redactionEvent, this);
|
||||
// reapply relation now redaction failed
|
||||
if (redactedEvent.isRelation()) {
|
||||
this.aggregateNonLiveRelation(redactedEvent);
|
||||
@@ -1968,7 +2006,7 @@ export class Room extends EventEmitter {
|
||||
});
|
||||
|
||||
if (oldName !== this.name) {
|
||||
this.emit("Room.name", this);
|
||||
this.emit(RoomEvent.Name, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2061,7 +2099,7 @@ export class Room extends EventEmitter {
|
||||
this.addReceiptsToStructure(event, synthetic);
|
||||
// send events after we've regenerated the structure & cache, otherwise things that
|
||||
// listened for the event would read stale data.
|
||||
this.emit("Room.receipt", event, this);
|
||||
this.emit(RoomEvent.Receipt, event, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2195,7 +2233,7 @@ export class Room extends EventEmitter {
|
||||
|
||||
// XXX: we could do a deep-comparison to see if the tags have really
|
||||
// changed - but do we want to bother?
|
||||
this.emit("Room.tags", event, this);
|
||||
this.emit(RoomEvent.Tags, event, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2210,7 +2248,7 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
const lastEvent = this.accountData[event.getType()];
|
||||
this.accountData[event.getType()] = event;
|
||||
this.emit("Room.accountData", event, this, lastEvent);
|
||||
this.emit(RoomEvent.AccountData, event, this, lastEvent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+109
-63
@@ -14,25 +14,34 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "../matrix";
|
||||
import { ReEmitter } from "../ReEmitter";
|
||||
import { MatrixClient, RoomEvent } from "../matrix";
|
||||
import { TypedReEmitter } from "../ReEmitter";
|
||||
import { RelationType } from "../@types/event";
|
||||
import { IRelationsRequestOpts } from "../@types/requests";
|
||||
import { MatrixEvent, IThreadBundledRelationship } from "./event";
|
||||
import { IThreadBundledRelationship, MatrixEvent } from "./event";
|
||||
import { Direction, EventTimeline } from "./event-timeline";
|
||||
import { EventTimelineSet } from './event-timeline-set';
|
||||
import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-set';
|
||||
import { Room } from './room';
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { RoomState } from "./room-state";
|
||||
|
||||
export enum ThreadEvent {
|
||||
New = "Thread.new",
|
||||
Ready = "Thread.ready",
|
||||
Update = "Thread.update",
|
||||
NewReply = "Thread.newReply",
|
||||
ViewThread = "Thred.viewThread",
|
||||
ViewThread = "Thread.viewThread",
|
||||
}
|
||||
|
||||
type EmittedEvents = Exclude<ThreadEvent, ThreadEvent.New>
|
||||
| RoomEvent.Timeline
|
||||
| RoomEvent.TimelineReset;
|
||||
|
||||
export type EventHandlerMap = {
|
||||
[ThreadEvent.Update]: (thread: Thread) => void;
|
||||
[ThreadEvent.NewReply]: (thread: Thread, event: MatrixEvent) => void;
|
||||
[ThreadEvent.ViewThread]: () => void;
|
||||
} & EventTimelineSetHandlerMap;
|
||||
|
||||
interface IThreadOpts {
|
||||
initialEvents?: MatrixEvent[];
|
||||
room: Room;
|
||||
@@ -42,15 +51,18 @@ interface IThreadOpts {
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
public static hasServerSideSupport: boolean;
|
||||
private static serverSupportPromise: Promise<boolean> | null;
|
||||
|
||||
/**
|
||||
* A reference to all the events ID at the bottom of the threads
|
||||
*/
|
||||
public readonly timelineSet;
|
||||
public readonly timelineSet: EventTimelineSet;
|
||||
|
||||
private _currentUserParticipated = false;
|
||||
|
||||
private reEmitter: ReEmitter;
|
||||
private reEmitter: TypedReEmitter<EmittedEvents, EventHandlerMap>;
|
||||
|
||||
private lastEvent: MatrixEvent;
|
||||
private replyCount = 0;
|
||||
@@ -75,13 +87,22 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
timelineSupport: true,
|
||||
pendingEvents: true,
|
||||
});
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
this.reEmitter = new TypedReEmitter(this);
|
||||
|
||||
this.reEmitter.reEmit(this.timelineSet, [
|
||||
"Room.timeline",
|
||||
"Room.timelineReset",
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
|
||||
if (Thread.hasServerSideSupport === undefined) {
|
||||
Thread.serverSupportPromise = this.client.doesServerSupportUnstableFeature("org.matrix.msc3440");
|
||||
Thread.serverSupportPromise.then((serverSupportsThread) => {
|
||||
Thread.hasServerSideSupport = serverSupportsThread;
|
||||
}).catch(() => {
|
||||
Thread.serverSupportPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
// If we weren't able to find the root event, it's probably missing
|
||||
// and we define the thread ID from one of the thread relation
|
||||
if (!rootEvent) {
|
||||
@@ -92,18 +113,13 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
}
|
||||
this.initialiseThread(this.rootEvent);
|
||||
|
||||
opts?.initialEvents?.forEach(event => this.addEvent(event));
|
||||
opts?.initialEvents?.forEach(event => this.addEvent(event, false));
|
||||
|
||||
this.room.on("Room.localEchoUpdated", this.onEcho);
|
||||
this.room.on("Room.timeline", this.onEcho);
|
||||
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
|
||||
this.room.on(RoomEvent.Timeline, this.onEcho);
|
||||
}
|
||||
|
||||
public get hasServerSideSupport(): boolean {
|
||||
return this.client.cachedCapabilities
|
||||
?.capabilities?.[RelationType.Thread]?.enabled;
|
||||
}
|
||||
|
||||
onEcho = (event: MatrixEvent) => {
|
||||
private onEcho = (event: MatrixEvent) => {
|
||||
if (this.timelineSet.eventIdToTimeline(event.getId())) {
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
@@ -113,26 +129,17 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to the thread and updates
|
||||
* the tail/root references if needed
|
||||
* Will fire "Thread.update"
|
||||
* @param event The event to add
|
||||
*/
|
||||
public async addEvent(event: MatrixEvent, toStartOfTimeline = false): Promise<void> {
|
||||
// Add all incoming events to the thread's timeline set when there's
|
||||
// no server support
|
||||
if (!this.hasServerSideSupport) {
|
||||
if (this.timelineSet.findEventById(event.getId())) {
|
||||
private addEventToTimeline(event: MatrixEvent, toStartOfTimeline: boolean): void {
|
||||
if (event.getUnsigned().transaction_id) {
|
||||
const existingEvent = this.room.getEventForTxnId(event.getUnsigned().transaction_id);
|
||||
if (existingEvent) {
|
||||
// remote echo of an event we sent earlier
|
||||
this.room.handleRemoteEcho(event, existingEvent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// all the relevant membership info to hydrate events with a sender
|
||||
// is held in the main room timeline
|
||||
// We want to fetch the room state from there and pass it down to this thread
|
||||
// timeline set to let it reconcile an event with its relevant RoomMember
|
||||
|
||||
event.setThread(this);
|
||||
if (!this.findEventById(event.getId())) {
|
||||
this.timelineSet.addEventToTimeline(
|
||||
event,
|
||||
this.liveTimeline,
|
||||
@@ -140,19 +147,38 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
false,
|
||||
this.roomState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to the thread and updates
|
||||
* the tail/root references if needed
|
||||
* Will fire "Thread.update"
|
||||
* @param event The event to add
|
||||
* @param {boolean} toStartOfTimeline whether the event is being added
|
||||
* to the start (and not the end) of the timeline.
|
||||
*/
|
||||
public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
|
||||
if (Thread.hasServerSideSupport === undefined) {
|
||||
await Thread.serverSupportPromise;
|
||||
}
|
||||
|
||||
// Add all incoming events to the thread's timeline set when there's no server support
|
||||
if (!Thread.hasServerSideSupport) {
|
||||
// all the relevant membership info to hydrate events with a sender
|
||||
// is held in the main room timeline
|
||||
// We want to fetch the room state from there and pass it down to this thread
|
||||
// timeline set to let it reconcile an event with its relevant RoomMember
|
||||
|
||||
event.setThread(this);
|
||||
this.addEventToTimeline(event, toStartOfTimeline);
|
||||
|
||||
await this.client.decryptEventIfNeeded(event, {});
|
||||
}
|
||||
|
||||
if (this.hasServerSideSupport && this.initialEventsFetched) {
|
||||
if (event.localTimestamp > this.lastReply().localTimestamp && !this.findEventById(event.getId())) {
|
||||
this.timelineSet.addEventToTimeline(
|
||||
event,
|
||||
this.liveTimeline,
|
||||
false,
|
||||
false,
|
||||
this.roomState,
|
||||
);
|
||||
if (Thread.hasServerSideSupport && this.initialEventsFetched) {
|
||||
if (event.localTimestamp > this.lastReply().localTimestamp) {
|
||||
this.addEventToTimeline(event, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +189,7 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
const isThreadReply = event.getRelation()?.rel_type === RelationType.Thread;
|
||||
// If no thread support exists we want to count all thread relation
|
||||
// added as a reply. We can't rely on the bundled relationships count
|
||||
if (!this.hasServerSideSupport && isThreadReply) {
|
||||
if (!Thread.hasServerSideSupport && isThreadReply) {
|
||||
this.replyCount++;
|
||||
}
|
||||
|
||||
@@ -176,7 +202,7 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
// This counting only works when server side support is enabled
|
||||
// as we started the counting from the value returned in the
|
||||
// bundled relationship
|
||||
if (this.hasServerSideSupport) {
|
||||
if (Thread.hasServerSideSupport) {
|
||||
this.replyCount++;
|
||||
}
|
||||
|
||||
@@ -188,10 +214,17 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
}
|
||||
|
||||
private initialiseThread(rootEvent: MatrixEvent | undefined): void {
|
||||
if (Thread.hasServerSideSupport === undefined) {
|
||||
Thread.serverSupportPromise.then(() => {
|
||||
this.initialiseThread(rootEvent);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const bundledRelationship = rootEvent
|
||||
?.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
|
||||
|
||||
if (this.hasServerSideSupport && bundledRelationship) {
|
||||
if (Thread.hasServerSideSupport && bundledRelationship) {
|
||||
this.replyCount = bundledRelationship.count;
|
||||
this._currentUserParticipated = bundledRelationship.current_user_participated;
|
||||
|
||||
@@ -199,19 +232,28 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
this.setEventMetadata(event);
|
||||
this.lastEvent = event;
|
||||
}
|
||||
|
||||
if (!bundledRelationship && rootEvent) {
|
||||
this.addEvent(rootEvent);
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchInitialEvents(): Promise<boolean> {
|
||||
try {
|
||||
await this.fetchEvents();
|
||||
public async fetchInitialEvents(): Promise<{
|
||||
originalEvent: MatrixEvent;
|
||||
events: MatrixEvent[];
|
||||
nextBatch?: string;
|
||||
prevBatch?: string;
|
||||
} | null> {
|
||||
if (Thread.hasServerSideSupport === undefined) {
|
||||
await Thread.serverSupportPromise;
|
||||
}
|
||||
|
||||
if (!Thread.hasServerSideSupport) {
|
||||
this.initialEventsFetched = true;
|
||||
return true;
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const response = await this.fetchEvents();
|
||||
this.initialEventsFetched = true;
|
||||
return response;
|
||||
} catch (e) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,6 +323,10 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
nextBatch?: string;
|
||||
prevBatch?: string;
|
||||
}> {
|
||||
if (Thread.hasServerSideSupport === undefined) {
|
||||
await Thread.serverSupportPromise;
|
||||
}
|
||||
|
||||
let {
|
||||
originalEvent,
|
||||
events,
|
||||
@@ -297,13 +343,13 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
// When there's no nextBatch returned with a `from` request we have reached
|
||||
// the end of the thread, and therefore want to return an empty one
|
||||
if (!opts.to && !nextBatch) {
|
||||
events = [originalEvent, ...events];
|
||||
events = [...events, originalEvent];
|
||||
}
|
||||
|
||||
for (const event of events) {
|
||||
await this.client.decryptEventIfNeeded(event);
|
||||
await Promise.all(events.map(event => {
|
||||
this.setEventMetadata(event);
|
||||
}
|
||||
return this.client.decryptEventIfNeeded(event);
|
||||
}));
|
||||
|
||||
const prependEvents = !opts.direction || opts.direction === Direction.Backward;
|
||||
|
||||
|
||||
@@ -14,13 +14,28 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
enum EventEmitterEvents {
|
||||
export enum EventEmitterEvents {
|
||||
NewListener = "newListener",
|
||||
RemoveListener = "removeListener",
|
||||
Error = "error",
|
||||
}
|
||||
|
||||
type AnyListener = (...args: any) => any;
|
||||
export type ListenerMap<E extends string> = { [eventName in E]: AnyListener };
|
||||
type EventEmitterEventListener = (eventName: string, listener: AnyListener) => void;
|
||||
type EventEmitterErrorListener = (error: Error) => void;
|
||||
|
||||
export type Listener<
|
||||
E extends string,
|
||||
A extends ListenerMap<E>,
|
||||
T extends E | EventEmitterEvents,
|
||||
> = T extends E ? A[T]
|
||||
: T extends EventEmitterEvents ? EventEmitterErrorListener
|
||||
: EventEmitterEventListener;
|
||||
|
||||
/**
|
||||
* Typed Event Emitter class which can act as a Base Model for all our model
|
||||
* and communication events.
|
||||
@@ -28,17 +43,26 @@ enum EventEmitterEvents {
|
||||
* to properly type this, so that our events are not stringly-based and prone
|
||||
* to silly typos.
|
||||
*/
|
||||
export abstract class TypedEventEmitter<Events extends string> extends EventEmitter {
|
||||
public addListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
export class TypedEventEmitter<
|
||||
Events extends string,
|
||||
Arguments extends ListenerMap<Events>,
|
||||
SuperclassArguments extends ListenerMap<any> = Arguments,
|
||||
> extends EventEmitter {
|
||||
public addListener<T extends Events | EventEmitterEvents>(
|
||||
event: T,
|
||||
listener: Listener<Events, Arguments, T>,
|
||||
): this {
|
||||
return super.addListener(event, listener);
|
||||
}
|
||||
|
||||
public emit(event: Events | EventEmitterEvents, ...args: any[]): boolean {
|
||||
public emit<T extends Events>(event: T, ...args: Parameters<SuperclassArguments[T]>): boolean;
|
||||
public emit<T extends Events>(event: T, ...args: Parameters<Arguments[T]>): boolean;
|
||||
public emit<T extends Events>(event: T, ...args: any[]): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
public eventNames(): (Events | EventEmitterEvents)[] {
|
||||
return super.eventNames() as Events[];
|
||||
return super.eventNames() as Array<Events | EventEmitterEvents>;
|
||||
}
|
||||
|
||||
public listenerCount(event: Events | EventEmitterEvents): number {
|
||||
@@ -49,23 +73,38 @@ export abstract class TypedEventEmitter<Events extends string> extends EventEmit
|
||||
return super.listeners(event);
|
||||
}
|
||||
|
||||
public off(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
public off<T extends Events | EventEmitterEvents>(
|
||||
event: T,
|
||||
listener: Listener<Events, Arguments, T>,
|
||||
): this {
|
||||
return super.off(event, listener);
|
||||
}
|
||||
|
||||
public on(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
public on<T extends Events | EventEmitterEvents>(
|
||||
event: T,
|
||||
listener: Listener<Events, Arguments, T>,
|
||||
): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
public once(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
public once<T extends Events | EventEmitterEvents>(
|
||||
event: T,
|
||||
listener: Listener<Events, Arguments, T>,
|
||||
): this {
|
||||
return super.once(event, listener);
|
||||
}
|
||||
|
||||
public prependListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
public prependListener<T extends Events | EventEmitterEvents>(
|
||||
event: T,
|
||||
listener: Listener<Events, Arguments, T>,
|
||||
): this {
|
||||
return super.prependListener(event, listener);
|
||||
}
|
||||
|
||||
public prependOnceListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
public prependOnceListener<T extends Events | EventEmitterEvents>(
|
||||
event: T,
|
||||
listener: Listener<Events, Arguments, T>,
|
||||
): this {
|
||||
return super.prependOnceListener(event, listener);
|
||||
}
|
||||
|
||||
@@ -73,7 +112,10 @@ export abstract class TypedEventEmitter<Events extends string> extends EventEmit
|
||||
return super.removeAllListeners(event);
|
||||
}
|
||||
|
||||
public removeListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
public removeListener<T extends Events | EventEmitterEvents>(
|
||||
event: T,
|
||||
listener: Listener<Events, Arguments, T>,
|
||||
): this {
|
||||
return super.removeListener(event, listener);
|
||||
}
|
||||
|
||||
|
||||
+31
-14
@@ -18,12 +18,29 @@ limitations under the License.
|
||||
* @module models/user
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { MatrixEvent } from "./event";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
|
||||
export class User extends EventEmitter {
|
||||
// eslint-disable-next-line camelcase
|
||||
export enum UserEvent {
|
||||
DisplayName = "User.displayName",
|
||||
AvatarUrl = "User.avatarUrl",
|
||||
Presence = "User.presence",
|
||||
CurrentlyActive = "User.currentlyActive",
|
||||
LastPresenceTs = "User.lastPresenceTs",
|
||||
/* @deprecated */
|
||||
_UnstableStatusMessage = "User.unstable_statusMessage",
|
||||
}
|
||||
|
||||
export type UserEventHandlerMap = {
|
||||
[UserEvent.DisplayName]: (event: MatrixEvent | undefined, user: User) => void;
|
||||
[UserEvent.AvatarUrl]: (event: MatrixEvent | undefined, user: User) => void;
|
||||
[UserEvent.Presence]: (event: MatrixEvent | undefined, user: User) => void;
|
||||
[UserEvent.CurrentlyActive]: (event: MatrixEvent | undefined, user: User) => void;
|
||||
[UserEvent.LastPresenceTs]: (event: MatrixEvent | undefined, user: User) => void;
|
||||
[UserEvent._UnstableStatusMessage]: (user: User) => void;
|
||||
};
|
||||
|
||||
export class User extends TypedEventEmitter<UserEvent, UserEventHandlerMap> {
|
||||
private modified: number;
|
||||
|
||||
// XXX these should be read-only
|
||||
@@ -39,9 +56,9 @@ export class User extends EventEmitter {
|
||||
presence?: MatrixEvent;
|
||||
profile?: MatrixEvent;
|
||||
} = {
|
||||
presence: null,
|
||||
profile: null,
|
||||
};
|
||||
presence: null,
|
||||
profile: null,
|
||||
};
|
||||
// eslint-disable-next-line camelcase
|
||||
public unstable_statusMessage = "";
|
||||
|
||||
@@ -94,25 +111,25 @@ export class User extends EventEmitter {
|
||||
const firstFire = this.events.presence === null;
|
||||
this.events.presence = event;
|
||||
|
||||
const eventsToFire = [];
|
||||
const eventsToFire: UserEvent[] = [];
|
||||
if (event.getContent().presence !== this.presence || firstFire) {
|
||||
eventsToFire.push("User.presence");
|
||||
eventsToFire.push(UserEvent.Presence);
|
||||
}
|
||||
if (event.getContent().avatar_url &&
|
||||
event.getContent().avatar_url !== this.avatarUrl) {
|
||||
eventsToFire.push("User.avatarUrl");
|
||||
eventsToFire.push(UserEvent.AvatarUrl);
|
||||
}
|
||||
if (event.getContent().displayname &&
|
||||
event.getContent().displayname !== this.displayName) {
|
||||
eventsToFire.push("User.displayName");
|
||||
eventsToFire.push(UserEvent.DisplayName);
|
||||
}
|
||||
if (event.getContent().currently_active !== undefined &&
|
||||
event.getContent().currently_active !== this.currentlyActive) {
|
||||
eventsToFire.push("User.currentlyActive");
|
||||
eventsToFire.push(UserEvent.CurrentlyActive);
|
||||
}
|
||||
|
||||
this.presence = event.getContent().presence;
|
||||
eventsToFire.push("User.lastPresenceTs");
|
||||
eventsToFire.push(UserEvent.LastPresenceTs);
|
||||
|
||||
if (event.getContent().status_msg) {
|
||||
this.presenceStatusMsg = event.getContent().status_msg;
|
||||
@@ -213,7 +230,7 @@ export class User extends EventEmitter {
|
||||
if (!event.getContent()) this.unstable_statusMessage = "";
|
||||
else this.unstable_statusMessage = event.getContent()["status"];
|
||||
this.updateModifiedTime();
|
||||
this.emit("User.unstable_statusMessage", this);
|
||||
this.emit(UserEvent._UnstableStatusMessage, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -158,8 +158,7 @@ export class PushProcessor {
|
||||
.find((r) => r.rule_id === override.rule_id);
|
||||
|
||||
if (existingRule) {
|
||||
// Copy over the actions, default, and conditions. Don't touch the user's
|
||||
// preference.
|
||||
// Copy over the actions, default, and conditions. Don't touch the user's preference.
|
||||
existingRule.default = override.default;
|
||||
existingRule.conditions = override.conditions;
|
||||
existingRule.actions = override.actions;
|
||||
@@ -447,6 +446,8 @@ export class PushProcessor {
|
||||
}
|
||||
|
||||
public ruleMatchesEvent(rule: IPushRule, ev: MatrixEvent): boolean {
|
||||
if (!rule.conditions?.length) return true;
|
||||
|
||||
let ret = true;
|
||||
for (let i = 0; i < rule.conditions.length; ++i) {
|
||||
const cond = rule.conditions[i];
|
||||
|
||||
@@ -16,8 +16,6 @@ limitations under the License.
|
||||
|
||||
/* eslint-disable @babel/no-invalid-this */
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { MemoryStore, IOpts as IBaseOpts } from "./memory";
|
||||
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend";
|
||||
import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend";
|
||||
@@ -27,6 +25,7 @@ import { logger } from '../logger';
|
||||
import { ISavedSync } from "./index";
|
||||
import { IIndexedDBBackend } from "./indexeddb-backend";
|
||||
import { ISyncResponse } from "../sync-accumulator";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
|
||||
/**
|
||||
* This is an internal module. See {@link IndexedDBStore} for the public class.
|
||||
@@ -46,6 +45,10 @@ interface IOpts extends IBaseOpts {
|
||||
workerFactory?: () => Worker;
|
||||
}
|
||||
|
||||
type EventHandlerMap = {
|
||||
"degraded": (e: Error) => void;
|
||||
};
|
||||
|
||||
export class IndexedDBStore extends MemoryStore {
|
||||
static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
|
||||
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
|
||||
@@ -59,7 +62,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
// the database, such that we can derive the set if users that have been
|
||||
// modified since we last saved.
|
||||
private userModifiedMap: Record<string, number> = {}; // user_id : timestamp
|
||||
private emitter = new EventEmitter();
|
||||
private emitter = new TypedEventEmitter<keyof EventHandlerMap, EventHandlerMap>();
|
||||
|
||||
/**
|
||||
* Construct a new Indexed Database store, which extends MemoryStore.
|
||||
|
||||
@@ -25,6 +25,15 @@ export enum LocalStorageErrors {
|
||||
QuotaExceededError = 'QuotaExceededError'
|
||||
}
|
||||
|
||||
type EventHandlerMap = {
|
||||
[LocalStorageErrors.Global]: (error: Error) => void;
|
||||
[LocalStorageErrors.SetItemError]: (error: Error) => void;
|
||||
[LocalStorageErrors.GetItemError]: (error: Error) => void;
|
||||
[LocalStorageErrors.RemoveItemError]: (error: Error) => void;
|
||||
[LocalStorageErrors.ClearError]: (error: Error) => void;
|
||||
[LocalStorageErrors.QuotaExceededError]: (error: Error) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used in element-web as a temporary hack to handle all the localStorage errors on the highest level possible
|
||||
* As of 15.11.2021 (DD/MM/YYYY) we're not properly handling local storage exceptions anywhere.
|
||||
@@ -33,5 +42,5 @@ export enum LocalStorageErrors {
|
||||
* maybe you should check out your disk, as it's probably dying and your session may die with it.
|
||||
* See: https://github.com/vector-im/element-web/issues/18423
|
||||
*/
|
||||
class LocalStorageErrorsEventsEmitter extends TypedEventEmitter<LocalStorageErrors> {}
|
||||
class LocalStorageErrorsEventsEmitter extends TypedEventEmitter<LocalStorageErrors, EventHandlerMap> {}
|
||||
export const localStorageErrorsEventsEmitter = new LocalStorageErrorsEventsEmitter();
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@ import { Group } from "../models/group";
|
||||
import { Room } from "../models/room";
|
||||
import { User } from "../models/user";
|
||||
import { IEvent, MatrixEvent } from "../models/event";
|
||||
import { RoomState } from "../models/room-state";
|
||||
import { RoomState, RoomStateEvent } from "../models/room-state";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { Filter } from "../filter";
|
||||
import { ISavedSync, IStore } from "./index";
|
||||
@@ -126,7 +126,7 @@ export class MemoryStore implements IStore {
|
||||
this.rooms[room.roomId] = room;
|
||||
// add listeners for room member changes so we can keep the room member
|
||||
// map up-to-date.
|
||||
room.currentState.on("RoomState.members", this.onRoomMember);
|
||||
room.currentState.on(RoomStateEvent.Members, this.onRoomMember);
|
||||
// add existing members
|
||||
room.currentState.getMembers().forEach((m) => {
|
||||
this.onRoomMember(null, room.currentState, m);
|
||||
@@ -185,7 +185,7 @@ export class MemoryStore implements IStore {
|
||||
*/
|
||||
public removeRoom(roomId: string): void {
|
||||
if (this.rooms[roomId]) {
|
||||
this.rooms[roomId].removeListener("RoomState.members", this.onRoomMember);
|
||||
this.rooms[roomId].currentState.removeListener(RoomStateEvent.Members, this.onRoomMember);
|
||||
}
|
||||
delete this.rooms[roomId];
|
||||
}
|
||||
|
||||
+74
-57
@@ -23,8 +23,8 @@ limitations under the License.
|
||||
* for HTTP and WS at some point.
|
||||
*/
|
||||
|
||||
import { User } from "./models/user";
|
||||
import { NotificationCountType, Room } from "./models/room";
|
||||
import { User, UserEvent } from "./models/user";
|
||||
import { NotificationCountType, Room, RoomEvent } from "./models/room";
|
||||
import { Group } from "./models/group";
|
||||
import * as utils from "./utils";
|
||||
import { IDeferred } from "./utils";
|
||||
@@ -33,7 +33,7 @@ import { EventTimeline } from "./models/event-timeline";
|
||||
import { PushProcessor } from "./pushprocessor";
|
||||
import { logger } from './logger';
|
||||
import { InvalidStoreError } from './errors';
|
||||
import { IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client";
|
||||
import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client";
|
||||
import {
|
||||
Category,
|
||||
IEphemeral,
|
||||
@@ -53,6 +53,8 @@ import { MatrixError, Method } from "./http-api";
|
||||
import { ISavedSync } from "./store";
|
||||
import { EventType } from "./@types/event";
|
||||
import { IPushRules } from "./@types/PushRules";
|
||||
import { RoomStateEvent } from "./models/room-state";
|
||||
import { RoomMemberEvent } from "./models/room-member";
|
||||
|
||||
const DEBUG = true;
|
||||
|
||||
@@ -171,8 +173,10 @@ export class SyncApi {
|
||||
}
|
||||
|
||||
if (client.getNotifTimelineSet()) {
|
||||
client.reEmitter.reEmit(client.getNotifTimelineSet(),
|
||||
["Room.timeline", "Room.timelineReset"]);
|
||||
client.reEmitter.reEmit(client.getNotifTimelineSet(), [
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,16 +196,17 @@ export class SyncApi {
|
||||
timelineSupport,
|
||||
unstableClientRelationAggregation,
|
||||
});
|
||||
client.reEmitter.reEmit(room, ["Room.name", "Room.timeline",
|
||||
"Room.redaction",
|
||||
"Room.redactionCancelled",
|
||||
"Room.receipt", "Room.tags",
|
||||
"Room.timelineReset",
|
||||
"Room.localEchoUpdated",
|
||||
"Room.accountData",
|
||||
"Room.myMembership",
|
||||
"Room.replaceEvent",
|
||||
"Room.visibilityChange",
|
||||
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);
|
||||
return room;
|
||||
@@ -214,7 +219,10 @@ export class SyncApi {
|
||||
public createGroup(groupId: string): Group {
|
||||
const client = this.client;
|
||||
const group = new Group(groupId);
|
||||
client.reEmitter.reEmit(group, ["Group.profile", "Group.myMembership"]);
|
||||
client.reEmitter.reEmit(group, [
|
||||
ClientEvent.GroupProfile,
|
||||
ClientEvent.GroupMyMembership,
|
||||
]);
|
||||
client.store.storeGroup(group);
|
||||
return group;
|
||||
}
|
||||
@@ -229,17 +237,19 @@ export class SyncApi {
|
||||
// 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, [
|
||||
"RoomState.events", "RoomState.members", "RoomState.newMember",
|
||||
RoomStateEvent.Events,
|
||||
RoomStateEvent.Members,
|
||||
RoomStateEvent.NewMember,
|
||||
RoomStateEvent.Update,
|
||||
]);
|
||||
room.currentState.on("RoomState.newMember", function(event, state, member) {
|
||||
room.currentState.on(RoomStateEvent.NewMember, function(event, state, member) {
|
||||
member.user = client.getUser(member.userId);
|
||||
client.reEmitter.reEmit(
|
||||
member,
|
||||
[
|
||||
"RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel",
|
||||
"RoomMember.membership",
|
||||
],
|
||||
);
|
||||
client.reEmitter.reEmit(member, [
|
||||
RoomMemberEvent.Name,
|
||||
RoomMemberEvent.Typing,
|
||||
RoomMemberEvent.PowerLevel,
|
||||
RoomMemberEvent.Membership,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -249,9 +259,9 @@ export class SyncApi {
|
||||
*/
|
||||
private deregisterStateListeners(room: Room): void {
|
||||
// could do with a better way of achieving this.
|
||||
room.currentState.removeAllListeners("RoomState.events");
|
||||
room.currentState.removeAllListeners("RoomState.members");
|
||||
room.currentState.removeAllListeners("RoomState.newMember");
|
||||
room.currentState.removeAllListeners(RoomStateEvent.Events);
|
||||
room.currentState.removeAllListeners(RoomStateEvent.Members);
|
||||
room.currentState.removeAllListeners(RoomStateEvent.NewMember);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -310,11 +320,11 @@ export class SyncApi {
|
||||
EventTimeline.BACKWARDS);
|
||||
|
||||
this.processRoomEvents(room, stateEvents, timelineEvents);
|
||||
await this.processThreadEvents(room, threadedEvents);
|
||||
await this.processThreadEvents(room, threadedEvents, false);
|
||||
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
client.emit("Room", room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
|
||||
this.processEventsForNotifs(room, events);
|
||||
});
|
||||
@@ -362,7 +372,7 @@ export class SyncApi {
|
||||
user.setPresenceEvent(presenceEvent);
|
||||
client.store.storeUser(user);
|
||||
}
|
||||
client.emit("event", presenceEvent);
|
||||
client.emit(ClientEvent.Event, presenceEvent);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -388,7 +398,7 @@ export class SyncApi {
|
||||
response.messages.start);
|
||||
|
||||
client.store.storeRoom(this._peekRoom);
|
||||
client.emit("Room", this._peekRoom);
|
||||
client.emit(ClientEvent.Room, this._peekRoom);
|
||||
|
||||
this.peekPoll(this._peekRoom);
|
||||
return this._peekRoom;
|
||||
@@ -445,7 +455,7 @@ export class SyncApi {
|
||||
user.setPresenceEvent(presenceEvent);
|
||||
this.client.store.storeUser(user);
|
||||
}
|
||||
this.client.emit("event", presenceEvent);
|
||||
this.client.emit(ClientEvent.Event, presenceEvent);
|
||||
});
|
||||
|
||||
// strip out events which aren't for the given room_id (e.g presence)
|
||||
@@ -840,7 +850,7 @@ export class SyncApi {
|
||||
logger.error("Caught /sync error", e.stack || e);
|
||||
|
||||
// Emit the exception for client handling
|
||||
this.client.emit("sync.unexpectedError", e);
|
||||
this.client.emit(ClientEvent.SyncUnexpectedError, e);
|
||||
}
|
||||
|
||||
// update this as it may have changed
|
||||
@@ -1073,7 +1083,7 @@ export class SyncApi {
|
||||
user.setPresenceEvent(presenceEvent);
|
||||
client.store.storeUser(user);
|
||||
}
|
||||
client.emit("event", presenceEvent);
|
||||
client.emit(ClientEvent.Event, presenceEvent);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1096,7 +1106,7 @@ export class SyncApi {
|
||||
client.pushRules = PushProcessor.rewriteDefaultRules(rules);
|
||||
}
|
||||
const prevEvent = prevEventsMap[accountDataEvent.getId()];
|
||||
client.emit("accountData", accountDataEvent, prevEvent);
|
||||
client.emit(ClientEvent.AccountData, accountDataEvent, prevEvent);
|
||||
return accountDataEvent;
|
||||
},
|
||||
);
|
||||
@@ -1149,7 +1159,7 @@ export class SyncApi {
|
||||
}
|
||||
}
|
||||
|
||||
client.emit("toDeviceEvent", toDeviceEvent);
|
||||
client.emit(ClientEvent.ToDeviceEvent, toDeviceEvent);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
@@ -1201,10 +1211,10 @@ export class SyncApi {
|
||||
if (inviteObj.isBrandNewRoom) {
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
client.emit("Room", room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
}
|
||||
stateEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
room.updateMyMembership("invite");
|
||||
});
|
||||
@@ -1307,7 +1317,7 @@ export class SyncApi {
|
||||
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
|
||||
|
||||
this.processRoomEvents(room, stateEvents, timelineEvents, syncEventData.fromCache);
|
||||
await this.processThreadEvents(room, threadedEvents);
|
||||
await this.processThreadEvents(room, threadedEvents, false);
|
||||
|
||||
// set summary after processing events,
|
||||
// because it will trigger a name calculation
|
||||
@@ -1325,13 +1335,13 @@ export class SyncApi {
|
||||
room.recalculate();
|
||||
if (joinObj.isBrandNewRoom) {
|
||||
client.store.storeRoom(room);
|
||||
client.emit("Room", room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
}
|
||||
|
||||
this.processEventsForNotifs(room, events);
|
||||
|
||||
const processRoomEvent = async (e) => {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
if (e.isState() && e.getType() == "m.room.encryption" && this.opts.crypto) {
|
||||
await this.opts.crypto.onCryptoEvent(e);
|
||||
}
|
||||
@@ -1351,10 +1361,10 @@ export class SyncApi {
|
||||
await utils.promiseMapSeries(timelineEvents, processRoomEvent);
|
||||
await utils.promiseMapSeries(threadedEvents, processRoomEvent);
|
||||
ephemeralEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
accountDataEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
|
||||
room.updateMyMembership("join");
|
||||
@@ -1375,28 +1385,28 @@ export class SyncApi {
|
||||
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
|
||||
|
||||
this.processRoomEvents(room, stateEvents, timelineEvents);
|
||||
await this.processThreadEvents(room, threadedEvents);
|
||||
await this.processThreadEvents(room, threadedEvents, false);
|
||||
room.addAccountData(accountDataEvents);
|
||||
|
||||
room.recalculate();
|
||||
if (leaveObj.isBrandNewRoom) {
|
||||
client.store.storeRoom(room);
|
||||
client.emit("Room", room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
}
|
||||
|
||||
this.processEventsForNotifs(room, events);
|
||||
|
||||
stateEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
timelineEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
threadedEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
accountDataEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
|
||||
room.updateMyMembership("leave");
|
||||
@@ -1551,7 +1561,7 @@ export class SyncApi {
|
||||
group.setMyMembership(sectionName);
|
||||
if (isBrandNew) {
|
||||
// Now we've filled in all the fields, emit the Group event
|
||||
this.client.emit("Group", group);
|
||||
this.client.emit(ClientEvent.Group, group);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1720,14 +1730,18 @@ export class SyncApi {
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): Promise<void> {
|
||||
return this.client.processThreadEvents(room, threadedEvents);
|
||||
private processThreadEvents(
|
||||
room: Room,
|
||||
threadedEvents: MatrixEvent[],
|
||||
toStartOfTimeline: boolean,
|
||||
): Promise<void> {
|
||||
return this.client.processThreadEvents(room, threadedEvents, toStartOfTimeline);
|
||||
}
|
||||
|
||||
// extractRelatedEvents(event: MatrixEvent, events: MatrixEvent[], relatedEvents: MatrixEvent[] = []): MatrixEvent[] {
|
||||
// relatedEvents.push(event);
|
||||
|
||||
// const parentEventId = event.parentEventId;
|
||||
// const parentEventId = event.getAssociatedId();
|
||||
// const parentEventIndex = events.findIndex(event => event.getId() === parentEventId);
|
||||
|
||||
// if (parentEventIndex > -1) {
|
||||
@@ -1778,7 +1792,7 @@ export class SyncApi {
|
||||
const old = this.syncState;
|
||||
this.syncState = newState;
|
||||
this.syncStateData = data;
|
||||
this.client.emit("sync", this.syncState, old, data);
|
||||
this.client.emit(ClientEvent.Sync, this.syncState, old, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1796,8 +1810,11 @@ export class SyncApi {
|
||||
function createNewUser(client: MatrixClient, userId: string): User {
|
||||
const user = new User(userId);
|
||||
client.reEmitter.reEmit(user, [
|
||||
"User.avatarUrl", "User.displayName", "User.presence",
|
||||
"User.currentlyActive", "User.lastPresenceTs",
|
||||
UserEvent.AvatarUrl,
|
||||
UserEvent.DisplayName,
|
||||
UserEvent.Presence,
|
||||
UserEvent.CurrentlyActive,
|
||||
UserEvent.LastPresenceTs,
|
||||
]);
|
||||
return user;
|
||||
}
|
||||
|
||||
+1
-1
@@ -443,7 +443,7 @@ export function isNullOrUndefined(val: any): boolean {
|
||||
}
|
||||
|
||||
export interface IDeferred<T> {
|
||||
resolve: (value: T) => void;
|
||||
resolve: (value: T | Promise<T>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
promise: Promise<T>;
|
||||
}
|
||||
|
||||
+98
-33
@@ -2,6 +2,7 @@
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 - 2022 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -21,8 +22,6 @@ limitations under the License.
|
||||
* @module webrtc/call
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import * as utils from '../utils';
|
||||
import { MatrixEvent } from '../models/event';
|
||||
@@ -47,6 +46,7 @@ import {
|
||||
import { CallFeed } from './callFeed';
|
||||
import { MatrixClient } from "../client";
|
||||
import { ISendEventResponse } from "../@types/requests";
|
||||
import { EventEmitterEvents, TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
|
||||
// events: hangup, error(err), replaced(call), state(state, oldState)
|
||||
|
||||
@@ -241,6 +241,21 @@ function genCallID(): string {
|
||||
return Date.now().toString() + randomString(16);
|
||||
}
|
||||
|
||||
export type CallEventHandlerMap = {
|
||||
[CallEvent.DataChannel]: (channel: RTCDataChannel) => void;
|
||||
[CallEvent.FeedsChanged]: (feeds: CallFeed[]) => void;
|
||||
[CallEvent.Replaced]: (newCall: MatrixCall) => void;
|
||||
[CallEvent.Error]: (error: CallError) => void;
|
||||
[CallEvent.RemoteHoldUnhold]: (onHold: boolean) => void;
|
||||
[CallEvent.LocalHoldUnhold]: (onHold: boolean) => void;
|
||||
[CallEvent.LengthChanged]: (length: number) => void;
|
||||
[CallEvent.State]: (state: CallState, oldState?: CallState) => void;
|
||||
[CallEvent.Hangup]: () => void;
|
||||
[CallEvent.AssertedIdentityChanged]: () => void;
|
||||
/* @deprecated */
|
||||
[CallEvent.HoldUnhold]: (onHold: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a new Matrix Call.
|
||||
* @constructor
|
||||
@@ -252,7 +267,7 @@ function genCallID(): string {
|
||||
* @param {Array<Object>} opts.turnServers Optional. A list of TURN servers.
|
||||
* @param {MatrixClient} opts.client The Matrix Client instance to send events to.
|
||||
*/
|
||||
export class MatrixCall extends EventEmitter {
|
||||
export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap> {
|
||||
public roomId: string;
|
||||
public callId: string;
|
||||
public state = CallState.Fledgling;
|
||||
@@ -571,9 +586,11 @@ export class MatrixCall extends EventEmitter {
|
||||
private pushNewLocalFeed(stream: MediaStream, purpose: SDPStreamMetadataPurpose, addToPeerConnection = true): void {
|
||||
const userId = this.client.getUserId();
|
||||
|
||||
// TODO: Find out what is going on here
|
||||
// why do we enable audio (and only audio) tracks here? -- matthew
|
||||
// Tracks don't always start off enabled, eg. chrome will give a disabled
|
||||
// audio track if you ask for user media audio and already had one that
|
||||
// you'd set to disabled (presumably because it clones them internally).
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
setTracksEnabled(stream.getVideoTracks(), true);
|
||||
|
||||
// We try to replace an existing feed if there already is one with the same purpose
|
||||
const existingFeed = this.getLocalFeeds().find((feed) => feed.purpose === purpose);
|
||||
@@ -616,7 +633,8 @@ export class MatrixCall extends EventEmitter {
|
||||
`id="${track.id}", ` +
|
||||
`kind="${track.kind}", ` +
|
||||
`streamId="${callFeed.stream.id}", ` +
|
||||
`streamPurpose="${callFeed.purpose}"` +
|
||||
`streamPurpose="${callFeed.purpose}", ` +
|
||||
`enabled=${track.enabled}` +
|
||||
`) to peer connection`,
|
||||
);
|
||||
senderArray.push(this.peerConn.addTrack(track, callFeed.stream));
|
||||
@@ -933,29 +951,13 @@ export class MatrixCall extends EventEmitter {
|
||||
if (!this.opponentSupportsSDPStreamMetadata()) return;
|
||||
|
||||
try {
|
||||
const upgradeAudio = audio && !this.hasLocalUserMediaAudioTrack;
|
||||
const upgradeVideo = video && !this.hasLocalUserMediaVideoTrack;
|
||||
logger.debug(`Upgrading call: audio?=${upgradeAudio} video?=${upgradeVideo}`);
|
||||
const getAudio = audio || this.hasLocalUserMediaAudioTrack;
|
||||
const getVideo = video || this.hasLocalUserMediaVideoTrack;
|
||||
|
||||
const stream = await this.client.getMediaHandler().getUserMediaStream(upgradeAudio, upgradeVideo, false);
|
||||
if (upgradeAudio && upgradeVideo) {
|
||||
if (this.hasLocalUserMediaAudioTrack) return;
|
||||
if (this.hasLocalUserMediaVideoTrack) return;
|
||||
|
||||
this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia);
|
||||
} else if (upgradeAudio) {
|
||||
if (this.hasLocalUserMediaAudioTrack) return;
|
||||
|
||||
const audioTrack = stream.getAudioTracks()[0];
|
||||
this.localUsermediaStream.addTrack(audioTrack);
|
||||
this.peerConn.addTrack(audioTrack, this.localUsermediaStream);
|
||||
} else if (upgradeVideo) {
|
||||
if (this.hasLocalUserMediaVideoTrack) return;
|
||||
|
||||
const videoTrack = stream.getVideoTracks()[0];
|
||||
this.localUsermediaStream.addTrack(videoTrack);
|
||||
this.peerConn.addTrack(videoTrack, this.localUsermediaStream);
|
||||
}
|
||||
// updateLocalUsermediaStream() will take the tracks, use them as
|
||||
// replacement and throw the stream away, so it isn't reusable
|
||||
const stream = await this.client.getMediaHandler().getUserMediaStream(getAudio, getVideo, false);
|
||||
await this.updateLocalUsermediaStream(stream, audio, video);
|
||||
} catch (error) {
|
||||
logger.error("Failed to upgrade the call", error);
|
||||
this.emit(CallEvent.Error,
|
||||
@@ -1071,6 +1073,63 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces/adds the tracks from the passed stream to the localUsermediaStream
|
||||
* @param {MediaStream} stream to use a replacement for the local usermedia stream
|
||||
*/
|
||||
public async updateLocalUsermediaStream(
|
||||
stream: MediaStream, forceAudio = false, forceVideo = false,
|
||||
): Promise<void> {
|
||||
const callFeed = this.localUsermediaFeed;
|
||||
const audioEnabled = forceAudio || (!callFeed.isAudioMuted() && !this.remoteOnHold);
|
||||
const videoEnabled = forceVideo || (!callFeed.isVideoMuted() && !this.remoteOnHold);
|
||||
setTracksEnabled(stream.getAudioTracks(), audioEnabled);
|
||||
setTracksEnabled(stream.getVideoTracks(), videoEnabled);
|
||||
|
||||
// We want to keep the same stream id, so we replace the tracks rather than the whole stream
|
||||
for (const track of this.localUsermediaStream.getTracks()) {
|
||||
this.localUsermediaStream.removeTrack(track);
|
||||
track.stop();
|
||||
}
|
||||
for (const track of stream.getTracks()) {
|
||||
this.localUsermediaStream.addTrack(track);
|
||||
}
|
||||
|
||||
const newSenders = [];
|
||||
|
||||
for (const track of stream.getTracks()) {
|
||||
const oldSender = this.usermediaSenders.find((sender) => sender.track?.kind === track.kind);
|
||||
let newSender: RTCRtpSender;
|
||||
|
||||
if (oldSender) {
|
||||
logger.info(
|
||||
`Replacing track (` +
|
||||
`id="${track.id}", ` +
|
||||
`kind="${track.kind}", ` +
|
||||
`streamId="${stream.id}", ` +
|
||||
`streamPurpose="${callFeed.purpose}"` +
|
||||
`) to peer connection`,
|
||||
);
|
||||
await oldSender.replaceTrack(track);
|
||||
newSender = oldSender;
|
||||
} else {
|
||||
logger.info(
|
||||
`Adding track (` +
|
||||
`id="${track.id}", ` +
|
||||
`kind="${track.kind}", ` +
|
||||
`streamId="${stream.id}", ` +
|
||||
`streamPurpose="${callFeed.purpose}"` +
|
||||
`) to peer connection`,
|
||||
);
|
||||
newSender = this.peerConn.addTrack(track, this.localUsermediaStream);
|
||||
}
|
||||
|
||||
newSenders.push(newSender);
|
||||
}
|
||||
|
||||
this.usermediaSenders = newSenders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether our outbound video should be muted or not.
|
||||
* @param {boolean} muted True to mute the outbound video.
|
||||
@@ -1199,8 +1258,8 @@ export class MatrixCall extends EventEmitter {
|
||||
[SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(),
|
||||
});
|
||||
|
||||
const micShouldBeMuted = this.localUsermediaFeed?.isAudioMuted() || this.remoteOnHold;
|
||||
const vidShouldBeMuted = this.localUsermediaFeed?.isVideoMuted() || this.remoteOnHold;
|
||||
const micShouldBeMuted = this.isMicrophoneMuted() || this.remoteOnHold;
|
||||
const vidShouldBeMuted = this.isLocalVideoMuted() || this.remoteOnHold;
|
||||
|
||||
setTracksEnabled(this.localUsermediaStream.getAudioTracks(), !micShouldBeMuted);
|
||||
setTracksEnabled(this.localUsermediaStream.getVideoTracks(), !vidShouldBeMuted);
|
||||
@@ -1943,7 +2002,7 @@ export class MatrixCall extends EventEmitter {
|
||||
|
||||
await this.sendVoipEvent(EventType.CallReplaces, bodyToTransferee);
|
||||
|
||||
await this.terminate(CallParty.Local, CallErrorCode.Replaced, true);
|
||||
await this.terminate(CallParty.Local, CallErrorCode.Transfered, true);
|
||||
await transferTargetCall.terminate(CallParty.Local, CallErrorCode.Transfered, true);
|
||||
}
|
||||
|
||||
@@ -1973,7 +2032,7 @@ export class MatrixCall extends EventEmitter {
|
||||
this.peerConn.close();
|
||||
}
|
||||
if (shouldEmit) {
|
||||
this.emit(CallEvent.Hangup, this);
|
||||
this.emit(CallEvent.Hangup);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1995,7 +2054,7 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
|
||||
private checkForErrorListener(): void {
|
||||
if (this.listeners("error").length === 0) {
|
||||
if (this.listeners(EventEmitterEvents.Error).length === 0) {
|
||||
throw new Error(
|
||||
"You MUST attach an error listener using call.on('error', function() {})",
|
||||
);
|
||||
@@ -2064,6 +2123,12 @@ export class MatrixCall extends EventEmitter {
|
||||
|
||||
try {
|
||||
const stream = await this.client.getMediaHandler().getUserMediaStream(audio, video);
|
||||
|
||||
// make sure all the tracks are enabled (same as pushNewLocalFeed -
|
||||
// we probably ought to just have one code path for adding streams)
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
setTracksEnabled(stream.getVideoTracks(), true);
|
||||
|
||||
const callFeed = new CallFeed({
|
||||
client: this.client,
|
||||
roomId: this.roomId,
|
||||
|
||||
@@ -14,17 +14,27 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from '../models/event';
|
||||
import { MatrixEvent, MatrixEventEvent } from '../models/event';
|
||||
import { logger } from '../logger';
|
||||
import { createNewMatrixCall, MatrixCall, CallErrorCode, CallState, CallDirection } from './call';
|
||||
import { CallDirection, CallErrorCode, CallState, createNewMatrixCall, MatrixCall } from './call';
|
||||
import { EventType } from '../@types/event';
|
||||
import { MatrixClient } from '../client';
|
||||
import { ClientEvent, MatrixClient } from '../client';
|
||||
import { MCallAnswer, MCallHangupReject } from "./callEventTypes";
|
||||
import { SyncState } from "../sync";
|
||||
import { RoomEvent } from "../models/room";
|
||||
|
||||
// Don't ring unless we'd be ringing for at least 3 seconds: the user needs some
|
||||
// time to press the 'accept' button
|
||||
const RING_GRACE_PERIOD = 3000;
|
||||
|
||||
export enum CallEventHandlerEvent {
|
||||
Incoming = "Call.incoming",
|
||||
}
|
||||
|
||||
export type CallEventHandlerEventHandlerMap = {
|
||||
[CallEventHandlerEvent.Incoming]: (call: MatrixCall) => void;
|
||||
};
|
||||
|
||||
export class CallEventHandler {
|
||||
client: MatrixClient;
|
||||
calls: Map<string, MatrixCall>;
|
||||
@@ -47,17 +57,17 @@ export class CallEventHandler {
|
||||
}
|
||||
|
||||
public start() {
|
||||
this.client.on("sync", this.evaluateEventBuffer);
|
||||
this.client.on("Room.timeline", this.onRoomTimeline);
|
||||
this.client.on(ClientEvent.Sync, this.evaluateEventBuffer);
|
||||
this.client.on(RoomEvent.Timeline, this.onRoomTimeline);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.client.removeListener("sync", this.evaluateEventBuffer);
|
||||
this.client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
this.client.removeListener(ClientEvent.Sync, this.evaluateEventBuffer);
|
||||
this.client.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
|
||||
}
|
||||
|
||||
private evaluateEventBuffer = async () => {
|
||||
if (this.client.getSyncState() === "SYNCING") {
|
||||
if (this.client.getSyncState() === SyncState.Syncing) {
|
||||
await Promise.all(this.callEventBuffer.map(event => {
|
||||
this.client.decryptEventIfNeeded(event);
|
||||
}));
|
||||
@@ -101,7 +111,7 @@ export class CallEventHandler {
|
||||
|
||||
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
|
||||
// add an event listener for once the event is decrypted.
|
||||
event.once("Event.decrypted", async () => {
|
||||
event.once(MatrixEventEvent.Decrypted, async () => {
|
||||
if (!this.eventIsACall(event)) return;
|
||||
|
||||
if (this.callEventBuffer.includes(event)) {
|
||||
@@ -221,7 +231,7 @@ export class CallEventHandler {
|
||||
call.hangup(CallErrorCode.Replaced, true);
|
||||
}
|
||||
} else {
|
||||
this.client.emit("Call.incoming", call);
|
||||
this.client.emit(CallEventHandlerEvent.Incoming, call);
|
||||
}
|
||||
return;
|
||||
} else if (type === EventType.CallCandidates) {
|
||||
@@ -257,7 +267,11 @@ export class CallEventHandler {
|
||||
} else {
|
||||
call.onRejectReceived(content as MCallHangupReject);
|
||||
}
|
||||
this.calls.delete(content.call_id);
|
||||
|
||||
// @ts-expect-error typescript thinks the state can't be 'ended' because we're
|
||||
// inside the if block where it wasn't, but it could have changed because
|
||||
// on[Hangup|Reject]Received are side-effecty.
|
||||
if (call.state === CallState.Ended) this.calls.delete(content.call_id);
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
|
||||
import { SDPStreamMetadataPurpose } from "./callEventTypes";
|
||||
import { MatrixClient } from "../client";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
|
||||
const POLLING_INTERVAL = 200; // ms
|
||||
export const SPEAKING_THRESHOLD = -60; // dB
|
||||
@@ -47,7 +46,14 @@ export enum CallFeedEvent {
|
||||
Speaking = "speaking",
|
||||
}
|
||||
|
||||
export class CallFeed extends EventEmitter {
|
||||
type EventHandlerMap = {
|
||||
[CallFeedEvent.NewStream]: (stream: MediaStream) => void;
|
||||
[CallFeedEvent.MuteStateChanged]: (audioMuted: boolean, videoMuted: boolean) => void;
|
||||
[CallFeedEvent.VolumeChanged]: (volume: number) => void;
|
||||
[CallFeedEvent.Speaking]: (speaking: boolean) => void;
|
||||
};
|
||||
|
||||
export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap> {
|
||||
public stream: MediaStream;
|
||||
public userId: string;
|
||||
public purpose: SDPStreamMetadataPurpose;
|
||||
|
||||
+86
-16
@@ -2,7 +2,7 @@
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
Copyright 2021 - 2022 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -18,20 +18,30 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { MatrixClient } from "../client";
|
||||
import { CallState } from "./call";
|
||||
|
||||
export class MediaHandler {
|
||||
private audioInput: string;
|
||||
private videoInput: string;
|
||||
private userMediaStreams: MediaStream[] = [];
|
||||
private screensharingStreams: MediaStream[] = [];
|
||||
private localUserMediaStream?: MediaStream;
|
||||
public userMediaStreams: MediaStream[] = [];
|
||||
public screensharingStreams: MediaStream[] = [];
|
||||
|
||||
constructor(private client: MatrixClient) { }
|
||||
|
||||
/**
|
||||
* Set an audio input device to use for MatrixCalls
|
||||
* @param {string} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
public setAudioInput(deviceId: string): void {
|
||||
public async setAudioInput(deviceId: string): Promise<void> {
|
||||
logger.info("LOG setting audio input to", deviceId);
|
||||
|
||||
if (this.audioInput === deviceId) return;
|
||||
|
||||
this.audioInput = deviceId;
|
||||
await this.updateLocalUsermediaStreams();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,8 +49,39 @@ export class MediaHandler {
|
||||
* @param {string} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
public setVideoInput(deviceId: string): void {
|
||||
public async setVideoInput(deviceId: string): Promise<void> {
|
||||
logger.info("LOG setting video input to", deviceId);
|
||||
|
||||
if (this.videoInput === deviceId) return;
|
||||
|
||||
this.videoInput = deviceId;
|
||||
await this.updateLocalUsermediaStreams();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests new usermedia streams and replace the old ones
|
||||
*/
|
||||
public async updateLocalUsermediaStreams(): Promise<void> {
|
||||
if (this.userMediaStreams.length === 0) return;
|
||||
|
||||
const callMediaStreamParams: Map<string, { audio: boolean, video: boolean }> = new Map();
|
||||
for (const call of this.client.callEventHandler.calls.values()) {
|
||||
callMediaStreamParams.set(call.callId, {
|
||||
audio: call.hasLocalUserMediaAudioTrack,
|
||||
video: call.hasLocalUserMediaVideoTrack,
|
||||
});
|
||||
}
|
||||
|
||||
for (const call of this.client.callEventHandler.calls.values()) {
|
||||
if (call.state === CallState.Ended || !callMediaStreamParams.has(call.callId)) continue;
|
||||
|
||||
const { audio, video } = callMediaStreamParams.get(call.callId);
|
||||
|
||||
// This stream won't be reusable as we will replace the tracks of the old stream
|
||||
const stream = await this.getUserMediaStream(audio, video, false);
|
||||
|
||||
await call.updateLocalUsermediaStream(stream);
|
||||
}
|
||||
}
|
||||
|
||||
public async hasAudioDevice(): Promise<boolean> {
|
||||
@@ -65,20 +106,44 @@ export class MediaHandler {
|
||||
|
||||
let stream: MediaStream;
|
||||
|
||||
// Find a stream with matching tracks
|
||||
const matchingStream = this.userMediaStreams.find((stream) => {
|
||||
if (shouldRequestAudio !== (stream.getAudioTracks().length > 0)) return false;
|
||||
if (shouldRequestVideo !== (stream.getVideoTracks().length > 0)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (matchingStream) {
|
||||
logger.log("Cloning user media stream", matchingStream.id);
|
||||
stream = matchingStream.clone();
|
||||
} else {
|
||||
if (
|
||||
!this.localUserMediaStream ||
|
||||
(this.localUserMediaStream.getAudioTracks().length === 0 && shouldRequestAudio) ||
|
||||
(this.localUserMediaStream.getVideoTracks().length === 0 && shouldRequestVideo) ||
|
||||
(this.localUserMediaStream.getAudioTracks()[0]?.getSettings()?.deviceId !== this.audioInput) ||
|
||||
(this.localUserMediaStream.getVideoTracks()[0]?.getSettings()?.deviceId !== this.videoInput)
|
||||
) {
|
||||
const constraints = this.getUserMediaContraints(shouldRequestAudio, shouldRequestVideo);
|
||||
logger.log("Getting user media with constraints", constraints);
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
|
||||
for (const track of stream.getTracks()) {
|
||||
const settings = track.getSettings();
|
||||
|
||||
if (track.kind === "audio") {
|
||||
this.audioInput = settings.deviceId;
|
||||
} else if (track.kind === "video") {
|
||||
this.videoInput = settings.deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
if (reusable) {
|
||||
this.localUserMediaStream = stream;
|
||||
}
|
||||
} else {
|
||||
stream = this.localUserMediaStream.clone();
|
||||
|
||||
if (!shouldRequestAudio) {
|
||||
for (const track of stream.getAudioTracks()) {
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldRequestVideo) {
|
||||
for (const track of stream.getVideoTracks()) {
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reusable) {
|
||||
@@ -103,6 +168,10 @@ export class MediaHandler {
|
||||
logger.debug("Splicing usermedia stream out stream array", mediaStream.id);
|
||||
this.userMediaStreams.splice(index, 1);
|
||||
}
|
||||
|
||||
if (this.localUserMediaStream === mediaStream) {
|
||||
this.localUserMediaStream = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,6 +243,7 @@ export class MediaHandler {
|
||||
|
||||
this.userMediaStreams = [];
|
||||
this.screensharingStreams = [];
|
||||
this.localUserMediaStream = undefined;
|
||||
}
|
||||
|
||||
private getUserMediaContraints(audio: boolean, video: boolean): MediaStreamConstraints {
|
||||
|
||||
Reference in New Issue
Block a user