You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-06 12:02:40 +03:00
Merge remote-tracking branch 'origin/develop' into travis/event-fixes
This commit is contained in:
@@ -55,6 +55,7 @@
|
|||||||
"bs58": "^4.0.1",
|
"bs58": "^4.0.1",
|
||||||
"content-type": "^1.0.4",
|
"content-type": "^1.0.4",
|
||||||
"loglevel": "^1.7.1",
|
"loglevel": "^1.7.1",
|
||||||
|
"p-retry": "^4.5.0",
|
||||||
"qs": "^6.9.6",
|
"qs": "^6.9.6",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"unhomoglyph": "^1.0.6"
|
"unhomoglyph": "^1.0.6"
|
||||||
|
@@ -1012,7 +1012,7 @@ describe("megolm", function() {
|
|||||||
},
|
},
|
||||||
event: true,
|
event: true,
|
||||||
});
|
});
|
||||||
event._senderCurve25519Key = testSenderKey;
|
event.senderCurve25519Key = testSenderKey;
|
||||||
return testClient.client.crypto._onRoomKeyEvent(event);
|
return testClient.client.crypto._onRoomKeyEvent(event);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const event = testUtils.mkEvent({
|
const event = testUtils.mkEvent({
|
||||||
|
@@ -234,7 +234,7 @@ describe("Crypto", function() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
// make onRoomKeyEvent think this was an encrypted event
|
// make onRoomKeyEvent think this was an encrypted event
|
||||||
ksEvent._senderCurve25519Key = "akey";
|
ksEvent.senderCurve25519Key = "akey";
|
||||||
return ksEvent;
|
return ksEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,9 +274,9 @@ describe("Crypto", function() {
|
|||||||
// alice encrypts each event, and then bob tries to decrypt
|
// alice encrypts each event, and then bob tries to decrypt
|
||||||
// them without any keys, so that they'll be in pending
|
// them without any keys, so that they'll be in pending
|
||||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||||
event._clearEvent = {};
|
event.clearEvent = {};
|
||||||
event._senderCurve25519Key = null;
|
event.senderCurve25519Key = null;
|
||||||
event._claimedEd25519Key = null;
|
event.claimedEd25519Key = null;
|
||||||
try {
|
try {
|
||||||
await bobClient.crypto.decryptEvent(event);
|
await bobClient.crypto.decryptEvent(event);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@@ -25,6 +25,7 @@ import {
|
|||||||
} from "../../../src/models/MSC3089TreeSpace";
|
} from "../../../src/models/MSC3089TreeSpace";
|
||||||
import { DEFAULT_ALPHABET } from "../../../src/utils";
|
import { DEFAULT_ALPHABET } from "../../../src/utils";
|
||||||
import { MockBlob } from "../../MockBlob";
|
import { MockBlob } from "../../MockBlob";
|
||||||
|
import { MatrixError } from "../../../src/http-api";
|
||||||
|
|
||||||
describe("MSC3089TreeSpace", () => {
|
describe("MSC3089TreeSpace", () => {
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
@@ -93,7 +94,7 @@ describe("MSC3089TreeSpace", () => {
|
|||||||
});
|
});
|
||||||
client.sendStateEvent = fn;
|
client.sendStateEvent = fn;
|
||||||
await tree.setName(newName);
|
await tree.setName(newName);
|
||||||
expect(fn.mock.calls.length).toBe(1);
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support inviting users to the space', async () => {
|
it('should support inviting users to the space', async () => {
|
||||||
@@ -104,8 +105,62 @@ describe("MSC3089TreeSpace", () => {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
client.invite = fn;
|
client.invite = fn;
|
||||||
await tree.invite(target);
|
await tree.invite(target, false);
|
||||||
expect(fn.mock.calls.length).toBe(1);
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retry invites to the space', async () => {
|
||||||
|
const target = targetUser;
|
||||||
|
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||||
|
expect(inviteRoomId).toEqual(roomId);
|
||||||
|
expect(userId).toEqual(target);
|
||||||
|
if (fn.mock.calls.length === 1) return Promise.reject(new Error("Sample Failure"));
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
client.invite = fn;
|
||||||
|
await tree.invite(target, false);
|
||||||
|
expect(fn).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not retry invite permission errors', async () => {
|
||||||
|
const target = targetUser;
|
||||||
|
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||||
|
expect(inviteRoomId).toEqual(roomId);
|
||||||
|
expect(userId).toEqual(target);
|
||||||
|
return Promise.reject(new MatrixError({ errcode: "M_FORBIDDEN", error: "Sample Failure" }));
|
||||||
|
});
|
||||||
|
client.invite = fn;
|
||||||
|
try {
|
||||||
|
await tree.invite(target, false);
|
||||||
|
|
||||||
|
// noinspection ExceptionCaughtLocallyJS
|
||||||
|
throw new Error("Failed to fail");
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.errcode).toEqual("M_FORBIDDEN");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invite to subspaces', async () => {
|
||||||
|
const target = targetUser;
|
||||||
|
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||||
|
expect(inviteRoomId).toEqual(roomId);
|
||||||
|
expect(userId).toEqual(target);
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
client.invite = fn;
|
||||||
|
tree.getDirectories = () => [
|
||||||
|
// Bare minimum overrides. We proxy to our mock function manually so we can
|
||||||
|
// count the calls, not to ensure accuracy. The invite function behaving correctly
|
||||||
|
// is covered by another test.
|
||||||
|
{ invite: (userId) => fn(tree.roomId, userId) } as MSC3089TreeSpace,
|
||||||
|
{ invite: (userId) => fn(tree.roomId, userId) } as MSC3089TreeSpace,
|
||||||
|
{ invite: (userId) => fn(tree.roomId, userId) } as MSC3089TreeSpace,
|
||||||
|
];
|
||||||
|
|
||||||
|
await tree.invite(target, true);
|
||||||
|
expect(fn).toHaveBeenCalledTimes(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function evaluatePowerLevels(pls: any, role: TreePermissions, expectedPl: number) {
|
async function evaluatePowerLevels(pls: any, role: TreePermissions, expectedPl: number) {
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import * as utils from "../test-utils";
|
import * as utils from "../test-utils";
|
||||||
import { RoomState } from "../../src/models/room-state";
|
import { RoomState } from "../../src/models/room-state";
|
||||||
import { RoomMember } from "../../src/models/room-member";
|
|
||||||
|
|
||||||
describe("RoomState", function() {
|
describe("RoomState", function() {
|
||||||
const roomId = "!foo:bar";
|
const roomId = "!foo:bar";
|
||||||
@@ -193,12 +192,7 @@ describe("RoomState", function() {
|
|||||||
expect(emitCount).toEqual(2);
|
expect(emitCount).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call setPowerLevelEvent on each RoomMember for m.room.power_levels",
|
it("should call setPowerLevelEvent on each RoomMember for m.room.power_levels", function() {
|
||||||
function() {
|
|
||||||
// mock up the room members
|
|
||||||
state.members[userA] = utils.mock(RoomMember);
|
|
||||||
state.members[userB] = utils.mock(RoomMember);
|
|
||||||
|
|
||||||
const powerLevelEvent = utils.mkEvent({
|
const powerLevelEvent = utils.mkEvent({
|
||||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||||
content: {
|
content: {
|
||||||
@@ -208,18 +202,16 @@ describe("RoomState", function() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// spy on the room members
|
||||||
|
jest.spyOn(state.members[userA], "setPowerLevelEvent");
|
||||||
|
jest.spyOn(state.members[userB], "setPowerLevelEvent");
|
||||||
state.setStateEvents([powerLevelEvent]);
|
state.setStateEvents([powerLevelEvent]);
|
||||||
|
|
||||||
expect(state.members[userA].setPowerLevelEvent).toHaveBeenCalledWith(
|
expect(state.members[userA].setPowerLevelEvent).toHaveBeenCalledWith(powerLevelEvent);
|
||||||
powerLevelEvent,
|
expect(state.members[userB].setPowerLevelEvent).toHaveBeenCalledWith(powerLevelEvent);
|
||||||
);
|
|
||||||
expect(state.members[userB].setPowerLevelEvent).toHaveBeenCalledWith(
|
|
||||||
powerLevelEvent,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call setPowerLevelEvent on a new RoomMember if power levels exist",
|
it("should call setPowerLevelEvent on a new RoomMember if power levels exist", function() {
|
||||||
function() {
|
|
||||||
const memberEvent = utils.mkMembership({
|
const memberEvent = utils.mkMembership({
|
||||||
mship: "join", user: userC, room: roomId, event: true,
|
mship: "join", user: userC, room: roomId, event: true,
|
||||||
});
|
});
|
||||||
@@ -243,13 +235,12 @@ describe("RoomState", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call setMembershipEvent on the right RoomMember", function() {
|
it("should call setMembershipEvent on the right RoomMember", function() {
|
||||||
// mock up the room members
|
|
||||||
state.members[userA] = utils.mock(RoomMember);
|
|
||||||
state.members[userB] = utils.mock(RoomMember);
|
|
||||||
|
|
||||||
const memberEvent = utils.mkMembership({
|
const memberEvent = utils.mkMembership({
|
||||||
user: userB, mship: "leave", room: roomId, event: true,
|
user: userB, mship: "leave", room: roomId, event: true,
|
||||||
});
|
});
|
||||||
|
// spy on the room members
|
||||||
|
jest.spyOn(state.members[userA], "setMembershipEvent");
|
||||||
|
jest.spyOn(state.members[userB], "setMembershipEvent");
|
||||||
state.setStateEvents([memberEvent]);
|
state.setStateEvents([memberEvent]);
|
||||||
|
|
||||||
expect(state.members[userA].setMembershipEvent).not.toHaveBeenCalled();
|
expect(state.members[userA].setMembershipEvent).not.toHaveBeenCalled();
|
||||||
@@ -374,17 +365,13 @@ describe("RoomState", function() {
|
|||||||
user_ids: [userA],
|
user_ids: [userA],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// mock up the room members
|
// spy on the room members
|
||||||
state.members[userA] = utils.mock(RoomMember);
|
jest.spyOn(state.members[userA], "setTypingEvent");
|
||||||
state.members[userB] = utils.mock(RoomMember);
|
jest.spyOn(state.members[userB], "setTypingEvent");
|
||||||
state.setTypingEvent(typingEvent);
|
state.setTypingEvent(typingEvent);
|
||||||
|
|
||||||
expect(state.members[userA].setTypingEvent).toHaveBeenCalledWith(
|
expect(state.members[userA].setTypingEvent).toHaveBeenCalledWith(typingEvent);
|
||||||
typingEvent,
|
expect(state.members[userB].setTypingEvent).toHaveBeenCalledWith(typingEvent);
|
||||||
);
|
|
||||||
expect(state.members[userB].setTypingEvent).toHaveBeenCalledWith(
|
|
||||||
typingEvent,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ import {
|
|||||||
lexicographicCompare,
|
lexicographicCompare,
|
||||||
nextString,
|
nextString,
|
||||||
prevString,
|
prevString,
|
||||||
|
simpleRetryOperation,
|
||||||
stringToBase,
|
stringToBase,
|
||||||
} from "../../src/utils";
|
} from "../../src/utils";
|
||||||
import { logger } from "../../src/logger";
|
import { logger } from "../../src/logger";
|
||||||
@@ -268,6 +269,34 @@ describe("utils", function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('simpleRetryOperation', () => {
|
||||||
|
it('should retry', async () => {
|
||||||
|
let count = 0;
|
||||||
|
const val = {};
|
||||||
|
const fn = (attempt) => {
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// If this expectation fails then it can appear as a Jest Timeout due to
|
||||||
|
// the retry running beyond the test limit.
|
||||||
|
expect(attempt).toEqual(count);
|
||||||
|
|
||||||
|
if (count > 1) {
|
||||||
|
return Promise.resolve(val);
|
||||||
|
} else {
|
||||||
|
return Promise.reject(new Error("Iterative failure"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ret = await simpleRetryOperation(fn);
|
||||||
|
expect(ret).toBe(val);
|
||||||
|
expect(count).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// We don't test much else of the function because then we're just testing that the
|
||||||
|
// underlying library behaves, which should be tested on its own. Our API surface is
|
||||||
|
// all that concerns us.
|
||||||
|
});
|
||||||
|
|
||||||
describe('DEFAULT_ALPHABET', () => {
|
describe('DEFAULT_ALPHABET', () => {
|
||||||
it('should be usefully printable ASCII in order', () => {
|
it('should be usefully printable ASCII in order', () => {
|
||||||
expect(DEFAULT_ALPHABET).toEqual(
|
expect(DEFAULT_ALPHABET).toEqual(
|
||||||
|
@@ -87,6 +87,11 @@ export enum EventType {
|
|||||||
Dummy = "m.dummy",
|
Dummy = "m.dummy",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum RelationType {
|
||||||
|
Annotation = "m.annotation",
|
||||||
|
Replace = "m.replace",
|
||||||
|
}
|
||||||
|
|
||||||
export enum MsgType {
|
export enum MsgType {
|
||||||
Text = "m.text",
|
Text = "m.text",
|
||||||
Emote = "m.emote",
|
Emote = "m.emote",
|
||||||
|
@@ -21,7 +21,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { SyncApi } from "./sync";
|
import { SyncApi } from "./sync";
|
||||||
import { EventStatus, MatrixEvent } from "./models/event";
|
import { EventStatus, IDecryptOptions, MatrixEvent } from "./models/event";
|
||||||
import { StubStore } from "./store/stub";
|
import { StubStore } from "./store/stub";
|
||||||
import { createNewMatrixCall, MatrixCall } from "./webrtc/call";
|
import { createNewMatrixCall, MatrixCall } from "./webrtc/call";
|
||||||
import { Filter } from "./filter";
|
import { Filter } from "./filter";
|
||||||
@@ -2730,7 +2730,7 @@ export class MatrixClient extends EventEmitter {
|
|||||||
* @return {Promise} Resolves: TODO
|
* @return {Promise} Resolves: TODO
|
||||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||||
*/
|
*/
|
||||||
public setAccountData(eventType: string, content: any, callback?: Callback): Promise<void> {
|
public setAccountData(eventType: EventType | string, content: any, callback?: Callback): Promise<void> {
|
||||||
const path = utils.encodeUri("/user/$userId/account_data/$type", {
|
const path = utils.encodeUri("/user/$userId/account_data/$type", {
|
||||||
$userId: this.credentials.userId,
|
$userId: this.credentials.userId,
|
||||||
$type: eventType,
|
$type: eventType,
|
||||||
@@ -2749,7 +2749,7 @@ export class MatrixClient extends EventEmitter {
|
|||||||
* @param {string} eventType The event type
|
* @param {string} eventType The event type
|
||||||
* @return {?object} The contents of the given account data event
|
* @return {?object} The contents of the given account data event
|
||||||
*/
|
*/
|
||||||
public getAccountData(eventType: string): any {
|
public getAccountData(eventType: string): MatrixEvent {
|
||||||
return this.store.getAccountData(eventType);
|
return this.store.getAccountData(eventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3042,7 +3042,7 @@ export class MatrixClient extends EventEmitter {
|
|||||||
if (event && event.getType() === "m.room.power_levels") {
|
if (event && event.getType() === "m.room.power_levels") {
|
||||||
// take a copy of the content to ensure we don't corrupt
|
// take a copy of the content to ensure we don't corrupt
|
||||||
// existing client state with a failed power level change
|
// existing client state with a failed power level change
|
||||||
content = utils.deepCopy(event.getContent());
|
content = utils.deepCopy(event.getContent()) as typeof content;
|
||||||
}
|
}
|
||||||
content.users[userId] = powerLevel;
|
content.users[userId] = powerLevel;
|
||||||
const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", {
|
const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", {
|
||||||
@@ -5707,13 +5707,13 @@ export class MatrixClient extends EventEmitter {
|
|||||||
* @param {boolean} options.isRetry True if this is a retry (enables more logging)
|
* @param {boolean} options.isRetry True if this is a retry (enables more logging)
|
||||||
* @param {boolean} options.emit Emits "event.decrypted" if set to true
|
* @param {boolean} options.emit Emits "event.decrypted" if set to true
|
||||||
*/
|
*/
|
||||||
public decryptEventIfNeeded(event: MatrixEvent, options?: { emit: boolean, isRetry: boolean }): Promise<void> {
|
public decryptEventIfNeeded(event: MatrixEvent, options?: IDecryptOptions): Promise<void> {
|
||||||
if (event.shouldAttemptDecryption()) {
|
if (event.shouldAttemptDecryption()) {
|
||||||
event.attemptDecryption(this.crypto, options);
|
event.attemptDecryption(this.crypto, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.isBeingDecrypted()) {
|
if (event.isBeingDecrypted()) {
|
||||||
return event._decryptionPromise;
|
return event.getDecryptionPromise();
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018 New Vector Ltd
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -23,7 +23,7 @@ limitations under the License.
|
|||||||
* @param {string} htmlBody the HTML representation of the message
|
* @param {string} htmlBody the HTML representation of the message
|
||||||
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
||||||
*/
|
*/
|
||||||
export function makeHtmlMessage(body, htmlBody) {
|
export function makeHtmlMessage(body: string, htmlBody: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
@@ -38,7 +38,7 @@ export function makeHtmlMessage(body, htmlBody) {
|
|||||||
* @param {string} htmlBody the HTML representation of the notice
|
* @param {string} htmlBody the HTML representation of the notice
|
||||||
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
||||||
*/
|
*/
|
||||||
export function makeHtmlNotice(body, htmlBody) {
|
export function makeHtmlNotice(body: string, htmlBody: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
@@ -53,7 +53,7 @@ export function makeHtmlNotice(body, htmlBody) {
|
|||||||
* @param {string} htmlBody the HTML representation of the emote
|
* @param {string} htmlBody the HTML representation of the emote
|
||||||
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
||||||
*/
|
*/
|
||||||
export function makeHtmlEmote(body, htmlBody) {
|
export function makeHtmlEmote(body: string, htmlBody: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.emote",
|
msgtype: "m.emote",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
@@ -67,7 +67,7 @@ export function makeHtmlEmote(body, htmlBody) {
|
|||||||
* @param {string} body the plaintext body of the emote
|
* @param {string} body the plaintext body of the emote
|
||||||
* @returns {{msgtype: string, body: string}}
|
* @returns {{msgtype: string, body: string}}
|
||||||
*/
|
*/
|
||||||
export function makeTextMessage(body) {
|
export function makeTextMessage(body: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
body: body,
|
body: body,
|
||||||
@@ -79,7 +79,7 @@ export function makeTextMessage(body) {
|
|||||||
* @param {string} body the plaintext body of the notice
|
* @param {string} body the plaintext body of the notice
|
||||||
* @returns {{msgtype: string, body: string}}
|
* @returns {{msgtype: string, body: string}}
|
||||||
*/
|
*/
|
||||||
export function makeNotice(body) {
|
export function makeNotice(body: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
body: body,
|
body: body,
|
||||||
@@ -91,7 +91,7 @@ export function makeNotice(body) {
|
|||||||
* @param {string} body the plaintext body of the emote
|
* @param {string} body the plaintext body of the emote
|
||||||
* @returns {{msgtype: string, body: string}}
|
* @returns {{msgtype: string, body: string}}
|
||||||
*/
|
*/
|
||||||
export function makeEmoteMessage(body) {
|
export function makeEmoteMessage(body: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.emote",
|
msgtype: "m.emote",
|
||||||
body: body,
|
body: body,
|
@@ -1,6 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -34,8 +33,14 @@ import * as utils from "./utils";
|
|||||||
* for such URLs.
|
* for such URLs.
|
||||||
* @return {string} The complete URL to the content.
|
* @return {string} The complete URL to the content.
|
||||||
*/
|
*/
|
||||||
export function getHttpUriForMxc(baseUrl, mxc, width, height,
|
export function getHttpUriForMxc(
|
||||||
resizeMethod, allowDirectLinks) {
|
baseUrl: string,
|
||||||
|
mxc: string,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
resizeMethod: string,
|
||||||
|
allowDirectLinks: boolean,
|
||||||
|
): string {
|
||||||
if (typeof mxc !== "string" || !mxc) {
|
if (typeof mxc !== "string" || !mxc) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -51,13 +56,13 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height,
|
|||||||
const params = {};
|
const params = {};
|
||||||
|
|
||||||
if (width) {
|
if (width) {
|
||||||
params.width = Math.round(width);
|
params["width"] = Math.round(width);
|
||||||
}
|
}
|
||||||
if (height) {
|
if (height) {
|
||||||
params.height = Math.round(height);
|
params["height"] = Math.round(height);
|
||||||
}
|
}
|
||||||
if (resizeMethod) {
|
if (resizeMethod) {
|
||||||
params.method = resizeMethod;
|
params["method"] = resizeMethod;
|
||||||
}
|
}
|
||||||
if (Object.keys(params).length > 0) {
|
if (Object.keys(params).length > 0) {
|
||||||
// these are thumbnailing params so they probably want the
|
// these are thumbnailing params so they probably want the
|
||||||
@@ -71,7 +76,7 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height,
|
|||||||
fragment = serverAndMediaId.substr(fragmentOffset);
|
fragment = serverAndMediaId.substr(fragmentOffset);
|
||||||
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
|
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
|
||||||
}
|
}
|
||||||
return baseUrl + prefix + serverAndMediaId +
|
|
||||||
(Object.keys(params).length === 0 ? "" :
|
const urlParams = (Object.keys(params).length === 0 ? "" : ("?" + utils.encodeParams(params)));
|
||||||
("?" + utils.encodeParams(params))) + fragment;
|
return baseUrl + prefix + serverAndMediaId + urlParams + fragment;
|
||||||
}
|
}
|
@@ -1,145 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module filter-component
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a value matches a given field value, which may be a * terminated
|
|
||||||
* wildcard pattern.
|
|
||||||
* @param {String} actual_value The value to be compared
|
|
||||||
* @param {String} filter_value The filter pattern to be compared
|
|
||||||
* @return {bool} true if the actual_value matches the filter_value
|
|
||||||
*/
|
|
||||||
function _matches_wildcard(actual_value, filter_value) {
|
|
||||||
if (filter_value.endsWith("*")) {
|
|
||||||
const type_prefix = filter_value.slice(0, -1);
|
|
||||||
return actual_value.substr(0, type_prefix.length) === type_prefix;
|
|
||||||
} else {
|
|
||||||
return actual_value === filter_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* FilterComponent is a section of a Filter definition which defines the
|
|
||||||
* types, rooms, senders filters etc to be applied to a particular type of resource.
|
|
||||||
* This is all ported over from synapse's Filter object.
|
|
||||||
*
|
|
||||||
* N.B. that synapse refers to these as 'Filters', and what js-sdk refers to as
|
|
||||||
* 'Filters' are referred to as 'FilterCollections'.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
* @param {Object} filter_json the definition of this filter JSON, e.g. { 'contains_url': true }
|
|
||||||
*/
|
|
||||||
export function FilterComponent(filter_json) {
|
|
||||||
this.filter_json = filter_json;
|
|
||||||
|
|
||||||
this.types = filter_json.types || null;
|
|
||||||
this.not_types = filter_json.not_types || [];
|
|
||||||
|
|
||||||
this.rooms = filter_json.rooms || null;
|
|
||||||
this.not_rooms = filter_json.not_rooms || [];
|
|
||||||
|
|
||||||
this.senders = filter_json.senders || null;
|
|
||||||
this.not_senders = filter_json.not_senders || [];
|
|
||||||
|
|
||||||
this.contains_url = filter_json.contains_url || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks with the filter component matches the given event
|
|
||||||
* @param {MatrixEvent} event event to be checked against the filter
|
|
||||||
* @return {bool} true if the event matches the filter
|
|
||||||
*/
|
|
||||||
FilterComponent.prototype.check = function(event) {
|
|
||||||
return this._checkFields(
|
|
||||||
event.getRoomId(),
|
|
||||||
event.getSender(),
|
|
||||||
event.getType(),
|
|
||||||
event.getContent() ? event.getContent().url !== undefined : false,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the filter component matches the given event fields.
|
|
||||||
* @param {String} room_id the room_id for the event being checked
|
|
||||||
* @param {String} sender the sender of the event being checked
|
|
||||||
* @param {String} event_type the type of the event being checked
|
|
||||||
* @param {String} contains_url whether the event contains a content.url field
|
|
||||||
* @return {bool} true if the event fields match the filter
|
|
||||||
*/
|
|
||||||
FilterComponent.prototype._checkFields =
|
|
||||||
function(room_id, sender, event_type, contains_url) {
|
|
||||||
const literal_keys = {
|
|
||||||
"rooms": function(v) {
|
|
||||||
return room_id === v;
|
|
||||||
},
|
|
||||||
"senders": function(v) {
|
|
||||||
return sender === v;
|
|
||||||
},
|
|
||||||
"types": function(v) {
|
|
||||||
return _matches_wildcard(event_type, v);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const self = this;
|
|
||||||
for (let n=0; n < Object.keys(literal_keys).length; n++) {
|
|
||||||
const name = Object.keys(literal_keys)[n];
|
|
||||||
const match_func = literal_keys[name];
|
|
||||||
const not_name = "not_" + name;
|
|
||||||
const disallowed_values = self[not_name];
|
|
||||||
if (disallowed_values.filter(match_func).length > 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowed_values = self[name];
|
|
||||||
if (allowed_values && allowed_values.length > 0) {
|
|
||||||
const anyMatch = allowed_values.some(match_func);
|
|
||||||
if (!anyMatch) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const contains_url_filter = this.filter_json.contains_url;
|
|
||||||
if (contains_url_filter !== undefined) {
|
|
||||||
if (contains_url_filter !== contains_url) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters a list of events down to those which match this filter component
|
|
||||||
* @param {MatrixEvent[]} events Events to be checked againt the filter component
|
|
||||||
* @return {MatrixEvent[]} events which matched the filter component
|
|
||||||
*/
|
|
||||||
FilterComponent.prototype.filter = function(events) {
|
|
||||||
return events.filter(this.check, this);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the limit field for a given filter component, providing a default of
|
|
||||||
* 10 if none is otherwise specified. Cargo-culted from Synapse.
|
|
||||||
* @return {Number} the limit for this filter component.
|
|
||||||
*/
|
|
||||||
FilterComponent.prototype.limit = function() {
|
|
||||||
return this.filter_json.limit !== undefined ? this.filter_json.limit : 10;
|
|
||||||
};
|
|
141
src/filter-component.ts
Normal file
141
src/filter-component.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MatrixEvent } from "./models/event";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module filter-component
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a value matches a given field value, which may be a * terminated
|
||||||
|
* wildcard pattern.
|
||||||
|
* @param {String} actualValue The value to be compared
|
||||||
|
* @param {String} filterValue The filter pattern to be compared
|
||||||
|
* @return {bool} true if the actualValue matches the filterValue
|
||||||
|
*/
|
||||||
|
function matchesWildcard(actualValue: string, filterValue: string): boolean {
|
||||||
|
if (filterValue.endsWith("*")) {
|
||||||
|
const typePrefix = filterValue.slice(0, -1);
|
||||||
|
return actualValue.substr(0, typePrefix.length) === typePrefix;
|
||||||
|
} else {
|
||||||
|
return actualValue === filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
export interface IFilterComponent {
|
||||||
|
types?: string[];
|
||||||
|
not_types?: string[];
|
||||||
|
rooms?: string[];
|
||||||
|
not_rooms?: string[];
|
||||||
|
senders?: string[];
|
||||||
|
not_senders?: string[];
|
||||||
|
contains_url?: boolean;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FilterComponent is a section of a Filter definition which defines the
|
||||||
|
* types, rooms, senders filters etc to be applied to a particular type of resource.
|
||||||
|
* This is all ported over from synapse's Filter object.
|
||||||
|
*
|
||||||
|
* N.B. that synapse refers to these as 'Filters', and what js-sdk refers to as
|
||||||
|
* 'Filters' are referred to as 'FilterCollections'.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true }
|
||||||
|
*/
|
||||||
|
export class FilterComponent {
|
||||||
|
constructor(private filterJson: IFilterComponent) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks with the filter component matches the given event
|
||||||
|
* @param {MatrixEvent} event event to be checked against the filter
|
||||||
|
* @return {bool} true if the event matches the filter
|
||||||
|
*/
|
||||||
|
check(event: MatrixEvent): boolean {
|
||||||
|
return this.checkFields(
|
||||||
|
event.getRoomId(),
|
||||||
|
event.getSender(),
|
||||||
|
event.getType(),
|
||||||
|
event.getContent() ? event.getContent().url !== undefined : false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the filter component matches the given event fields.
|
||||||
|
* @param {String} roomId the roomId for the event being checked
|
||||||
|
* @param {String} sender the sender of the event being checked
|
||||||
|
* @param {String} eventType the type of the event being checked
|
||||||
|
* @param {boolean} containsUrl whether the event contains a content.url field
|
||||||
|
* @return {boolean} true if the event fields match the filter
|
||||||
|
*/
|
||||||
|
private checkFields(roomId: string, sender: string, eventType: string, containsUrl: boolean): boolean {
|
||||||
|
const literalKeys = {
|
||||||
|
"rooms": function(v: string): boolean {
|
||||||
|
return roomId === v;
|
||||||
|
},
|
||||||
|
"senders": function(v: string): boolean {
|
||||||
|
return sender === v;
|
||||||
|
},
|
||||||
|
"types": function(v: string): boolean {
|
||||||
|
return matchesWildcard(eventType, v);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let n = 0; n < Object.keys(literalKeys).length; n++) {
|
||||||
|
const name = Object.keys(literalKeys)[n];
|
||||||
|
const matchFunc = literalKeys[name];
|
||||||
|
const notName = "not_" + name;
|
||||||
|
const disallowedValues: string[] = this.filterJson[notName];
|
||||||
|
if (disallowedValues?.some(matchFunc)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedValues: string[] = this.filterJson[name];
|
||||||
|
if (allowedValues && !allowedValues.some(matchFunc)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const containsUrlFilter = this.filterJson.contains_url;
|
||||||
|
if (containsUrlFilter !== undefined && containsUrlFilter !== containsUrl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters a list of events down to those which match this filter component
|
||||||
|
* @param {MatrixEvent[]} events Events to be checked againt the filter component
|
||||||
|
* @return {MatrixEvent[]} events which matched the filter component
|
||||||
|
*/
|
||||||
|
filter(events: MatrixEvent[]): MatrixEvent[] {
|
||||||
|
return events.filter(this.check, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the limit field for a given filter component, providing a default of
|
||||||
|
* 10 if none is otherwise specified. Cargo-culted from Synapse.
|
||||||
|
* @return {Number} the limit for this filter component.
|
||||||
|
*/
|
||||||
|
limit(): number {
|
||||||
|
return this.filterJson.limit !== undefined ? this.filterJson.limit : 10;
|
||||||
|
}
|
||||||
|
}
|
199
src/filter.js
199
src/filter.js
@@ -1,199 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module filter
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { FilterComponent } from "./filter-component";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {string} keyNesting
|
|
||||||
* @param {*} val
|
|
||||||
*/
|
|
||||||
function setProp(obj, keyNesting, val) {
|
|
||||||
const nestedKeys = keyNesting.split(".");
|
|
||||||
let currentObj = obj;
|
|
||||||
for (let i = 0; i < (nestedKeys.length - 1); i++) {
|
|
||||||
if (!currentObj[nestedKeys[i]]) {
|
|
||||||
currentObj[nestedKeys[i]] = {};
|
|
||||||
}
|
|
||||||
currentObj = currentObj[nestedKeys[i]];
|
|
||||||
}
|
|
||||||
currentObj[nestedKeys[nestedKeys.length - 1]] = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct a new Filter.
|
|
||||||
* @constructor
|
|
||||||
* @param {string} userId The user ID for this filter.
|
|
||||||
* @param {string=} filterId The filter ID if known.
|
|
||||||
* @prop {string} userId The user ID of the filter
|
|
||||||
* @prop {?string} filterId The filter ID
|
|
||||||
*/
|
|
||||||
export function Filter(userId, filterId) {
|
|
||||||
this.userId = userId;
|
|
||||||
this.filterId = filterId;
|
|
||||||
this.definition = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
Filter.LAZY_LOADING_MESSAGES_FILTER = {
|
|
||||||
lazy_load_members: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the ID of this filter on your homeserver (if known)
|
|
||||||
* @return {?Number} The filter ID
|
|
||||||
*/
|
|
||||||
Filter.prototype.getFilterId = function() {
|
|
||||||
return this.filterId;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the JSON body of the filter.
|
|
||||||
* @return {Object} The filter definition
|
|
||||||
*/
|
|
||||||
Filter.prototype.getDefinition = function() {
|
|
||||||
return this.definition;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the JSON body of the filter
|
|
||||||
* @param {Object} definition The filter definition
|
|
||||||
*/
|
|
||||||
Filter.prototype.setDefinition = function(definition) {
|
|
||||||
this.definition = definition;
|
|
||||||
|
|
||||||
// This is all ported from synapse's FilterCollection()
|
|
||||||
|
|
||||||
// definitions look something like:
|
|
||||||
// {
|
|
||||||
// "room": {
|
|
||||||
// "rooms": ["!abcde:example.com"],
|
|
||||||
// "not_rooms": ["!123456:example.com"],
|
|
||||||
// "state": {
|
|
||||||
// "types": ["m.room.*"],
|
|
||||||
// "not_rooms": ["!726s6s6q:example.com"],
|
|
||||||
// "lazy_load_members": true,
|
|
||||||
// },
|
|
||||||
// "timeline": {
|
|
||||||
// "limit": 10,
|
|
||||||
// "types": ["m.room.message"],
|
|
||||||
// "not_rooms": ["!726s6s6q:example.com"],
|
|
||||||
// "not_senders": ["@spam:example.com"]
|
|
||||||
// "contains_url": true
|
|
||||||
// },
|
|
||||||
// "ephemeral": {
|
|
||||||
// "types": ["m.receipt", "m.typing"],
|
|
||||||
// "not_rooms": ["!726s6s6q:example.com"],
|
|
||||||
// "not_senders": ["@spam:example.com"]
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// "presence": {
|
|
||||||
// "types": ["m.presence"],
|
|
||||||
// "not_senders": ["@alice:example.com"]
|
|
||||||
// },
|
|
||||||
// "event_format": "client",
|
|
||||||
// "event_fields": ["type", "content", "sender"]
|
|
||||||
// }
|
|
||||||
|
|
||||||
const room_filter_json = definition.room;
|
|
||||||
|
|
||||||
// consider the top level rooms/not_rooms filter
|
|
||||||
const room_filter_fields = {};
|
|
||||||
if (room_filter_json) {
|
|
||||||
if (room_filter_json.rooms) {
|
|
||||||
room_filter_fields.rooms = room_filter_json.rooms;
|
|
||||||
}
|
|
||||||
if (room_filter_json.rooms) {
|
|
||||||
room_filter_fields.not_rooms = room_filter_json.not_rooms;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._include_leave = room_filter_json.include_leave || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._room_filter = new FilterComponent(room_filter_fields);
|
|
||||||
this._room_timeline_filter = new FilterComponent(
|
|
||||||
room_filter_json ? (room_filter_json.timeline || {}) : {},
|
|
||||||
);
|
|
||||||
|
|
||||||
// don't bother porting this from synapse yet:
|
|
||||||
// this._room_state_filter =
|
|
||||||
// new FilterComponent(room_filter_json.state || {});
|
|
||||||
// this._room_ephemeral_filter =
|
|
||||||
// new FilterComponent(room_filter_json.ephemeral || {});
|
|
||||||
// this._room_account_data_filter =
|
|
||||||
// new FilterComponent(room_filter_json.account_data || {});
|
|
||||||
// this._presence_filter =
|
|
||||||
// new FilterComponent(definition.presence || {});
|
|
||||||
// this._account_data_filter =
|
|
||||||
// new FilterComponent(definition.account_data || {});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the room.timeline filter component of the filter
|
|
||||||
* @return {FilterComponent} room timeline filter component
|
|
||||||
*/
|
|
||||||
Filter.prototype.getRoomTimelineFilterComponent = function() {
|
|
||||||
return this._room_timeline_filter;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter the list of events based on whether they are allowed in a timeline
|
|
||||||
* based on this filter
|
|
||||||
* @param {MatrixEvent[]} events the list of events being filtered
|
|
||||||
* @return {MatrixEvent[]} the list of events which match the filter
|
|
||||||
*/
|
|
||||||
Filter.prototype.filterRoomTimeline = function(events) {
|
|
||||||
return this._room_timeline_filter.filter(this._room_filter.filter(events));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the max number of events to return for each room's timeline.
|
|
||||||
* @param {Number} limit The max number of events to return for each room.
|
|
||||||
*/
|
|
||||||
Filter.prototype.setTimelineLimit = function(limit) {
|
|
||||||
setProp(this.definition, "room.timeline.limit", limit);
|
|
||||||
};
|
|
||||||
|
|
||||||
Filter.prototype.setLazyLoadMembers = function(enabled) {
|
|
||||||
setProp(this.definition, "room.state.lazy_load_members", !!enabled);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Control whether left rooms should be included in responses.
|
|
||||||
* @param {boolean} includeLeave True to make rooms the user has left appear
|
|
||||||
* in responses.
|
|
||||||
*/
|
|
||||||
Filter.prototype.setIncludeLeaveRooms = function(includeLeave) {
|
|
||||||
setProp(this.definition, "room.include_leave", includeLeave);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a filter from existing data.
|
|
||||||
* @static
|
|
||||||
* @param {string} userId
|
|
||||||
* @param {string} filterId
|
|
||||||
* @param {Object} jsonObj
|
|
||||||
* @return {Filter}
|
|
||||||
*/
|
|
||||||
Filter.fromJson = function(userId, filterId, jsonObj) {
|
|
||||||
const filter = new Filter(userId, filterId);
|
|
||||||
filter.setDefinition(jsonObj);
|
|
||||||
return filter;
|
|
||||||
};
|
|
226
src/filter.ts
Normal file
226
src/filter.ts
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 - 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module filter
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FilterComponent, IFilterComponent } from "./filter-component";
|
||||||
|
import { MatrixEvent } from "./models/event";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {string} keyNesting
|
||||||
|
* @param {*} val
|
||||||
|
*/
|
||||||
|
function setProp(obj: object, keyNesting: string, val: any) {
|
||||||
|
const nestedKeys = keyNesting.split(".");
|
||||||
|
let currentObj = obj;
|
||||||
|
for (let i = 0; i < (nestedKeys.length - 1); i++) {
|
||||||
|
if (!currentObj[nestedKeys[i]]) {
|
||||||
|
currentObj[nestedKeys[i]] = {};
|
||||||
|
}
|
||||||
|
currentObj = currentObj[nestedKeys[i]];
|
||||||
|
}
|
||||||
|
currentObj[nestedKeys[nestedKeys.length - 1]] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
interface IFilterDefinition {
|
||||||
|
event_fields?: string[];
|
||||||
|
event_format?: "client" | "federation";
|
||||||
|
presence?: IFilterComponent;
|
||||||
|
account_data?: IFilterComponent;
|
||||||
|
room?: IRoomFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRoomEventFilter extends IFilterComponent {
|
||||||
|
lazy_load_members?: boolean;
|
||||||
|
include_redundant_members?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IStateFilter extends IRoomEventFilter {}
|
||||||
|
|
||||||
|
interface IRoomFilter {
|
||||||
|
not_rooms?: string[];
|
||||||
|
rooms?: string[];
|
||||||
|
ephemeral?: IRoomEventFilter;
|
||||||
|
include_leave?: boolean;
|
||||||
|
state?: IStateFilter;
|
||||||
|
timeline?: IRoomEventFilter;
|
||||||
|
account_data?: IRoomEventFilter;
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new Filter.
|
||||||
|
* @constructor
|
||||||
|
* @param {string} userId The user ID for this filter.
|
||||||
|
* @param {string=} filterId The filter ID if known.
|
||||||
|
* @prop {string} userId The user ID of the filter
|
||||||
|
* @prop {?string} filterId The filter ID
|
||||||
|
*/
|
||||||
|
export class Filter {
|
||||||
|
static LAZY_LOADING_MESSAGES_FILTER = {
|
||||||
|
lazy_load_members: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a filter from existing data.
|
||||||
|
* @static
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {string} filterId
|
||||||
|
* @param {Object} jsonObj
|
||||||
|
* @return {Filter}
|
||||||
|
*/
|
||||||
|
static fromJson(userId: string, filterId: string, jsonObj: IFilterDefinition): Filter {
|
||||||
|
const filter = new Filter(userId, filterId);
|
||||||
|
filter.setDefinition(jsonObj);
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private definition: IFilterDefinition = {};
|
||||||
|
private roomFilter: FilterComponent;
|
||||||
|
private roomTimelineFilter: FilterComponent;
|
||||||
|
|
||||||
|
constructor(public readonly userId: string, public readonly filterId: string) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID of this filter on your homeserver (if known)
|
||||||
|
* @return {?string} The filter ID
|
||||||
|
*/
|
||||||
|
getFilterId(): string | null {
|
||||||
|
return this.filterId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the JSON body of the filter.
|
||||||
|
* @return {Object} The filter definition
|
||||||
|
*/
|
||||||
|
getDefinition(): IFilterDefinition {
|
||||||
|
return this.definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the JSON body of the filter
|
||||||
|
* @param {Object} definition The filter definition
|
||||||
|
*/
|
||||||
|
setDefinition(definition: IFilterDefinition) {
|
||||||
|
this.definition = definition;
|
||||||
|
|
||||||
|
// This is all ported from synapse's FilterCollection()
|
||||||
|
|
||||||
|
// definitions look something like:
|
||||||
|
// {
|
||||||
|
// "room": {
|
||||||
|
// "rooms": ["!abcde:example.com"],
|
||||||
|
// "not_rooms": ["!123456:example.com"],
|
||||||
|
// "state": {
|
||||||
|
// "types": ["m.room.*"],
|
||||||
|
// "not_rooms": ["!726s6s6q:example.com"],
|
||||||
|
// "lazy_load_members": true,
|
||||||
|
// },
|
||||||
|
// "timeline": {
|
||||||
|
// "limit": 10,
|
||||||
|
// "types": ["m.room.message"],
|
||||||
|
// "not_rooms": ["!726s6s6q:example.com"],
|
||||||
|
// "not_senders": ["@spam:example.com"]
|
||||||
|
// "contains_url": true
|
||||||
|
// },
|
||||||
|
// "ephemeral": {
|
||||||
|
// "types": ["m.receipt", "m.typing"],
|
||||||
|
// "not_rooms": ["!726s6s6q:example.com"],
|
||||||
|
// "not_senders": ["@spam:example.com"]
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "presence": {
|
||||||
|
// "types": ["m.presence"],
|
||||||
|
// "not_senders": ["@alice:example.com"]
|
||||||
|
// },
|
||||||
|
// "event_format": "client",
|
||||||
|
// "event_fields": ["type", "content", "sender"]
|
||||||
|
// }
|
||||||
|
|
||||||
|
const roomFilterJson = definition.room;
|
||||||
|
|
||||||
|
// consider the top level rooms/not_rooms filter
|
||||||
|
const roomFilterFields: IRoomFilter = {};
|
||||||
|
if (roomFilterJson) {
|
||||||
|
if (roomFilterJson.rooms) {
|
||||||
|
roomFilterFields.rooms = roomFilterJson.rooms;
|
||||||
|
}
|
||||||
|
if (roomFilterJson.rooms) {
|
||||||
|
roomFilterFields.not_rooms = roomFilterJson.not_rooms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.roomFilter = new FilterComponent(roomFilterFields);
|
||||||
|
this.roomTimelineFilter = new FilterComponent(
|
||||||
|
roomFilterJson ? (roomFilterJson.timeline || {}) : {},
|
||||||
|
);
|
||||||
|
|
||||||
|
// don't bother porting this from synapse yet:
|
||||||
|
// this._room_state_filter =
|
||||||
|
// new FilterComponent(roomFilterJson.state || {});
|
||||||
|
// this._room_ephemeral_filter =
|
||||||
|
// new FilterComponent(roomFilterJson.ephemeral || {});
|
||||||
|
// this._room_account_data_filter =
|
||||||
|
// new FilterComponent(roomFilterJson.account_data || {});
|
||||||
|
// this._presence_filter =
|
||||||
|
// new FilterComponent(definition.presence || {});
|
||||||
|
// this._account_data_filter =
|
||||||
|
// new FilterComponent(definition.account_data || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the room.timeline filter component of the filter
|
||||||
|
* @return {FilterComponent} room timeline filter component
|
||||||
|
*/
|
||||||
|
getRoomTimelineFilterComponent(): FilterComponent {
|
||||||
|
return this.roomTimelineFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the list of events based on whether they are allowed in a timeline
|
||||||
|
* based on this filter
|
||||||
|
* @param {MatrixEvent[]} events the list of events being filtered
|
||||||
|
* @return {MatrixEvent[]} the list of events which match the filter
|
||||||
|
*/
|
||||||
|
filterRoomTimeline(events: MatrixEvent[]): MatrixEvent[] {
|
||||||
|
return this.roomTimelineFilter.filter(this.roomFilter.filter(events));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the max number of events to return for each room's timeline.
|
||||||
|
* @param {Number} limit The max number of events to return for each room.
|
||||||
|
*/
|
||||||
|
setTimelineLimit(limit: number) {
|
||||||
|
setProp(this.definition, "room.timeline.limit", limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLazyLoadMembers(enabled: boolean) {
|
||||||
|
setProp(this.definition, "room.state.lazy_load_members", !!enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Control whether left rooms should be included in responses.
|
||||||
|
* @param {boolean} includeLeave True to make rooms the user has left appear
|
||||||
|
* in responses.
|
||||||
|
*/
|
||||||
|
setIncludeLeaveRooms(includeLeave: boolean) {
|
||||||
|
setProp(this.definition, "room.include_leave", includeLeave);
|
||||||
|
}
|
||||||
|
}
|
@@ -19,8 +19,16 @@ import { EventType, IEncryptedFile, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_M
|
|||||||
import { Room } from "./room";
|
import { Room } from "./room";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { MatrixEvent } from "./event";
|
import { MatrixEvent } from "./event";
|
||||||
import { averageBetweenStrings, DEFAULT_ALPHABET, lexicographicCompare, nextString, prevString } from "../utils";
|
import {
|
||||||
|
averageBetweenStrings,
|
||||||
|
DEFAULT_ALPHABET,
|
||||||
|
lexicographicCompare,
|
||||||
|
nextString,
|
||||||
|
prevString,
|
||||||
|
simpleRetryOperation,
|
||||||
|
} from "../utils";
|
||||||
import { MSC3089Branch } from "./MSC3089Branch";
|
import { MSC3089Branch } from "./MSC3089Branch";
|
||||||
|
import promiseRetry from "p-retry";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The recommended defaults for a tree space's power levels. Note that this
|
* The recommended defaults for a tree space's power levels. Note that this
|
||||||
@@ -110,12 +118,29 @@ export class MSC3089TreeSpace {
|
|||||||
* Invites a user to the tree space. They will be given the default Viewer
|
* Invites a user to the tree space. They will be given the default Viewer
|
||||||
* permission level unless specified elsewhere.
|
* permission level unless specified elsewhere.
|
||||||
* @param {string} userId The user ID to invite.
|
* @param {string} userId The user ID to invite.
|
||||||
|
* @param {boolean} andSubspaces True (default) to invite the user to all
|
||||||
|
* directories/subspaces too, recursively.
|
||||||
* @returns {Promise<void>} Resolves when complete.
|
* @returns {Promise<void>} Resolves when complete.
|
||||||
*/
|
*/
|
||||||
public invite(userId: string): Promise<void> {
|
public invite(userId: string, andSubspaces = true): Promise<void> {
|
||||||
// TODO: [@@TR] Reliable invites
|
|
||||||
// TODO: [@@TR] Share keys
|
// TODO: [@@TR] Share keys
|
||||||
return this.client.invite(this.roomId, userId);
|
const promises: Promise<void>[] = [this.retryInvite(userId)];
|
||||||
|
if (andSubspaces) {
|
||||||
|
promises.push(...this.getDirectories().map(d => d.invite(userId, andSubspaces)));
|
||||||
|
}
|
||||||
|
return Promise.all(promises).then(); // .then() to coerce types
|
||||||
|
}
|
||||||
|
|
||||||
|
private retryInvite(userId: string): Promise<void> {
|
||||||
|
return simpleRetryOperation(() => {
|
||||||
|
return this.client.invite(this.roomId, userId).catch(e => {
|
||||||
|
// We don't want to retry permission errors forever...
|
||||||
|
if (e?.errcode === "M_FORBIDDEN") {
|
||||||
|
throw new promiseRetry.AbortError(e);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,115 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module models/event-context
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct a new EventContext
|
|
||||||
*
|
|
||||||
* An eventcontext is used for circumstances such as search results, when we
|
|
||||||
* have a particular event of interest, and a bunch of events before and after
|
|
||||||
* it.
|
|
||||||
*
|
|
||||||
* It also stores pagination tokens for going backwards and forwards in the
|
|
||||||
* timeline.
|
|
||||||
*
|
|
||||||
* @param {MatrixEvent} ourEvent the event at the centre of this context
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export function EventContext(ourEvent) {
|
|
||||||
this._timeline = [ourEvent];
|
|
||||||
this._ourEventIndex = 0;
|
|
||||||
this._paginateTokens = { b: null, f: null };
|
|
||||||
|
|
||||||
// this is used by MatrixClient to keep track of active requests
|
|
||||||
this._paginateRequests = { b: null, f: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the main event of interest
|
|
||||||
*
|
|
||||||
* This is a convenience function for getTimeline()[getOurEventIndex()].
|
|
||||||
*
|
|
||||||
* @return {MatrixEvent} The event at the centre of this context.
|
|
||||||
*/
|
|
||||||
EventContext.prototype.getEvent = function() {
|
|
||||||
return this._timeline[this._ourEventIndex];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the list of events in this context
|
|
||||||
*
|
|
||||||
* @return {Array} An array of MatrixEvents
|
|
||||||
*/
|
|
||||||
EventContext.prototype.getTimeline = function() {
|
|
||||||
return this._timeline;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the index in the timeline of our event
|
|
||||||
*
|
|
||||||
* @return {Number}
|
|
||||||
*/
|
|
||||||
EventContext.prototype.getOurEventIndex = function() {
|
|
||||||
return this._ourEventIndex;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a pagination token.
|
|
||||||
*
|
|
||||||
* @param {boolean} backwards true to get the pagination token for going
|
|
||||||
* backwards in time
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
EventContext.prototype.getPaginateToken = function(backwards) {
|
|
||||||
return this._paginateTokens[backwards ? 'b' : 'f'];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a pagination token.
|
|
||||||
*
|
|
||||||
* Generally this will be used only by the matrix js sdk.
|
|
||||||
*
|
|
||||||
* @param {string} token pagination token
|
|
||||||
* @param {boolean} backwards true to set the pagination token for going
|
|
||||||
* backwards in time
|
|
||||||
*/
|
|
||||||
EventContext.prototype.setPaginateToken = function(token, backwards) {
|
|
||||||
this._paginateTokens[backwards ? 'b' : 'f'] = token;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add more events to the timeline
|
|
||||||
*
|
|
||||||
* @param {Array} events new events, in timeline order
|
|
||||||
* @param {boolean} atStart true to insert new events at the start
|
|
||||||
*/
|
|
||||||
EventContext.prototype.addEvents = function(events, atStart) {
|
|
||||||
// TODO: should we share logic with Room.addEventsToTimeline?
|
|
||||||
// Should Room even use EventContext?
|
|
||||||
|
|
||||||
if (atStart) {
|
|
||||||
this._timeline = events.concat(this._timeline);
|
|
||||||
this._ourEventIndex += events.length;
|
|
||||||
} else {
|
|
||||||
this._timeline = this._timeline.concat(events);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
123
src/models/event-context.ts
Normal file
123
src/models/event-context.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MatrixEvent } from "./event";
|
||||||
|
|
||||||
|
enum Direction {
|
||||||
|
Backward = "b",
|
||||||
|
Forward = "f",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module models/event-context
|
||||||
|
*/
|
||||||
|
export class EventContext {
|
||||||
|
private timeline: MatrixEvent[];
|
||||||
|
private ourEventIndex = 0;
|
||||||
|
private paginateTokens: Record<Direction, string | null> = {
|
||||||
|
[Direction.Backward]: null,
|
||||||
|
[Direction.Forward]: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new EventContext
|
||||||
|
*
|
||||||
|
* An eventcontext is used for circumstances such as search results, when we
|
||||||
|
* have a particular event of interest, and a bunch of events before and after
|
||||||
|
* it.
|
||||||
|
*
|
||||||
|
* It also stores pagination tokens for going backwards and forwards in the
|
||||||
|
* timeline.
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent} ourEvent the event at the centre of this context
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
constructor(ourEvent: MatrixEvent) {
|
||||||
|
this.timeline = [ourEvent];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the main event of interest
|
||||||
|
*
|
||||||
|
* This is a convenience function for getTimeline()[getOurEventIndex()].
|
||||||
|
*
|
||||||
|
* @return {MatrixEvent} The event at the centre of this context.
|
||||||
|
*/
|
||||||
|
public getEvent(): MatrixEvent {
|
||||||
|
return this.timeline[this.ourEventIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of events in this context
|
||||||
|
*
|
||||||
|
* @return {Array} An array of MatrixEvents
|
||||||
|
*/
|
||||||
|
public getTimeline(): MatrixEvent[] {
|
||||||
|
return this.timeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the index in the timeline of our event
|
||||||
|
*
|
||||||
|
* @return {Number}
|
||||||
|
*/
|
||||||
|
public getOurEventIndex(): number {
|
||||||
|
return this.ourEventIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a pagination token.
|
||||||
|
*
|
||||||
|
* @param {boolean} backwards true to get the pagination token for going
|
||||||
|
* backwards in time
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
public getPaginateToken(backwards = false): string {
|
||||||
|
return this.paginateTokens[backwards ? Direction.Backward : Direction.Forward];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a pagination token.
|
||||||
|
*
|
||||||
|
* Generally this will be used only by the matrix js sdk.
|
||||||
|
*
|
||||||
|
* @param {string} token pagination token
|
||||||
|
* @param {boolean} backwards true to set the pagination token for going
|
||||||
|
* backwards in time
|
||||||
|
*/
|
||||||
|
public setPaginateToken(token: string, backwards = false): void {
|
||||||
|
this.paginateTokens[backwards ? Direction.Backward : Direction.Forward] = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add more events to the timeline
|
||||||
|
*
|
||||||
|
* @param {Array} events new events, in timeline order
|
||||||
|
* @param {boolean} atStart true to insert new events at the start
|
||||||
|
*/
|
||||||
|
public addEvents(events: MatrixEvent[], atStart = false): void {
|
||||||
|
// TODO: should we share logic with Room.addEventsToTimeline?
|
||||||
|
// Should Room even use EventContext?
|
||||||
|
|
||||||
|
if (atStart) {
|
||||||
|
this.timeline = events.concat(this.timeline);
|
||||||
|
this.ourEventIndex += events.length;
|
||||||
|
} else {
|
||||||
|
this.timeline = this.timeline.concat(events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -15,8 +15,11 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { EventStatus } from '../models/event';
|
|
||||||
|
import { EventStatus, MatrixEvent } from './event';
|
||||||
|
import { Room } from './room';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
|
import { RelationType } from "../@types/event";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A container for relation events that supports easy access to common ways of
|
* A container for relation events that supports easy access to common ways of
|
||||||
@@ -27,8 +30,16 @@ import { logger } from '../logger';
|
|||||||
* EventTimelineSet#getRelationsForEvent.
|
* EventTimelineSet#getRelationsForEvent.
|
||||||
*/
|
*/
|
||||||
export class Relations extends EventEmitter {
|
export class Relations extends EventEmitter {
|
||||||
|
private relationEventIds = new Set<string>();
|
||||||
|
private relations = new Set<MatrixEvent>();
|
||||||
|
private annotationsByKey: Record<string, Set<MatrixEvent>> = {};
|
||||||
|
private annotationsBySender: Record<string, Set<MatrixEvent>> = {};
|
||||||
|
private sortedAnnotationsByKey: [string, Set<MatrixEvent>][] = [];
|
||||||
|
private targetEvent: MatrixEvent = null;
|
||||||
|
private creationEmitted = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} relationType
|
* @param {RelationType} relationType
|
||||||
* The type of relation involved, such as "m.annotation", "m.reference",
|
* The type of relation involved, such as "m.annotation", "m.reference",
|
||||||
* "m.replace", etc.
|
* "m.replace", etc.
|
||||||
* @param {String} eventType
|
* @param {String} eventType
|
||||||
@@ -37,18 +48,12 @@ export class Relations extends EventEmitter {
|
|||||||
* Room for this container. May be null for non-room cases, such as the
|
* Room for this container. May be null for non-room cases, such as the
|
||||||
* notification timeline.
|
* notification timeline.
|
||||||
*/
|
*/
|
||||||
constructor(relationType, eventType, room) {
|
constructor(
|
||||||
|
public readonly relationType: RelationType,
|
||||||
|
public readonly eventType: string,
|
||||||
|
private readonly room: Room,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
this.relationType = relationType;
|
|
||||||
this.eventType = eventType;
|
|
||||||
this._relationEventIds = new Set();
|
|
||||||
this._relations = new Set();
|
|
||||||
this._annotationsByKey = {};
|
|
||||||
this._annotationsBySender = {};
|
|
||||||
this._sortedAnnotationsByKey = [];
|
|
||||||
this._targetEvent = null;
|
|
||||||
this._room = room;
|
|
||||||
this._creationEmitted = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,8 +62,8 @@ export class Relations extends EventEmitter {
|
|||||||
* @param {MatrixEvent} event
|
* @param {MatrixEvent} event
|
||||||
* The new relation event to be added.
|
* The new relation event to be added.
|
||||||
*/
|
*/
|
||||||
async addEvent(event) {
|
public async addEvent(event: MatrixEvent) {
|
||||||
if (this._relationEventIds.has(event.getId())) {
|
if (this.relationEventIds.has(event.getId())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,24 +84,24 @@ export class Relations extends EventEmitter {
|
|||||||
// If the event is in the process of being sent, listen for cancellation
|
// If the event is in the process of being sent, listen for cancellation
|
||||||
// so we can remove the event from the collection.
|
// so we can remove the event from the collection.
|
||||||
if (event.isSending()) {
|
if (event.isSending()) {
|
||||||
event.on("Event.status", this._onEventStatus);
|
event.on("Event.status", this.onEventStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._relations.add(event);
|
this.relations.add(event);
|
||||||
this._relationEventIds.add(event.getId());
|
this.relationEventIds.add(event.getId());
|
||||||
|
|
||||||
if (this.relationType === "m.annotation") {
|
if (this.relationType === RelationType.Annotation) {
|
||||||
this._addAnnotationToAggregation(event);
|
this.addAnnotationToAggregation(event);
|
||||||
} else if (this.relationType === "m.replace" && this._targetEvent) {
|
} else if (this.relationType === RelationType.Replace && this.targetEvent) {
|
||||||
const lastReplacement = await this.getLastReplacement();
|
const lastReplacement = await this.getLastReplacement();
|
||||||
this._targetEvent.makeReplaced(lastReplacement);
|
this.targetEvent.makeReplaced(lastReplacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
event.on("Event.beforeRedaction", this._onBeforeRedaction);
|
event.on("Event.beforeRedaction", this.onBeforeRedaction);
|
||||||
|
|
||||||
this.emit("Relations.add", event);
|
this.emit("Relations.add", event);
|
||||||
|
|
||||||
this._maybeEmitCreated();
|
this.maybeEmitCreated();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,8 +110,8 @@ export class Relations extends EventEmitter {
|
|||||||
* @param {MatrixEvent} event
|
* @param {MatrixEvent} event
|
||||||
* The relation event to remove.
|
* The relation event to remove.
|
||||||
*/
|
*/
|
||||||
async _removeEvent(event) {
|
private async removeEvent(event: MatrixEvent) {
|
||||||
if (!this._relations.has(event)) {
|
if (!this.relations.has(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,13 +129,13 @@ export class Relations extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._relations.delete(event);
|
this.relations.delete(event);
|
||||||
|
|
||||||
if (this.relationType === "m.annotation") {
|
if (this.relationType === RelationType.Annotation) {
|
||||||
this._removeAnnotationFromAggregation(event);
|
this.removeAnnotationFromAggregation(event);
|
||||||
} else if (this.relationType === "m.replace" && this._targetEvent) {
|
} else if (this.relationType === RelationType.Replace && this.targetEvent) {
|
||||||
const lastReplacement = await this.getLastReplacement();
|
const lastReplacement = await this.getLastReplacement();
|
||||||
this._targetEvent.makeReplaced(lastReplacement);
|
this.targetEvent.makeReplaced(lastReplacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit("Relations.remove", event);
|
this.emit("Relations.remove", event);
|
||||||
@@ -142,19 +147,19 @@ export class Relations extends EventEmitter {
|
|||||||
* @param {MatrixEvent} event The event whose status has changed
|
* @param {MatrixEvent} event The event whose status has changed
|
||||||
* @param {EventStatus} status The new status
|
* @param {EventStatus} status The new status
|
||||||
*/
|
*/
|
||||||
_onEventStatus = (event, status) => {
|
private onEventStatus = (event: MatrixEvent, status: EventStatus) => {
|
||||||
if (!event.isSending()) {
|
if (!event.isSending()) {
|
||||||
// Sending is done, so we don't need to listen anymore
|
// Sending is done, so we don't need to listen anymore
|
||||||
event.removeListener("Event.status", this._onEventStatus);
|
event.removeListener("Event.status", this.onEventStatus);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status !== EventStatus.CANCELLED) {
|
if (status !== EventStatus.CANCELLED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Event was cancelled, remove from the collection
|
// Event was cancelled, remove from the collection
|
||||||
event.removeListener("Event.status", this._onEventStatus);
|
event.removeListener("Event.status", this.onEventStatus);
|
||||||
this._removeEvent(event);
|
this.removeEvent(event);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all relation events in this collection.
|
* Get all relation events in this collection.
|
||||||
@@ -166,51 +171,51 @@ export class Relations extends EventEmitter {
|
|||||||
* @return {Array}
|
* @return {Array}
|
||||||
* Relation events in insertion order.
|
* Relation events in insertion order.
|
||||||
*/
|
*/
|
||||||
getRelations() {
|
public getRelations() {
|
||||||
return [...this._relations];
|
return [...this.relations];
|
||||||
}
|
}
|
||||||
|
|
||||||
_addAnnotationToAggregation(event) {
|
private addAnnotationToAggregation(event: MatrixEvent) {
|
||||||
const { key } = event.getRelation();
|
const { key } = event.getRelation();
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let eventsForKey = this._annotationsByKey[key];
|
let eventsForKey = this.annotationsByKey[key];
|
||||||
if (!eventsForKey) {
|
if (!eventsForKey) {
|
||||||
eventsForKey = this._annotationsByKey[key] = new Set();
|
eventsForKey = this.annotationsByKey[key] = new Set();
|
||||||
this._sortedAnnotationsByKey.push([key, eventsForKey]);
|
this.sortedAnnotationsByKey.push([key, eventsForKey]);
|
||||||
}
|
}
|
||||||
// Add the new event to the set for this key
|
// Add the new event to the set for this key
|
||||||
eventsForKey.add(event);
|
eventsForKey.add(event);
|
||||||
// Re-sort the [key, events] pairs in descending order of event count
|
// Re-sort the [key, events] pairs in descending order of event count
|
||||||
this._sortedAnnotationsByKey.sort((a, b) => {
|
this.sortedAnnotationsByKey.sort((a, b) => {
|
||||||
const aEvents = a[1];
|
const aEvents = a[1];
|
||||||
const bEvents = b[1];
|
const bEvents = b[1];
|
||||||
return bEvents.size - aEvents.size;
|
return bEvents.size - aEvents.size;
|
||||||
});
|
});
|
||||||
|
|
||||||
const sender = event.getSender();
|
const sender = event.getSender();
|
||||||
let eventsFromSender = this._annotationsBySender[sender];
|
let eventsFromSender = this.annotationsBySender[sender];
|
||||||
if (!eventsFromSender) {
|
if (!eventsFromSender) {
|
||||||
eventsFromSender = this._annotationsBySender[sender] = new Set();
|
eventsFromSender = this.annotationsBySender[sender] = new Set();
|
||||||
}
|
}
|
||||||
// Add the new event to the set for this sender
|
// Add the new event to the set for this sender
|
||||||
eventsFromSender.add(event);
|
eventsFromSender.add(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
_removeAnnotationFromAggregation(event) {
|
private removeAnnotationFromAggregation(event: MatrixEvent) {
|
||||||
const { key } = event.getRelation();
|
const { key } = event.getRelation();
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventsForKey = this._annotationsByKey[key];
|
const eventsForKey = this.annotationsByKey[key];
|
||||||
if (eventsForKey) {
|
if (eventsForKey) {
|
||||||
eventsForKey.delete(event);
|
eventsForKey.delete(event);
|
||||||
|
|
||||||
// Re-sort the [key, events] pairs in descending order of event count
|
// Re-sort the [key, events] pairs in descending order of event count
|
||||||
this._sortedAnnotationsByKey.sort((a, b) => {
|
this.sortedAnnotationsByKey.sort((a, b) => {
|
||||||
const aEvents = a[1];
|
const aEvents = a[1];
|
||||||
const bEvents = b[1];
|
const bEvents = b[1];
|
||||||
return bEvents.size - aEvents.size;
|
return bEvents.size - aEvents.size;
|
||||||
@@ -218,7 +223,7 @@ export class Relations extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sender = event.getSender();
|
const sender = event.getSender();
|
||||||
const eventsFromSender = this._annotationsBySender[sender];
|
const eventsFromSender = this.annotationsBySender[sender];
|
||||||
if (eventsFromSender) {
|
if (eventsFromSender) {
|
||||||
eventsFromSender.delete(event);
|
eventsFromSender.delete(event);
|
||||||
}
|
}
|
||||||
@@ -235,25 +240,25 @@ export class Relations extends EventEmitter {
|
|||||||
* @param {MatrixEvent} redactedEvent
|
* @param {MatrixEvent} redactedEvent
|
||||||
* The original relation event that is about to be redacted.
|
* The original relation event that is about to be redacted.
|
||||||
*/
|
*/
|
||||||
_onBeforeRedaction = async (redactedEvent) => {
|
private onBeforeRedaction = async (redactedEvent: MatrixEvent) => {
|
||||||
if (!this._relations.has(redactedEvent)) {
|
if (!this.relations.has(redactedEvent)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._relations.delete(redactedEvent);
|
this.relations.delete(redactedEvent);
|
||||||
|
|
||||||
if (this.relationType === "m.annotation") {
|
if (this.relationType === RelationType.Annotation) {
|
||||||
// Remove the redacted annotation from aggregation by key
|
// Remove the redacted annotation from aggregation by key
|
||||||
this._removeAnnotationFromAggregation(redactedEvent);
|
this.removeAnnotationFromAggregation(redactedEvent);
|
||||||
} else if (this.relationType === "m.replace" && this._targetEvent) {
|
} else if (this.relationType === RelationType.Replace && this.targetEvent) {
|
||||||
const lastReplacement = await this.getLastReplacement();
|
const lastReplacement = await this.getLastReplacement();
|
||||||
this._targetEvent.makeReplaced(lastReplacement);
|
this.targetEvent.makeReplaced(lastReplacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
redactedEvent.removeListener("Event.beforeRedaction", this._onBeforeRedaction);
|
redactedEvent.removeListener("Event.beforeRedaction", this.onBeforeRedaction);
|
||||||
|
|
||||||
this.emit("Relations.redaction", redactedEvent);
|
this.emit("Relations.redaction", redactedEvent);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all events in this collection grouped by key and sorted by descending
|
* Get all events in this collection grouped by key and sorted by descending
|
||||||
@@ -265,13 +270,13 @@ export class Relations extends EventEmitter {
|
|||||||
* An array of [key, events] pairs sorted by descending event count.
|
* An array of [key, events] pairs sorted by descending event count.
|
||||||
* The events are stored in a Set (which preserves insertion order).
|
* The events are stored in a Set (which preserves insertion order).
|
||||||
*/
|
*/
|
||||||
getSortedAnnotationsByKey() {
|
public getSortedAnnotationsByKey() {
|
||||||
if (this.relationType !== "m.annotation") {
|
if (this.relationType !== RelationType.Annotation) {
|
||||||
// Other relation types are not grouped currently.
|
// Other relation types are not grouped currently.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._sortedAnnotationsByKey;
|
return this.sortedAnnotationsByKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -283,13 +288,13 @@ export class Relations extends EventEmitter {
|
|||||||
* An object with each relation sender as a key and the matching Set of
|
* An object with each relation sender as a key and the matching Set of
|
||||||
* events for that sender as a value.
|
* events for that sender as a value.
|
||||||
*/
|
*/
|
||||||
getAnnotationsBySender() {
|
public getAnnotationsBySender() {
|
||||||
if (this.relationType !== "m.annotation") {
|
if (this.relationType !== RelationType.Annotation) {
|
||||||
// Other relation types are not grouped currently.
|
// Other relation types are not grouped currently.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._annotationsBySender;
|
return this.annotationsBySender;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -300,12 +305,12 @@ export class Relations extends EventEmitter {
|
|||||||
*
|
*
|
||||||
* @return {MatrixEvent?}
|
* @return {MatrixEvent?}
|
||||||
*/
|
*/
|
||||||
async getLastReplacement() {
|
public async getLastReplacement(): Promise<MatrixEvent | null> {
|
||||||
if (this.relationType !== "m.replace") {
|
if (this.relationType !== RelationType.Replace) {
|
||||||
// Aggregating on last only makes sense for this relation type
|
// Aggregating on last only makes sense for this relation type
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!this._targetEvent) {
|
if (!this.targetEvent) {
|
||||||
// Don't know which replacements to accept yet.
|
// Don't know which replacements to accept yet.
|
||||||
// This method shouldn't be called before the original
|
// This method shouldn't be called before the original
|
||||||
// event is known anyway.
|
// event is known anyway.
|
||||||
@@ -314,12 +319,11 @@ export class Relations extends EventEmitter {
|
|||||||
|
|
||||||
// the all-knowning server tells us that the event at some point had
|
// the all-knowning server tells us that the event at some point had
|
||||||
// this timestamp for its replacement, so any following replacement should definitely not be less
|
// this timestamp for its replacement, so any following replacement should definitely not be less
|
||||||
const replaceRelation =
|
const replaceRelation = this.targetEvent.getServerAggregatedRelation(RelationType.Replace);
|
||||||
this._targetEvent.getServerAggregatedRelation("m.replace");
|
|
||||||
const minTs = replaceRelation && replaceRelation.origin_server_ts;
|
const minTs = replaceRelation && replaceRelation.origin_server_ts;
|
||||||
|
|
||||||
const lastReplacement = this.getRelations().reduce((last, event) => {
|
const lastReplacement = this.getRelations().reduce((last, event) => {
|
||||||
if (event.getSender() !== this._targetEvent.getSender()) {
|
if (event.getSender() !== this.targetEvent.getSender()) {
|
||||||
return last;
|
return last;
|
||||||
}
|
}
|
||||||
if (minTs && minTs > event.getTs()) {
|
if (minTs && minTs > event.getTs()) {
|
||||||
@@ -332,9 +336,9 @@ export class Relations extends EventEmitter {
|
|||||||
}, null);
|
}, null);
|
||||||
|
|
||||||
if (lastReplacement?.shouldAttemptDecryption()) {
|
if (lastReplacement?.shouldAttemptDecryption()) {
|
||||||
await lastReplacement.attemptDecryption(this._room._client.crypto);
|
await lastReplacement.attemptDecryption(this.room._client.crypto);
|
||||||
} else if (lastReplacement?.isBeingDecrypted()) {
|
} else if (lastReplacement?.isBeingDecrypted()) {
|
||||||
await lastReplacement._decryptionPromise;
|
await lastReplacement.getDecryptionPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
return lastReplacement;
|
return lastReplacement;
|
||||||
@@ -343,38 +347,34 @@ export class Relations extends EventEmitter {
|
|||||||
/*
|
/*
|
||||||
* @param {MatrixEvent} targetEvent the event the relations are related to.
|
* @param {MatrixEvent} targetEvent the event the relations are related to.
|
||||||
*/
|
*/
|
||||||
async setTargetEvent(event) {
|
public async setTargetEvent(event: MatrixEvent) {
|
||||||
if (this._targetEvent) {
|
if (this.targetEvent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._targetEvent = event;
|
this.targetEvent = event;
|
||||||
|
|
||||||
if (this.relationType === "m.replace") {
|
if (this.relationType === RelationType.Replace) {
|
||||||
const replacement = await this.getLastReplacement();
|
const replacement = await this.getLastReplacement();
|
||||||
// this is the initial update, so only call it if we already have something
|
// this is the initial update, so only call it if we already have something
|
||||||
// to not emit Event.replaced needlessly
|
// to not emit Event.replaced needlessly
|
||||||
if (replacement) {
|
if (replacement) {
|
||||||
this._targetEvent.makeReplaced(replacement);
|
this.targetEvent.makeReplaced(replacement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._maybeEmitCreated();
|
this.maybeEmitCreated();
|
||||||
}
|
}
|
||||||
|
|
||||||
_maybeEmitCreated() {
|
private maybeEmitCreated() {
|
||||||
if (this._creationEmitted) {
|
if (this.creationEmitted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Only emit we're "created" once we have a target event instance _and_
|
// Only emit we're "created" once we have a target event instance _and_
|
||||||
// at least one related event.
|
// at least one related event.
|
||||||
if (!this._targetEvent || !this._relations.size) {
|
if (!this.targetEvent || !this.relations.size) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._creationEmitted = true;
|
this.creationEmitted = true;
|
||||||
this._targetEvent.emit(
|
this.targetEvent.emit("Event.relationsCreated", this.relationType, this.eventType);
|
||||||
"Event.relationsCreated",
|
|
||||||
this.relationType,
|
|
||||||
this.eventType,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,393 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module models/room-member
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { EventEmitter } from "events";
|
|
||||||
import { getHttpUriForMxc } from "../content-repo";
|
|
||||||
import * as utils from "../utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct a new room member.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
* @alias module:models/room-member
|
|
||||||
*
|
|
||||||
* @param {string} roomId The room ID of the member.
|
|
||||||
* @param {string} userId The user ID of the member.
|
|
||||||
* @prop {string} roomId The room ID for this member.
|
|
||||||
* @prop {string} userId The user ID of this member.
|
|
||||||
* @prop {boolean} typing True if the room member is currently typing.
|
|
||||||
* @prop {string} name The human-readable name for this room member. This will be
|
|
||||||
* disambiguated with a suffix of " (@user_id:matrix.org)" if another member shares the
|
|
||||||
* same displayname.
|
|
||||||
* @prop {string} rawDisplayName The ambiguous displayname of this room member.
|
|
||||||
* @prop {Number} powerLevel The power level for this room member.
|
|
||||||
* @prop {Number} powerLevelNorm The normalised power level (0-100) for this
|
|
||||||
* room member.
|
|
||||||
* @prop {User} user The User object for this room member, if one exists.
|
|
||||||
* @prop {string} membership The membership state for this room member e.g. 'join'.
|
|
||||||
* @prop {Object} events The events describing this RoomMember.
|
|
||||||
* @prop {MatrixEvent} events.member The m.room.member event for this RoomMember.
|
|
||||||
* @prop {boolean} disambiguate True if the member's name is disambiguated.
|
|
||||||
*/
|
|
||||||
export function RoomMember(roomId, userId) {
|
|
||||||
this.roomId = roomId;
|
|
||||||
this.userId = userId;
|
|
||||||
this.typing = false;
|
|
||||||
this.name = userId;
|
|
||||||
this.rawDisplayName = userId;
|
|
||||||
this.powerLevel = 0;
|
|
||||||
this.powerLevelNorm = 0;
|
|
||||||
this.user = null;
|
|
||||||
this.membership = null;
|
|
||||||
this.events = {
|
|
||||||
member: null,
|
|
||||||
};
|
|
||||||
this._isOutOfBand = false;
|
|
||||||
this._updateModifiedTime();
|
|
||||||
this.disambiguate = false;
|
|
||||||
}
|
|
||||||
utils.inherits(RoomMember, EventEmitter);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark the member as coming from a channel that is not sync
|
|
||||||
*/
|
|
||||||
RoomMember.prototype.markOutOfBand = function() {
|
|
||||||
this._isOutOfBand = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {bool} does the member come from a channel that is not sync?
|
|
||||||
* This is used to store the member seperately
|
|
||||||
* from the sync state so it available across browser sessions.
|
|
||||||
*/
|
|
||||||
RoomMember.prototype.isOutOfBand = function() {
|
|
||||||
return this._isOutOfBand;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update this room member's membership event. May fire "RoomMember.name" if
|
|
||||||
* this event updates this member's name.
|
|
||||||
* @param {MatrixEvent} event The <code>m.room.member</code> event
|
|
||||||
* @param {RoomState} roomState Optional. The room state to take into account
|
|
||||||
* when calculating (e.g. for disambiguating users with the same name).
|
|
||||||
* @fires module:client~MatrixClient#event:"RoomMember.name"
|
|
||||||
* @fires module:client~MatrixClient#event:"RoomMember.membership"
|
|
||||||
*/
|
|
||||||
RoomMember.prototype.setMembershipEvent = function(event, roomState) {
|
|
||||||
const displayName = event.getDirectionalContent().displayname;
|
|
||||||
|
|
||||||
if (event.getType() !== "m.room.member") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._isOutOfBand = false;
|
|
||||||
|
|
||||||
this.events.member = event;
|
|
||||||
|
|
||||||
const oldMembership = this.membership;
|
|
||||||
this.membership = event.getDirectionalContent().membership;
|
|
||||||
|
|
||||||
this.disambiguate = shouldDisambiguate(
|
|
||||||
this.userId,
|
|
||||||
displayName,
|
|
||||||
roomState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const oldName = this.name;
|
|
||||||
this.name = calculateDisplayName(
|
|
||||||
this.userId,
|
|
||||||
displayName,
|
|
||||||
roomState,
|
|
||||||
this.disambiguate,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.rawDisplayName = event.getDirectionalContent().displayname || this.userId;
|
|
||||||
if (oldMembership !== this.membership) {
|
|
||||||
this._updateModifiedTime();
|
|
||||||
this.emit("RoomMember.membership", event, this, oldMembership);
|
|
||||||
}
|
|
||||||
if (oldName !== this.name) {
|
|
||||||
this._updateModifiedTime();
|
|
||||||
this.emit("RoomMember.name", event, this, oldName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update this room member's power level event. May fire
|
|
||||||
* "RoomMember.powerLevel" if this event updates this member's power levels.
|
|
||||||
* @param {MatrixEvent} powerLevelEvent The <code>m.room.power_levels</code>
|
|
||||||
* event
|
|
||||||
* @fires module:client~MatrixClient#event:"RoomMember.powerLevel"
|
|
||||||
*/
|
|
||||||
RoomMember.prototype.setPowerLevelEvent = function(powerLevelEvent) {
|
|
||||||
if (powerLevelEvent.getType() !== "m.room.power_levels") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const evContent = powerLevelEvent.getDirectionalContent();
|
|
||||||
|
|
||||||
let maxLevel = evContent.users_default || 0;
|
|
||||||
const users = evContent.users || {};
|
|
||||||
Object.values(users).forEach(function(lvl) {
|
|
||||||
maxLevel = Math.max(maxLevel, lvl);
|
|
||||||
});
|
|
||||||
const oldPowerLevel = this.powerLevel;
|
|
||||||
const oldPowerLevelNorm = this.powerLevelNorm;
|
|
||||||
|
|
||||||
if (users[this.userId] !== undefined) {
|
|
||||||
this.powerLevel = users[this.userId];
|
|
||||||
} else if (evContent.users_default !== undefined) {
|
|
||||||
this.powerLevel = evContent.users_default;
|
|
||||||
} else {
|
|
||||||
this.powerLevel = 0;
|
|
||||||
}
|
|
||||||
this.powerLevelNorm = 0;
|
|
||||||
if (maxLevel > 0) {
|
|
||||||
this.powerLevelNorm = (this.powerLevel * 100) / maxLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
// emit for changes in powerLevelNorm as well (since the app will need to
|
|
||||||
// redraw everyone's level if the max has changed)
|
|
||||||
if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) {
|
|
||||||
this._updateModifiedTime();
|
|
||||||
this.emit("RoomMember.powerLevel", powerLevelEvent, this);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update this room member's typing event. May fire "RoomMember.typing" if
|
|
||||||
* this event changes this member's typing state.
|
|
||||||
* @param {MatrixEvent} event The typing event
|
|
||||||
* @fires module:client~MatrixClient#event:"RoomMember.typing"
|
|
||||||
*/
|
|
||||||
RoomMember.prototype.setTypingEvent = function(event) {
|
|
||||||
if (event.getType() !== "m.typing") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const oldTyping = this.typing;
|
|
||||||
this.typing = false;
|
|
||||||
const typingList = event.getContent().user_ids;
|
|
||||||
if (!Array.isArray(typingList)) {
|
|
||||||
// malformed event :/ bail early. TODO: whine?
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typingList.indexOf(this.userId) !== -1) {
|
|
||||||
this.typing = true;
|
|
||||||
}
|
|
||||||
if (oldTyping !== this.typing) {
|
|
||||||
this._updateModifiedTime();
|
|
||||||
this.emit("RoomMember.typing", event, this);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the last modified time to the current time.
|
|
||||||
*/
|
|
||||||
RoomMember.prototype._updateModifiedTime = function() {
|
|
||||||
this._modified = Date.now();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the timestamp when this RoomMember was last updated. This timestamp is
|
|
||||||
* updated when properties on this RoomMember are updated.
|
|
||||||
* It is updated <i>before</i> firing events.
|
|
||||||
* @return {number} The timestamp
|
|
||||||
*/
|
|
||||||
RoomMember.prototype.getLastModifiedTime = function() {
|
|
||||||
return this._modified;
|
|
||||||
};
|
|
||||||
|
|
||||||
RoomMember.prototype.isKicked = function() {
|
|
||||||
return this.membership === "leave" &&
|
|
||||||
this.events.member.getSender() !== this.events.member.getStateKey();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this member was invited with the is_direct flag set, return
|
|
||||||
* the user that invited this member
|
|
||||||
* @return {string} user id of the inviter
|
|
||||||
*/
|
|
||||||
RoomMember.prototype.getDMInviter = function() {
|
|
||||||
// when not available because that room state hasn't been loaded in,
|
|
||||||
// we don't really know, but more likely to not be a direct chat
|
|
||||||
if (this.events.member) {
|
|
||||||
// TODO: persist the is_direct flag on the member as more member events
|
|
||||||
// come in caused by displayName changes.
|
|
||||||
|
|
||||||
// the is_direct flag is set on the invite member event.
|
|
||||||
// This is copied on the prev_content section of the join member event
|
|
||||||
// when the invite is accepted.
|
|
||||||
|
|
||||||
const memberEvent = this.events.member;
|
|
||||||
let memberContent = memberEvent.getContent();
|
|
||||||
let inviteSender = memberEvent.getSender();
|
|
||||||
|
|
||||||
if (memberContent.membership === "join") {
|
|
||||||
memberContent = memberEvent.getPrevContent();
|
|
||||||
inviteSender = memberEvent.getUnsigned().prev_sender;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (memberContent.membership === "invite" && memberContent.is_direct) {
|
|
||||||
return inviteSender;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the avatar URL for a room member.
|
|
||||||
* @param {string} baseUrl The base homeserver URL See
|
|
||||||
* {@link module:client~MatrixClient#getHomeserverUrl}.
|
|
||||||
* @param {Number} width The desired width of the thumbnail.
|
|
||||||
* @param {Number} height The desired height of the thumbnail.
|
|
||||||
* @param {string} resizeMethod The thumbnail resize method to use, either
|
|
||||||
* "crop" or "scale".
|
|
||||||
* @param {Boolean} allowDefault (optional) Passing false causes this method to
|
|
||||||
* return null if the user has no avatar image. Otherwise, a default image URL
|
|
||||||
* will be returned. Default: true. (Deprecated)
|
|
||||||
* @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be
|
|
||||||
* returned even if it is a direct hyperlink rather than a matrix content URL.
|
|
||||||
* If false, any non-matrix content URLs will be ignored. Setting this option to
|
|
||||||
* true will expose URLs that, if fetched, will leak information about the user
|
|
||||||
* to anyone who they share a room with.
|
|
||||||
* @return {?string} the avatar URL or null.
|
|
||||||
*/
|
|
||||||
RoomMember.prototype.getAvatarUrl =
|
|
||||||
function(baseUrl, width, height, resizeMethod, allowDefault, allowDirectLinks) {
|
|
||||||
if (allowDefault === undefined) {
|
|
||||||
allowDefault = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawUrl = this.getMxcAvatarUrl();
|
|
||||||
|
|
||||||
if (!rawUrl && !allowDefault) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const httpUrl = getHttpUriForMxc(
|
|
||||||
baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks,
|
|
||||||
);
|
|
||||||
if (httpUrl) {
|
|
||||||
return httpUrl;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* get the mxc avatar url, either from a state event, or from a lazily loaded member
|
|
||||||
* @return {string} the mxc avatar url
|
|
||||||
*/
|
|
||||||
RoomMember.prototype.getMxcAvatarUrl = function() {
|
|
||||||
if (this.events.member) {
|
|
||||||
return this.events.member.getDirectionalContent().avatar_url;
|
|
||||||
} else if (this.user) {
|
|
||||||
return this.user.avatarUrl;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MXID_PATTERN = /@.+:.+/;
|
|
||||||
const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/;
|
|
||||||
|
|
||||||
function shouldDisambiguate(selfUserId, displayName, roomState) {
|
|
||||||
if (!displayName || displayName === selfUserId) return false;
|
|
||||||
|
|
||||||
// First check if the displayname is something we consider truthy
|
|
||||||
// after stripping it of zero width characters and padding spaces
|
|
||||||
if (!utils.removeHiddenChars(displayName)) return false;
|
|
||||||
|
|
||||||
if (!roomState) return false;
|
|
||||||
|
|
||||||
// Next check if the name contains something that look like a mxid
|
|
||||||
// If it does, it may be someone trying to impersonate someone else
|
|
||||||
// Show full mxid in this case
|
|
||||||
if (MXID_PATTERN.test(displayName)) return true;
|
|
||||||
|
|
||||||
// Also show mxid if the display name contains any LTR/RTL characters as these
|
|
||||||
// make it very difficult for us to find similar *looking* display names
|
|
||||||
// E.g "Mark" could be cloned by writing "kraM" but in RTL.
|
|
||||||
if (LTR_RTL_PATTERN.test(displayName)) return true;
|
|
||||||
|
|
||||||
// Also show mxid if there are other people with the same or similar
|
|
||||||
// displayname, after hidden character removal.
|
|
||||||
const userIds = roomState.getUserIdsWithDisplayName(displayName);
|
|
||||||
if (userIds.some((u) => u !== selfUserId)) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateDisplayName(selfUserId, displayName, roomState, disambiguate) {
|
|
||||||
if (disambiguate) return displayName + " (" + selfUserId + ")";
|
|
||||||
|
|
||||||
if (!displayName || displayName === selfUserId) return selfUserId;
|
|
||||||
|
|
||||||
// First check if the displayname is something we consider truthy
|
|
||||||
// after stripping it of zero width characters and padding spaces
|
|
||||||
if (!utils.removeHiddenChars(displayName)) return selfUserId;
|
|
||||||
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires whenever any room member's name changes.
|
|
||||||
* @event module:client~MatrixClient#"RoomMember.name"
|
|
||||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
|
||||||
* @param {RoomMember} member The member whose RoomMember.name changed.
|
|
||||||
* @param {string?} oldName The previous name. Null if the member didn't have a
|
|
||||||
* name previously.
|
|
||||||
* @example
|
|
||||||
* matrixClient.on("RoomMember.name", function(event, member){
|
|
||||||
* var newName = member.name;
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires whenever any room member's membership state changes.
|
|
||||||
* @event module:client~MatrixClient#"RoomMember.membership"
|
|
||||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
|
||||||
* @param {RoomMember} member The member whose RoomMember.membership changed.
|
|
||||||
* @param {string?} oldMembership The previous membership state. Null if it's a
|
|
||||||
* new member.
|
|
||||||
* @example
|
|
||||||
* matrixClient.on("RoomMember.membership", function(event, member, oldMembership){
|
|
||||||
* var newState = member.membership;
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires whenever any room member's typing state changes.
|
|
||||||
* @event module:client~MatrixClient#"RoomMember.typing"
|
|
||||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
|
||||||
* @param {RoomMember} member The member whose RoomMember.typing changed.
|
|
||||||
* @example
|
|
||||||
* matrixClient.on("RoomMember.typing", function(event, member){
|
|
||||||
* var isTyping = member.typing;
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires whenever any room member's power level changes.
|
|
||||||
* @event module:client~MatrixClient#"RoomMember.powerLevel"
|
|
||||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
|
||||||
* @param {RoomMember} member The member whose RoomMember.powerLevel changed.
|
|
||||||
* @example
|
|
||||||
* matrixClient.on("RoomMember.powerLevel", function(event, member){
|
|
||||||
* var newPowerLevel = member.powerLevel;
|
|
||||||
* var newNormPowerLevel = member.powerLevelNorm;
|
|
||||||
* });
|
|
||||||
*/
|
|
411
src/models/room-member.ts
Normal file
411
src/models/room-member.ts
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 - 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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";
|
||||||
|
|
||||||
|
export class RoomMember extends EventEmitter {
|
||||||
|
private _isOutOfBand = false;
|
||||||
|
private _modified: number;
|
||||||
|
|
||||||
|
// XXX these should be read-only
|
||||||
|
public typing = false;
|
||||||
|
public name: string;
|
||||||
|
public rawDisplayName: string;
|
||||||
|
public powerLevel = 0;
|
||||||
|
public powerLevelNorm = 0;
|
||||||
|
public user?: User = null;
|
||||||
|
public membership: string = null;
|
||||||
|
public disambiguate = false;
|
||||||
|
public events: {
|
||||||
|
member?: MatrixEvent;
|
||||||
|
} = {
|
||||||
|
member: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new room member.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @alias module:models/room-member
|
||||||
|
*
|
||||||
|
* @param {string} roomId The room ID of the member.
|
||||||
|
* @param {string} userId The user ID of the member.
|
||||||
|
* @prop {string} roomId The room ID for this member.
|
||||||
|
* @prop {string} userId The user ID of this member.
|
||||||
|
* @prop {boolean} typing True if the room member is currently typing.
|
||||||
|
* @prop {string} name The human-readable name for this room member. This will be
|
||||||
|
* disambiguated with a suffix of " (@user_id:matrix.org)" if another member shares the
|
||||||
|
* same displayname.
|
||||||
|
* @prop {string} rawDisplayName The ambiguous displayname of this room member.
|
||||||
|
* @prop {Number} powerLevel The power level for this room member.
|
||||||
|
* @prop {Number} powerLevelNorm The normalised power level (0-100) for this
|
||||||
|
* room member.
|
||||||
|
* @prop {User} user The User object for this room member, if one exists.
|
||||||
|
* @prop {string} membership The membership state for this room member e.g. 'join'.
|
||||||
|
* @prop {Object} events The events describing this RoomMember.
|
||||||
|
* @prop {MatrixEvent} events.member The m.room.member event for this RoomMember.
|
||||||
|
* @prop {boolean} disambiguate True if the member's name is disambiguated.
|
||||||
|
*/
|
||||||
|
constructor(public readonly roomId: string, public readonly userId: string) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = userId;
|
||||||
|
this.rawDisplayName = userId;
|
||||||
|
this.updateModifiedTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the member as coming from a channel that is not sync
|
||||||
|
*/
|
||||||
|
public markOutOfBand(): void {
|
||||||
|
this._isOutOfBand = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean} does the member come from a channel that is not sync?
|
||||||
|
* This is used to store the member seperately
|
||||||
|
* from the sync state so it available across browser sessions.
|
||||||
|
*/
|
||||||
|
public isOutOfBand(): boolean {
|
||||||
|
return this._isOutOfBand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update this room member's membership event. May fire "RoomMember.name" if
|
||||||
|
* this event updates this member's name.
|
||||||
|
* @param {MatrixEvent} event The <code>m.room.member</code> event
|
||||||
|
* @param {RoomState} roomState Optional. The room state to take into account
|
||||||
|
* when calculating (e.g. for disambiguating users with the same name).
|
||||||
|
* @fires module:client~MatrixClient#event:"RoomMember.name"
|
||||||
|
* @fires module:client~MatrixClient#event:"RoomMember.membership"
|
||||||
|
*/
|
||||||
|
public setMembershipEvent(event: MatrixEvent, roomState: RoomState): void {
|
||||||
|
const displayName = event.getDirectionalContent().displayname;
|
||||||
|
|
||||||
|
if (event.getType() !== "m.room.member") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._isOutOfBand = false;
|
||||||
|
|
||||||
|
this.events.member = event;
|
||||||
|
|
||||||
|
const oldMembership = this.membership;
|
||||||
|
this.membership = event.getDirectionalContent().membership;
|
||||||
|
|
||||||
|
this.disambiguate = shouldDisambiguate(
|
||||||
|
this.userId,
|
||||||
|
displayName,
|
||||||
|
roomState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const oldName = this.name;
|
||||||
|
this.name = calculateDisplayName(
|
||||||
|
this.userId,
|
||||||
|
displayName,
|
||||||
|
roomState,
|
||||||
|
this.disambiguate,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.rawDisplayName = event.getDirectionalContent().displayname || this.userId;
|
||||||
|
if (oldMembership !== this.membership) {
|
||||||
|
this.updateModifiedTime();
|
||||||
|
this.emit("RoomMember.membership", event, this, oldMembership);
|
||||||
|
}
|
||||||
|
if (oldName !== this.name) {
|
||||||
|
this.updateModifiedTime();
|
||||||
|
this.emit("RoomMember.name", event, this, oldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update this room member's power level event. May fire
|
||||||
|
* "RoomMember.powerLevel" if this event updates this member's power levels.
|
||||||
|
* @param {MatrixEvent} powerLevelEvent The <code>m.room.power_levels</code>
|
||||||
|
* event
|
||||||
|
* @fires module:client~MatrixClient#event:"RoomMember.powerLevel"
|
||||||
|
*/
|
||||||
|
public setPowerLevelEvent(powerLevelEvent: MatrixEvent): void {
|
||||||
|
if (powerLevelEvent.getType() !== "m.room.power_levels") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const evContent = powerLevelEvent.getDirectionalContent();
|
||||||
|
|
||||||
|
let maxLevel = evContent.users_default || 0;
|
||||||
|
const users = evContent.users || {};
|
||||||
|
Object.values(users).forEach(function(lvl: number) {
|
||||||
|
maxLevel = Math.max(maxLevel, lvl);
|
||||||
|
});
|
||||||
|
const oldPowerLevel = this.powerLevel;
|
||||||
|
const oldPowerLevelNorm = this.powerLevelNorm;
|
||||||
|
|
||||||
|
if (users[this.userId] !== undefined) {
|
||||||
|
this.powerLevel = users[this.userId];
|
||||||
|
} else if (evContent.users_default !== undefined) {
|
||||||
|
this.powerLevel = evContent.users_default;
|
||||||
|
} else {
|
||||||
|
this.powerLevel = 0;
|
||||||
|
}
|
||||||
|
this.powerLevelNorm = 0;
|
||||||
|
if (maxLevel > 0) {
|
||||||
|
this.powerLevelNorm = (this.powerLevel * 100) / maxLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit for changes in powerLevelNorm as well (since the app will need to
|
||||||
|
// redraw everyone's level if the max has changed)
|
||||||
|
if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) {
|
||||||
|
this.updateModifiedTime();
|
||||||
|
this.emit("RoomMember.powerLevel", powerLevelEvent, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update this room member's typing event. May fire "RoomMember.typing" if
|
||||||
|
* this event changes this member's typing state.
|
||||||
|
* @param {MatrixEvent} event The typing event
|
||||||
|
* @fires module:client~MatrixClient#event:"RoomMember.typing"
|
||||||
|
*/
|
||||||
|
public setTypingEvent(event: MatrixEvent): void {
|
||||||
|
if (event.getType() !== "m.typing") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const oldTyping = this.typing;
|
||||||
|
this.typing = false;
|
||||||
|
const typingList = event.getContent().user_ids;
|
||||||
|
if (!Array.isArray(typingList)) {
|
||||||
|
// malformed event :/ bail early. TODO: whine?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typingList.indexOf(this.userId) !== -1) {
|
||||||
|
this.typing = true;
|
||||||
|
}
|
||||||
|
if (oldTyping !== this.typing) {
|
||||||
|
this.updateModifiedTime();
|
||||||
|
this.emit("RoomMember.typing", event, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the last modified time to the current time.
|
||||||
|
*/
|
||||||
|
private updateModifiedTime() {
|
||||||
|
this._modified = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the timestamp when this RoomMember was last updated. This timestamp is
|
||||||
|
* updated when properties on this RoomMember are updated.
|
||||||
|
* It is updated <i>before</i> firing events.
|
||||||
|
* @return {number} The timestamp
|
||||||
|
*/
|
||||||
|
public getLastModifiedTime(): number {
|
||||||
|
return this._modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isKicked(): boolean {
|
||||||
|
return this.membership === "leave" &&
|
||||||
|
this.events.member.getSender() !== this.events.member.getStateKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this member was invited with the is_direct flag set, return
|
||||||
|
* the user that invited this member
|
||||||
|
* @return {string} user id of the inviter
|
||||||
|
*/
|
||||||
|
public getDMInviter(): string {
|
||||||
|
// when not available because that room state hasn't been loaded in,
|
||||||
|
// we don't really know, but more likely to not be a direct chat
|
||||||
|
if (this.events.member) {
|
||||||
|
// TODO: persist the is_direct flag on the member as more member events
|
||||||
|
// come in caused by displayName changes.
|
||||||
|
|
||||||
|
// the is_direct flag is set on the invite member event.
|
||||||
|
// This is copied on the prev_content section of the join member event
|
||||||
|
// when the invite is accepted.
|
||||||
|
|
||||||
|
const memberEvent = this.events.member;
|
||||||
|
let memberContent = memberEvent.getContent();
|
||||||
|
let inviteSender = memberEvent.getSender();
|
||||||
|
|
||||||
|
if (memberContent.membership === "join") {
|
||||||
|
memberContent = memberEvent.getPrevContent();
|
||||||
|
inviteSender = memberEvent.getUnsigned().prev_sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memberContent.membership === "invite" && memberContent.is_direct) {
|
||||||
|
return inviteSender;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the avatar URL for a room member.
|
||||||
|
* @param {string} baseUrl The base homeserver URL See
|
||||||
|
* {@link module:client~MatrixClient#getHomeserverUrl}.
|
||||||
|
* @param {Number} width The desired width of the thumbnail.
|
||||||
|
* @param {Number} height The desired height of the thumbnail.
|
||||||
|
* @param {string} resizeMethod The thumbnail resize method to use, either
|
||||||
|
* "crop" or "scale".
|
||||||
|
* @param {Boolean} allowDefault (optional) Passing false causes this method to
|
||||||
|
* return null if the user has no avatar image. Otherwise, a default image URL
|
||||||
|
* will be returned. Default: true. (Deprecated)
|
||||||
|
* @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be
|
||||||
|
* returned even if it is a direct hyperlink rather than a matrix content URL.
|
||||||
|
* If false, any non-matrix content URLs will be ignored. Setting this option to
|
||||||
|
* true will expose URLs that, if fetched, will leak information about the user
|
||||||
|
* to anyone who they share a room with.
|
||||||
|
* @return {?string} the avatar URL or null.
|
||||||
|
*/
|
||||||
|
public getAvatarUrl(
|
||||||
|
baseUrl: string,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
resizeMethod: string,
|
||||||
|
allowDefault = true,
|
||||||
|
allowDirectLinks: boolean,
|
||||||
|
): string | null {
|
||||||
|
const rawUrl = this.getMxcAvatarUrl();
|
||||||
|
|
||||||
|
if (!rawUrl && !allowDefault) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const httpUrl = getHttpUriForMxc(baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks);
|
||||||
|
if (httpUrl) {
|
||||||
|
return httpUrl;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the mxc avatar url, either from a state event, or from a lazily loaded member
|
||||||
|
* @return {string} the mxc avatar url
|
||||||
|
*/
|
||||||
|
public getMxcAvatarUrl(): string | null {
|
||||||
|
if (this.events.member) {
|
||||||
|
return this.events.member.getDirectionalContent().avatar_url;
|
||||||
|
} else if (this.user) {
|
||||||
|
return this.user.avatarUrl;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MXID_PATTERN = /@.+:.+/;
|
||||||
|
const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/;
|
||||||
|
|
||||||
|
function shouldDisambiguate(selfUserId: string, displayName: string, roomState: RoomState): boolean {
|
||||||
|
if (!displayName || displayName === selfUserId) return false;
|
||||||
|
|
||||||
|
// First check if the displayname is something we consider truthy
|
||||||
|
// after stripping it of zero width characters and padding spaces
|
||||||
|
if (!utils.removeHiddenChars(displayName)) return false;
|
||||||
|
|
||||||
|
if (!roomState) return false;
|
||||||
|
|
||||||
|
// Next check if the name contains something that look like a mxid
|
||||||
|
// If it does, it may be someone trying to impersonate someone else
|
||||||
|
// Show full mxid in this case
|
||||||
|
if (MXID_PATTERN.test(displayName)) return true;
|
||||||
|
|
||||||
|
// Also show mxid if the display name contains any LTR/RTL characters as these
|
||||||
|
// make it very difficult for us to find similar *looking* display names
|
||||||
|
// E.g "Mark" could be cloned by writing "kraM" but in RTL.
|
||||||
|
if (LTR_RTL_PATTERN.test(displayName)) return true;
|
||||||
|
|
||||||
|
// Also show mxid if there are other people with the same or similar
|
||||||
|
// displayname, after hidden character removal.
|
||||||
|
const userIds = roomState.getUserIdsWithDisplayName(displayName);
|
||||||
|
if (userIds.some((u) => u !== selfUserId)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateDisplayName(
|
||||||
|
selfUserId: string,
|
||||||
|
displayName: string,
|
||||||
|
roomState: RoomState,
|
||||||
|
disambiguate: boolean,
|
||||||
|
): string {
|
||||||
|
if (disambiguate) return displayName + " (" + selfUserId + ")";
|
||||||
|
|
||||||
|
if (!displayName || displayName === selfUserId) return selfUserId;
|
||||||
|
|
||||||
|
// First check if the displayname is something we consider truthy
|
||||||
|
// after stripping it of zero width characters and padding spaces
|
||||||
|
if (!utils.removeHiddenChars(displayName)) return selfUserId;
|
||||||
|
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires whenever any room member's name changes.
|
||||||
|
* @event module:client~MatrixClient#"RoomMember.name"
|
||||||
|
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||||
|
* @param {RoomMember} member The member whose RoomMember.name changed.
|
||||||
|
* @param {string?} oldName The previous name. Null if the member didn't have a
|
||||||
|
* name previously.
|
||||||
|
* @example
|
||||||
|
* matrixClient.on("RoomMember.name", function(event, member){
|
||||||
|
* var newName = member.name;
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires whenever any room member's membership state changes.
|
||||||
|
* @event module:client~MatrixClient#"RoomMember.membership"
|
||||||
|
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||||
|
* @param {RoomMember} member The member whose RoomMember.membership changed.
|
||||||
|
* @param {string?} oldMembership The previous membership state. Null if it's a
|
||||||
|
* new member.
|
||||||
|
* @example
|
||||||
|
* matrixClient.on("RoomMember.membership", function(event, member, oldMembership){
|
||||||
|
* var newState = member.membership;
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires whenever any room member's typing state changes.
|
||||||
|
* @event module:client~MatrixClient#"RoomMember.typing"
|
||||||
|
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||||
|
* @param {RoomMember} member The member whose RoomMember.typing changed.
|
||||||
|
* @example
|
||||||
|
* matrixClient.on("RoomMember.typing", function(event, member){
|
||||||
|
* var isTyping = member.typing;
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires whenever any room member's power level changes.
|
||||||
|
* @event module:client~MatrixClient#"RoomMember.powerLevel"
|
||||||
|
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||||
|
* @param {RoomMember} member The member whose RoomMember.powerLevel changed.
|
||||||
|
* @example
|
||||||
|
* matrixClient.on("RoomMember.powerLevel", function(event, member){
|
||||||
|
* var newPowerLevel = member.powerLevel;
|
||||||
|
* var newNormPowerLevel = member.powerLevelNorm;
|
||||||
|
* });
|
||||||
|
*/
|
@@ -1,6 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -19,6 +18,14 @@ limitations under the License.
|
|||||||
* @module models/room-summary
|
* @module models/room-summary
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
interface IInfo {
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
numMembers: number;
|
||||||
|
aliases: string[];
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a new Room Summary. A summary can be used for display on a recent
|
* Construct a new Room Summary. A summary can be used for display on a recent
|
||||||
* list, without having to load the entire room list into memory.
|
* list, without having to load the entire room list into memory.
|
||||||
@@ -32,8 +39,7 @@ limitations under the License.
|
|||||||
* @param {string[]} info.aliases The list of aliases for this room.
|
* @param {string[]} info.aliases The list of aliases for this room.
|
||||||
* @param {Number} info.timestamp The timestamp for this room.
|
* @param {Number} info.timestamp The timestamp for this room.
|
||||||
*/
|
*/
|
||||||
export function RoomSummary(roomId, info) {
|
export class RoomSummary {
|
||||||
this.roomId = roomId;
|
constructor(public readonly roomId: string, info?: IInfo) {}
|
||||||
this.info = info;
|
|
||||||
}
|
}
|
||||||
|
|
@@ -1,260 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module models/user
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as utils from "../utils";
|
|
||||||
import { EventEmitter } from "events";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct a new User. A User must have an ID and can optionally have extra
|
|
||||||
* information associated with it.
|
|
||||||
* @constructor
|
|
||||||
* @param {string} userId Required. The ID of this user.
|
|
||||||
* @prop {string} userId The ID of the user.
|
|
||||||
* @prop {Object} info The info object supplied in the constructor.
|
|
||||||
* @prop {string} displayName The 'displayname' of the user if known.
|
|
||||||
* @prop {string} avatarUrl The 'avatar_url' of the user if known.
|
|
||||||
* @prop {string} presence The presence enum if known.
|
|
||||||
* @prop {string} presenceStatusMsg The presence status message if known.
|
|
||||||
* @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted
|
|
||||||
* proactively with the server, or we saw a message from the user
|
|
||||||
* @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last
|
|
||||||
* received presence data for this user. We can subtract
|
|
||||||
* lastActiveAgo from this to approximate an absolute value for
|
|
||||||
* when a user was last active.
|
|
||||||
* @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be
|
|
||||||
* an approximation and that the user should be seen as active 'now'
|
|
||||||
* @prop {string} _unstable_statusMessage The status message for the user, if known. This is
|
|
||||||
* different from the presenceStatusMsg in that this is not tied to
|
|
||||||
* the user's presence, and should be represented differently.
|
|
||||||
* @prop {Object} events The events describing this user.
|
|
||||||
* @prop {MatrixEvent} events.presence The m.presence event for this user.
|
|
||||||
*/
|
|
||||||
export function User(userId) {
|
|
||||||
this.userId = userId;
|
|
||||||
this.presence = "offline";
|
|
||||||
this.presenceStatusMsg = null;
|
|
||||||
this._unstable_statusMessage = "";
|
|
||||||
this.displayName = userId;
|
|
||||||
this.rawDisplayName = userId;
|
|
||||||
this.avatarUrl = null;
|
|
||||||
this.lastActiveAgo = 0;
|
|
||||||
this.lastPresenceTs = 0;
|
|
||||||
this.currentlyActive = false;
|
|
||||||
this.events = {
|
|
||||||
presence: null,
|
|
||||||
profile: null,
|
|
||||||
};
|
|
||||||
this._updateModifiedTime();
|
|
||||||
}
|
|
||||||
utils.inherits(User, EventEmitter);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update this User with the given presence event. May fire "User.presence",
|
|
||||||
* "User.avatarUrl" and/or "User.displayName" if this event updates this user's
|
|
||||||
* properties.
|
|
||||||
* @param {MatrixEvent} event The <code>m.presence</code> event.
|
|
||||||
* @fires module:client~MatrixClient#event:"User.presence"
|
|
||||||
* @fires module:client~MatrixClient#event:"User.displayName"
|
|
||||||
* @fires module:client~MatrixClient#event:"User.avatarUrl"
|
|
||||||
*/
|
|
||||||
User.prototype.setPresenceEvent = function(event) {
|
|
||||||
if (event.getType() !== "m.presence") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const firstFire = this.events.presence === null;
|
|
||||||
this.events.presence = event;
|
|
||||||
|
|
||||||
const eventsToFire = [];
|
|
||||||
if (event.getContent().presence !== this.presence || firstFire) {
|
|
||||||
eventsToFire.push("User.presence");
|
|
||||||
}
|
|
||||||
if (event.getContent().avatar_url &&
|
|
||||||
event.getContent().avatar_url !== this.avatarUrl) {
|
|
||||||
eventsToFire.push("User.avatarUrl");
|
|
||||||
}
|
|
||||||
if (event.getContent().displayname &&
|
|
||||||
event.getContent().displayname !== this.displayName) {
|
|
||||||
eventsToFire.push("User.displayName");
|
|
||||||
}
|
|
||||||
if (event.getContent().currently_active !== undefined &&
|
|
||||||
event.getContent().currently_active !== this.currentlyActive) {
|
|
||||||
eventsToFire.push("User.currentlyActive");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.presence = event.getContent().presence;
|
|
||||||
eventsToFire.push("User.lastPresenceTs");
|
|
||||||
|
|
||||||
if (event.getContent().status_msg) {
|
|
||||||
this.presenceStatusMsg = event.getContent().status_msg;
|
|
||||||
}
|
|
||||||
if (event.getContent().displayname) {
|
|
||||||
this.displayName = event.getContent().displayname;
|
|
||||||
}
|
|
||||||
if (event.getContent().avatar_url) {
|
|
||||||
this.avatarUrl = event.getContent().avatar_url;
|
|
||||||
}
|
|
||||||
this.lastActiveAgo = event.getContent().last_active_ago;
|
|
||||||
this.lastPresenceTs = Date.now();
|
|
||||||
this.currentlyActive = event.getContent().currently_active;
|
|
||||||
|
|
||||||
this._updateModifiedTime();
|
|
||||||
|
|
||||||
for (let i = 0; i < eventsToFire.length; i++) {
|
|
||||||
this.emit(eventsToFire[i], event, this);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manually set this user's display name. No event is emitted in response to this
|
|
||||||
* as there is no underlying MatrixEvent to emit with.
|
|
||||||
* @param {string} name The new display name.
|
|
||||||
*/
|
|
||||||
User.prototype.setDisplayName = function(name) {
|
|
||||||
const oldName = this.displayName;
|
|
||||||
if (typeof name === "string") {
|
|
||||||
this.displayName = name;
|
|
||||||
} else {
|
|
||||||
this.displayName = undefined;
|
|
||||||
}
|
|
||||||
if (name !== oldName) {
|
|
||||||
this._updateModifiedTime();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manually set this user's non-disambiguated display name. No event is emitted
|
|
||||||
* in response to this as there is no underlying MatrixEvent to emit with.
|
|
||||||
* @param {string} name The new display name.
|
|
||||||
*/
|
|
||||||
User.prototype.setRawDisplayName = function(name) {
|
|
||||||
if (typeof name === "string") {
|
|
||||||
this.rawDisplayName = name;
|
|
||||||
} else {
|
|
||||||
this.rawDisplayName = undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manually set this user's avatar URL. No event is emitted in response to this
|
|
||||||
* as there is no underlying MatrixEvent to emit with.
|
|
||||||
* @param {string} url The new avatar URL.
|
|
||||||
*/
|
|
||||||
User.prototype.setAvatarUrl = function(url) {
|
|
||||||
const oldUrl = this.avatarUrl;
|
|
||||||
this.avatarUrl = url;
|
|
||||||
if (url !== oldUrl) {
|
|
||||||
this._updateModifiedTime();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the last modified time to the current time.
|
|
||||||
*/
|
|
||||||
User.prototype._updateModifiedTime = function() {
|
|
||||||
this._modified = Date.now();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the timestamp when this User was last updated. This timestamp is
|
|
||||||
* updated when this User receives a new Presence event which has updated a
|
|
||||||
* property on this object. It is updated <i>before</i> firing events.
|
|
||||||
* @return {number} The timestamp
|
|
||||||
*/
|
|
||||||
User.prototype.getLastModifiedTime = function() {
|
|
||||||
return this._modified;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the absolute timestamp when this User was last known active on the server.
|
|
||||||
* It is *NOT* accurate if this.currentlyActive is true.
|
|
||||||
* @return {number} The timestamp
|
|
||||||
*/
|
|
||||||
User.prototype.getLastActiveTs = function() {
|
|
||||||
return this.lastPresenceTs - this.lastActiveAgo;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manually set the user's status message.
|
|
||||||
* @param {MatrixEvent} event The <code>im.vector.user_status</code> event.
|
|
||||||
* @fires module:client~MatrixClient#event:"User._unstable_statusMessage"
|
|
||||||
*/
|
|
||||||
User.prototype._unstable_updateStatusMessage = function(event) {
|
|
||||||
if (!event.getContent()) this._unstable_statusMessage = "";
|
|
||||||
else this._unstable_statusMessage = event.getContent()["status"];
|
|
||||||
this._updateModifiedTime();
|
|
||||||
this.emit("User._unstable_statusMessage", this);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires whenever any user's lastPresenceTs changes,
|
|
||||||
* ie. whenever any presence event is received for a user.
|
|
||||||
* @event module:client~MatrixClient#"User.lastPresenceTs"
|
|
||||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
|
||||||
* @param {User} user The user whose User.lastPresenceTs changed.
|
|
||||||
* @example
|
|
||||||
* matrixClient.on("User.lastPresenceTs", function(event, user){
|
|
||||||
* var newlastPresenceTs = user.lastPresenceTs;
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires whenever any user's presence changes.
|
|
||||||
* @event module:client~MatrixClient#"User.presence"
|
|
||||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
|
||||||
* @param {User} user The user whose User.presence changed.
|
|
||||||
* @example
|
|
||||||
* matrixClient.on("User.presence", function(event, user){
|
|
||||||
* var newPresence = user.presence;
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires whenever any user's currentlyActive changes.
|
|
||||||
* @event module:client~MatrixClient#"User.currentlyActive"
|
|
||||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
|
||||||
* @param {User} user The user whose User.currentlyActive changed.
|
|
||||||
* @example
|
|
||||||
* matrixClient.on("User.currentlyActive", function(event, user){
|
|
||||||
* var newCurrentlyActive = user.currentlyActive;
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires whenever any user's display name changes.
|
|
||||||
* @event module:client~MatrixClient#"User.displayName"
|
|
||||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
|
||||||
* @param {User} user The user whose User.displayName changed.
|
|
||||||
* @example
|
|
||||||
* matrixClient.on("User.displayName", function(event, user){
|
|
||||||
* var newName = user.displayName;
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires whenever any user's avatar URL changes.
|
|
||||||
* @event module:client~MatrixClient#"User.avatarUrl"
|
|
||||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
|
||||||
* @param {User} user The user whose User.avatarUrl changed.
|
|
||||||
* @example
|
|
||||||
* matrixClient.on("User.avatarUrl", function(event, user){
|
|
||||||
* var newUrl = user.avatarUrl;
|
|
||||||
* });
|
|
||||||
*/
|
|
274
src/models/user.ts
Normal file
274
src/models/user.ts
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 - 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module models/user
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
import { MatrixEvent } from "./event";
|
||||||
|
|
||||||
|
export class User extends EventEmitter {
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
private modified: number;
|
||||||
|
|
||||||
|
// XXX these should be read-only
|
||||||
|
public displayName: string;
|
||||||
|
public rawDisplayName: string;
|
||||||
|
public avatarUrl: string;
|
||||||
|
public presenceStatusMsg: string = null;
|
||||||
|
public presence = "offline";
|
||||||
|
public lastActiveAgo = 0;
|
||||||
|
public lastPresenceTs = 0;
|
||||||
|
public currentlyActive = false;
|
||||||
|
public events: {
|
||||||
|
presence?: MatrixEvent;
|
||||||
|
profile?: MatrixEvent;
|
||||||
|
} = {
|
||||||
|
presence: null,
|
||||||
|
profile: null,
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
public unstable_statusMessage = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new User. A User must have an ID and can optionally have extra
|
||||||
|
* information associated with it.
|
||||||
|
* @constructor
|
||||||
|
* @param {string} userId Required. The ID of this user.
|
||||||
|
* @prop {string} userId The ID of the user.
|
||||||
|
* @prop {Object} info The info object supplied in the constructor.
|
||||||
|
* @prop {string} displayName The 'displayname' of the user if known.
|
||||||
|
* @prop {string} avatarUrl The 'avatar_url' of the user if known.
|
||||||
|
* @prop {string} presence The presence enum if known.
|
||||||
|
* @prop {string} presenceStatusMsg The presence status message if known.
|
||||||
|
* @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted
|
||||||
|
* proactively with the server, or we saw a message from the user
|
||||||
|
* @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last
|
||||||
|
* received presence data for this user. We can subtract
|
||||||
|
* lastActiveAgo from this to approximate an absolute value for
|
||||||
|
* when a user was last active.
|
||||||
|
* @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be
|
||||||
|
* an approximation and that the user should be seen as active 'now'
|
||||||
|
* @prop {string} unstable_statusMessage The status message for the user, if known. This is
|
||||||
|
* different from the presenceStatusMsg in that this is not tied to
|
||||||
|
* the user's presence, and should be represented differently.
|
||||||
|
* @prop {Object} events The events describing this user.
|
||||||
|
* @prop {MatrixEvent} events.presence The m.presence event for this user.
|
||||||
|
*/
|
||||||
|
constructor(public readonly userId: string) {
|
||||||
|
super();
|
||||||
|
this.displayName = userId;
|
||||||
|
this.rawDisplayName = userId;
|
||||||
|
this.avatarUrl = null;
|
||||||
|
this.updateModifiedTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update this User with the given presence event. May fire "User.presence",
|
||||||
|
* "User.avatarUrl" and/or "User.displayName" if this event updates this user's
|
||||||
|
* properties.
|
||||||
|
* @param {MatrixEvent} event The <code>m.presence</code> event.
|
||||||
|
* @fires module:client~MatrixClient#event:"User.presence"
|
||||||
|
* @fires module:client~MatrixClient#event:"User.displayName"
|
||||||
|
* @fires module:client~MatrixClient#event:"User.avatarUrl"
|
||||||
|
*/
|
||||||
|
public setPresenceEvent(event: MatrixEvent): void {
|
||||||
|
if (event.getType() !== "m.presence") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const firstFire = this.events.presence === null;
|
||||||
|
this.events.presence = event;
|
||||||
|
|
||||||
|
const eventsToFire = [];
|
||||||
|
if (event.getContent().presence !== this.presence || firstFire) {
|
||||||
|
eventsToFire.push("User.presence");
|
||||||
|
}
|
||||||
|
if (event.getContent().avatar_url &&
|
||||||
|
event.getContent().avatar_url !== this.avatarUrl) {
|
||||||
|
eventsToFire.push("User.avatarUrl");
|
||||||
|
}
|
||||||
|
if (event.getContent().displayname &&
|
||||||
|
event.getContent().displayname !== this.displayName) {
|
||||||
|
eventsToFire.push("User.displayName");
|
||||||
|
}
|
||||||
|
if (event.getContent().currently_active !== undefined &&
|
||||||
|
event.getContent().currently_active !== this.currentlyActive) {
|
||||||
|
eventsToFire.push("User.currentlyActive");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.presence = event.getContent().presence;
|
||||||
|
eventsToFire.push("User.lastPresenceTs");
|
||||||
|
|
||||||
|
if (event.getContent().status_msg) {
|
||||||
|
this.presenceStatusMsg = event.getContent().status_msg;
|
||||||
|
}
|
||||||
|
if (event.getContent().displayname) {
|
||||||
|
this.displayName = event.getContent().displayname;
|
||||||
|
}
|
||||||
|
if (event.getContent().avatar_url) {
|
||||||
|
this.avatarUrl = event.getContent().avatar_url;
|
||||||
|
}
|
||||||
|
this.lastActiveAgo = event.getContent().last_active_ago;
|
||||||
|
this.lastPresenceTs = Date.now();
|
||||||
|
this.currentlyActive = event.getContent().currently_active;
|
||||||
|
|
||||||
|
this.updateModifiedTime();
|
||||||
|
|
||||||
|
for (let i = 0; i < eventsToFire.length; i++) {
|
||||||
|
this.emit(eventsToFire[i], event, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually set this user's display name. No event is emitted in response to this
|
||||||
|
* as there is no underlying MatrixEvent to emit with.
|
||||||
|
* @param {string} name The new display name.
|
||||||
|
*/
|
||||||
|
public setDisplayName(name: string): void {
|
||||||
|
const oldName = this.displayName;
|
||||||
|
if (typeof name === "string") {
|
||||||
|
this.displayName = name;
|
||||||
|
} else {
|
||||||
|
this.displayName = undefined;
|
||||||
|
}
|
||||||
|
if (name !== oldName) {
|
||||||
|
this.updateModifiedTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually set this user's non-disambiguated display name. No event is emitted
|
||||||
|
* in response to this as there is no underlying MatrixEvent to emit with.
|
||||||
|
* @param {string} name The new display name.
|
||||||
|
*/
|
||||||
|
public setRawDisplayName(name: string): void {
|
||||||
|
if (typeof name === "string") {
|
||||||
|
this.rawDisplayName = name;
|
||||||
|
} else {
|
||||||
|
this.rawDisplayName = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually set this user's avatar URL. No event is emitted in response to this
|
||||||
|
* as there is no underlying MatrixEvent to emit with.
|
||||||
|
* @param {string} url The new avatar URL.
|
||||||
|
*/
|
||||||
|
public setAvatarUrl(url: string): void {
|
||||||
|
const oldUrl = this.avatarUrl;
|
||||||
|
this.avatarUrl = url;
|
||||||
|
if (url !== oldUrl) {
|
||||||
|
this.updateModifiedTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the last modified time to the current time.
|
||||||
|
*/
|
||||||
|
private updateModifiedTime(): void {
|
||||||
|
this.modified = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the timestamp when this User was last updated. This timestamp is
|
||||||
|
* updated when this User receives a new Presence event which has updated a
|
||||||
|
* property on this object. It is updated <i>before</i> firing events.
|
||||||
|
* @return {number} The timestamp
|
||||||
|
*/
|
||||||
|
public getLastModifiedTime(): number {
|
||||||
|
return this.modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the absolute timestamp when this User was last known active on the server.
|
||||||
|
* It is *NOT* accurate if this.currentlyActive is true.
|
||||||
|
* @return {number} The timestamp
|
||||||
|
*/
|
||||||
|
public getLastActiveTs(): number {
|
||||||
|
return this.lastPresenceTs - this.lastActiveAgo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually set the user's status message.
|
||||||
|
* @param {MatrixEvent} event The <code>im.vector.user_status</code> event.
|
||||||
|
* @fires module:client~MatrixClient#event:"User.unstable_statusMessage"
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
public unstable_updateStatusMessage(event: MatrixEvent): void {
|
||||||
|
if (!event.getContent()) this.unstable_statusMessage = "";
|
||||||
|
else this.unstable_statusMessage = event.getContent()["status"];
|
||||||
|
this.updateModifiedTime();
|
||||||
|
this.emit("User.unstable_statusMessage", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires whenever any user's lastPresenceTs changes,
|
||||||
|
* ie. whenever any presence event is received for a user.
|
||||||
|
* @event module:client~MatrixClient#"User.lastPresenceTs"
|
||||||
|
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||||
|
* @param {User} user The user whose User.lastPresenceTs changed.
|
||||||
|
* @example
|
||||||
|
* matrixClient.on("User.lastPresenceTs", function(event, user){
|
||||||
|
* var newlastPresenceTs = user.lastPresenceTs;
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires whenever any user's presence changes.
|
||||||
|
* @event module:client~MatrixClient#"User.presence"
|
||||||
|
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||||
|
* @param {User} user The user whose User.presence changed.
|
||||||
|
* @example
|
||||||
|
* matrixClient.on("User.presence", function(event, user){
|
||||||
|
* var newPresence = user.presence;
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires whenever any user's currentlyActive changes.
|
||||||
|
* @event module:client~MatrixClient#"User.currentlyActive"
|
||||||
|
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||||
|
* @param {User} user The user whose User.currentlyActive changed.
|
||||||
|
* @example
|
||||||
|
* matrixClient.on("User.currentlyActive", function(event, user){
|
||||||
|
* var newCurrentlyActive = user.currentlyActive;
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires whenever any user's display name changes.
|
||||||
|
* @event module:client~MatrixClient#"User.displayName"
|
||||||
|
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||||
|
* @param {User} user The user whose User.displayName changed.
|
||||||
|
* @example
|
||||||
|
* matrixClient.on("User.displayName", function(event, user){
|
||||||
|
* var newName = user.displayName;
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires whenever any user's avatar URL changes.
|
||||||
|
* @event module:client~MatrixClient#"User.avatarUrl"
|
||||||
|
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||||
|
* @param {User} user The user whose User.avatarUrl changed.
|
||||||
|
* @example
|
||||||
|
* matrixClient.on("User.avatarUrl", function(event, user){
|
||||||
|
* var newUrl = user.avatarUrl;
|
||||||
|
* });
|
||||||
|
*/
|
226
src/store/index.ts
Normal file
226
src/store/index.ts
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventType } from "../@types/event";
|
||||||
|
import { Group } from "../models/group";
|
||||||
|
import { Room } from "../models/room";
|
||||||
|
import { User } from "../models/user";
|
||||||
|
import { MatrixEvent } from "../models/event";
|
||||||
|
import { Filter } from "../filter";
|
||||||
|
import { RoomSummary } from "../models/room-summary";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a stub store. This does no-ops on most store methods.
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export interface IStore {
|
||||||
|
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
||||||
|
isNewlyCreated(): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the sync token.
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
getSyncToken(): string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the sync token.
|
||||||
|
* @param {string} token
|
||||||
|
*/
|
||||||
|
setSyncToken(token: string);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op.
|
||||||
|
* @param {Group} group
|
||||||
|
*/
|
||||||
|
storeGroup(group: Group);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op.
|
||||||
|
* @param {string} groupId
|
||||||
|
* @return {null}
|
||||||
|
*/
|
||||||
|
getGroup(groupId: string): Group | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op.
|
||||||
|
* @return {Array} An empty array.
|
||||||
|
*/
|
||||||
|
getGroups(): Group[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op.
|
||||||
|
* @param {Room} room
|
||||||
|
*/
|
||||||
|
storeRoom(room: Room);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op.
|
||||||
|
* @param {string} roomId
|
||||||
|
* @return {null}
|
||||||
|
*/
|
||||||
|
getRoom(roomId: string): Room | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op.
|
||||||
|
* @return {Array} An empty array.
|
||||||
|
*/
|
||||||
|
getRooms(): Room[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permanently delete a room.
|
||||||
|
* @param {string} roomId
|
||||||
|
*/
|
||||||
|
removeRoom(roomId: string);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op.
|
||||||
|
* @return {Array} An empty array.
|
||||||
|
*/
|
||||||
|
getRoomSummaries(): RoomSummary[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op.
|
||||||
|
* @param {User} user
|
||||||
|
*/
|
||||||
|
storeUser(user: User);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op.
|
||||||
|
* @param {string} userId
|
||||||
|
* @return {null}
|
||||||
|
*/
|
||||||
|
getUser(userId: string): User | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op.
|
||||||
|
* @return {User[]}
|
||||||
|
*/
|
||||||
|
getUsers(): User[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op.
|
||||||
|
* @param {Room} room
|
||||||
|
* @param {integer} limit
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
|
scrollback(room: Room, limit: number): MatrixEvent[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store events for a room.
|
||||||
|
* @param {Room} room The room to store events for.
|
||||||
|
* @param {Array<MatrixEvent>} events The events to store.
|
||||||
|
* @param {string} token The token associated with these events.
|
||||||
|
* @param {boolean} toStart True if these are paginated results.
|
||||||
|
*/
|
||||||
|
storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a filter.
|
||||||
|
* @param {Filter} filter
|
||||||
|
*/
|
||||||
|
storeFilter(filter: Filter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a filter.
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {string} filterId
|
||||||
|
* @return {?Filter} A filter or null.
|
||||||
|
*/
|
||||||
|
getFilter(userId: string, filterId: string): Filter | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a filter ID with the given name.
|
||||||
|
* @param {string} filterName The filter name.
|
||||||
|
* @return {?string} The filter ID or null.
|
||||||
|
*/
|
||||||
|
getFilterIdByName(filterName: string): string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a filter name to ID mapping.
|
||||||
|
* @param {string} filterName
|
||||||
|
* @param {string} filterId
|
||||||
|
*/
|
||||||
|
setFilterIdByName(filterName: string, filterId: string);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store user-scoped account data events
|
||||||
|
* @param {Array<MatrixEvent>} events The events to store.
|
||||||
|
*/
|
||||||
|
storeAccountDataEvents(events: MatrixEvent[]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get account data event by event type
|
||||||
|
* @param {string} eventType The event type being queried
|
||||||
|
*/
|
||||||
|
getAccountData(eventType: EventType | string): MatrixEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setSyncData does nothing as there is no backing data store.
|
||||||
|
*
|
||||||
|
* @param {Object} syncData The sync data
|
||||||
|
* @return {Promise} An immediately resolved promise.
|
||||||
|
*/
|
||||||
|
setSyncData(syncData: object): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We never want to save because we have nothing to save to.
|
||||||
|
*
|
||||||
|
* @return {boolean} If the store wants to save
|
||||||
|
*/
|
||||||
|
wantsSave(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save does nothing as there is no backing data store.
|
||||||
|
*/
|
||||||
|
save(force: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startup does nothing.
|
||||||
|
* @return {Promise} An immediately resolved promise.
|
||||||
|
*/
|
||||||
|
startup(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise} Resolves with a sync response to restore the
|
||||||
|
* client state to where it was at the last save, or null if there
|
||||||
|
* is no saved sync data.
|
||||||
|
*/
|
||||||
|
getSavedSync(): Promise<object>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise} If there is a saved sync, the nextBatch token
|
||||||
|
* for this sync, otherwise null.
|
||||||
|
*/
|
||||||
|
getSavedSyncToken(): Promise<string | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all data from this store. Does nothing since this store
|
||||||
|
* doesn't store anything.
|
||||||
|
* @return {Promise} An immediately resolved promise.
|
||||||
|
*/
|
||||||
|
deleteAllData(): Promise<void>;
|
||||||
|
|
||||||
|
getOutOfBandMembers(roomId: string): Promise<MatrixEvent[] | null>;
|
||||||
|
|
||||||
|
setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void>;
|
||||||
|
|
||||||
|
clearOutOfBandMembers(roomId: string): Promise<void>;
|
||||||
|
|
||||||
|
getClientOptions(): Promise<object>;
|
||||||
|
|
||||||
|
storeClientOptions(options: object): Promise<void>;
|
||||||
|
}
|
@@ -1,319 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* eslint-disable @babel/no-invalid-this */
|
|
||||||
|
|
||||||
import { MemoryStore } from "./memory";
|
|
||||||
import * as utils from "../utils";
|
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend.js";
|
|
||||||
import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend.js";
|
|
||||||
import { User } from "../models/user";
|
|
||||||
import { MatrixEvent } from "../models/event";
|
|
||||||
import { logger } from '../logger';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an internal module. See {@link IndexedDBStore} for the public class.
|
|
||||||
* @module store/indexeddb
|
|
||||||
*/
|
|
||||||
|
|
||||||
// If this value is too small we'll be writing very often which will cause
|
|
||||||
// noticable stop-the-world pauses. If this value is too big we'll be writing
|
|
||||||
// so infrequently that the /sync size gets bigger on reload. Writing more
|
|
||||||
// often does not affect the length of the pause since the entire /sync
|
|
||||||
// response is persisted each time.
|
|
||||||
const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct a new Indexed Database store, which extends MemoryStore.
|
|
||||||
*
|
|
||||||
* This store functions like a MemoryStore except it periodically persists
|
|
||||||
* the contents of the store to an IndexedDB backend.
|
|
||||||
*
|
|
||||||
* All data is still kept in-memory but can be loaded from disk by calling
|
|
||||||
* <code>startup()</code>. This can make startup times quicker as a complete
|
|
||||||
* sync from the server is not required. This does not reduce memory usage as all
|
|
||||||
* the data is eagerly fetched when <code>startup()</code> is called.
|
|
||||||
* <pre>
|
|
||||||
* let opts = { indexedDB: window.indexedDB, localStorage: window.localStorage };
|
|
||||||
* let store = new IndexedDBStore(opts);
|
|
||||||
* await store.startup(); // load from indexed db
|
|
||||||
* let client = sdk.createClient({
|
|
||||||
* store: store,
|
|
||||||
* });
|
|
||||||
* client.startClient();
|
|
||||||
* client.on("sync", function(state, prevState, data) {
|
|
||||||
* if (state === "PREPARED") {
|
|
||||||
* console.log("Started up, now with go faster stripes!");
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
* @extends MemoryStore
|
|
||||||
* @param {Object} opts Options object.
|
|
||||||
* @param {Object} opts.indexedDB The Indexed DB interface e.g.
|
|
||||||
* <code>window.indexedDB</code>
|
|
||||||
* @param {string=} opts.dbName Optional database name. The same name must be used
|
|
||||||
* to open the same database.
|
|
||||||
* @param {string=} opts.workerScript Optional URL to a script to invoke a web
|
|
||||||
* worker with to run IndexedDB queries on the web worker. The IndexedDbStoreWorker
|
|
||||||
* class is provided for this purpose and requires the application to provide a
|
|
||||||
* trivial wrapper script around it.
|
|
||||||
* @param {Object=} opts.workerApi The webWorker API object. If omitted, the global Worker
|
|
||||||
* object will be used if it exists.
|
|
||||||
* @prop {IndexedDBStoreBackend} backend The backend instance. Call through to
|
|
||||||
* this API if you need to perform specific indexeddb actions like deleting the
|
|
||||||
* database.
|
|
||||||
*/
|
|
||||||
export function IndexedDBStore(opts) {
|
|
||||||
MemoryStore.call(this, opts);
|
|
||||||
|
|
||||||
if (!opts.indexedDB) {
|
|
||||||
throw new Error('Missing required option: indexedDB');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.workerScript) {
|
|
||||||
// try & find a webworker-compatible API
|
|
||||||
let workerApi = opts.workerApi;
|
|
||||||
if (!workerApi) {
|
|
||||||
// default to the global Worker object (which is where it in a browser)
|
|
||||||
workerApi = global.Worker;
|
|
||||||
}
|
|
||||||
this.backend = new RemoteIndexedDBStoreBackend(
|
|
||||||
opts.workerScript, opts.dbName, workerApi,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.backend = new LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.startedUp = false;
|
|
||||||
this._syncTs = 0;
|
|
||||||
|
|
||||||
// Records the last-modified-time of each user at the last point we saved
|
|
||||||
// the database, such that we can derive the set if users that have been
|
|
||||||
// modified since we last saved.
|
|
||||||
this._userModifiedMap = {
|
|
||||||
// user_id : timestamp
|
|
||||||
};
|
|
||||||
}
|
|
||||||
utils.inherits(IndexedDBStore, MemoryStore);
|
|
||||||
utils.extend(IndexedDBStore.prototype, EventEmitter.prototype);
|
|
||||||
|
|
||||||
IndexedDBStore.exists = function(indexedDB, dbName) {
|
|
||||||
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {Promise} Resolved when loaded from indexed db.
|
|
||||||
*/
|
|
||||||
IndexedDBStore.prototype.startup = function() {
|
|
||||||
if (this.startedUp) {
|
|
||||||
logger.log(`IndexedDBStore.startup: already started`);
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.log(`IndexedDBStore.startup: connecting to backend`);
|
|
||||||
return this.backend.connect().then(() => {
|
|
||||||
logger.log(`IndexedDBStore.startup: loading presence events`);
|
|
||||||
return this.backend.getUserPresenceEvents();
|
|
||||||
}).then((userPresenceEvents) => {
|
|
||||||
logger.log(`IndexedDBStore.startup: processing presence events`);
|
|
||||||
userPresenceEvents.forEach(([userId, rawEvent]) => {
|
|
||||||
const u = new User(userId);
|
|
||||||
if (rawEvent) {
|
|
||||||
u.setPresenceEvent(new MatrixEvent(rawEvent));
|
|
||||||
}
|
|
||||||
this._userModifiedMap[u.userId] = u.getLastModifiedTime();
|
|
||||||
this.storeUser(u);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {Promise} Resolves with a sync response to restore the
|
|
||||||
* client state to where it was at the last save, or null if there
|
|
||||||
* is no saved sync data.
|
|
||||||
*/
|
|
||||||
IndexedDBStore.prototype.getSavedSync = degradable(function() {
|
|
||||||
return this.backend.getSavedSync();
|
|
||||||
}, "getSavedSync");
|
|
||||||
|
|
||||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
|
||||||
IndexedDBStore.prototype.isNewlyCreated = degradable(function() {
|
|
||||||
return this.backend.isNewlyCreated();
|
|
||||||
}, "isNewlyCreated");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {Promise} If there is a saved sync, the nextBatch token
|
|
||||||
* for this sync, otherwise null.
|
|
||||||
*/
|
|
||||||
IndexedDBStore.prototype.getSavedSyncToken = degradable(function() {
|
|
||||||
return this.backend.getNextBatchToken();
|
|
||||||
}, "getSavedSyncToken"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete all data from this store.
|
|
||||||
* @return {Promise} Resolves if the data was deleted from the database.
|
|
||||||
*/
|
|
||||||
IndexedDBStore.prototype.deleteAllData = degradable(function() {
|
|
||||||
MemoryStore.prototype.deleteAllData.call(this);
|
|
||||||
return this.backend.clearDatabase().then(() => {
|
|
||||||
logger.log("Deleted indexeddb data.");
|
|
||||||
}, (err) => {
|
|
||||||
logger.error(`Failed to delete indexeddb data: ${err}`);
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this store would like to save its data
|
|
||||||
* Note that obviously whether the store wants to save or
|
|
||||||
* not could change between calling this function and calling
|
|
||||||
* save().
|
|
||||||
*
|
|
||||||
* @return {boolean} True if calling save() will actually save
|
|
||||||
* (at the time this function is called).
|
|
||||||
*/
|
|
||||||
IndexedDBStore.prototype.wantsSave = function() {
|
|
||||||
const now = Date.now();
|
|
||||||
return now - this._syncTs > WRITE_DELAY_MS;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Possibly write data to the database.
|
|
||||||
*
|
|
||||||
* @param {bool} force True to force a save to happen
|
|
||||||
* @return {Promise} Promise resolves after the write completes
|
|
||||||
* (or immediately if no write is performed)
|
|
||||||
*/
|
|
||||||
IndexedDBStore.prototype.save = function(force) {
|
|
||||||
if (force || this.wantsSave()) {
|
|
||||||
return this._reallySave();
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
IndexedDBStore.prototype._reallySave = degradable(function() {
|
|
||||||
this._syncTs = Date.now(); // set now to guard against multi-writes
|
|
||||||
|
|
||||||
// work out changed users (this doesn't handle deletions but you
|
|
||||||
// can't 'delete' users as they are just presence events).
|
|
||||||
const userTuples = [];
|
|
||||||
for (const u of this.getUsers()) {
|
|
||||||
if (this._userModifiedMap[u.userId] === u.getLastModifiedTime()) continue;
|
|
||||||
if (!u.events.presence) continue;
|
|
||||||
|
|
||||||
userTuples.push([u.userId, u.events.presence.event]);
|
|
||||||
|
|
||||||
// note that we've saved this version of the user
|
|
||||||
this._userModifiedMap[u.userId] = u.getLastModifiedTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.backend.syncToDatabase(userTuples);
|
|
||||||
});
|
|
||||||
|
|
||||||
IndexedDBStore.prototype.setSyncData = degradable(function(syncData) {
|
|
||||||
return this.backend.setSyncData(syncData);
|
|
||||||
}, "setSyncData");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the out-of-band membership events for this room that
|
|
||||||
* were previously loaded.
|
|
||||||
* @param {string} roomId
|
|
||||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
|
||||||
* @returns {null} in case the members for this room haven't been stored yet
|
|
||||||
*/
|
|
||||||
IndexedDBStore.prototype.getOutOfBandMembers = degradable(function(roomId) {
|
|
||||||
return this.backend.getOutOfBandMembers(roomId);
|
|
||||||
}, "getOutOfBandMembers");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the out-of-band membership events for this room. Note that
|
|
||||||
* it still makes sense to store an empty array as the OOB status for the room is
|
|
||||||
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
|
|
||||||
* @param {string} roomId
|
|
||||||
* @param {event[]} membershipEvents the membership events to store
|
|
||||||
* @returns {Promise} when all members have been stored
|
|
||||||
*/
|
|
||||||
IndexedDBStore.prototype.setOutOfBandMembers = degradable(function(
|
|
||||||
roomId,
|
|
||||||
membershipEvents,
|
|
||||||
) {
|
|
||||||
MemoryStore.prototype.setOutOfBandMembers.call(this, roomId, membershipEvents);
|
|
||||||
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
|
|
||||||
}, "setOutOfBandMembers");
|
|
||||||
|
|
||||||
IndexedDBStore.prototype.clearOutOfBandMembers = degradable(function(roomId) {
|
|
||||||
MemoryStore.prototype.clearOutOfBandMembers.call(this);
|
|
||||||
return this.backend.clearOutOfBandMembers(roomId);
|
|
||||||
}, "clearOutOfBandMembers");
|
|
||||||
|
|
||||||
IndexedDBStore.prototype.getClientOptions = degradable(function() {
|
|
||||||
return this.backend.getClientOptions();
|
|
||||||
}, "getClientOptions");
|
|
||||||
|
|
||||||
IndexedDBStore.prototype.storeClientOptions = degradable(function(options) {
|
|
||||||
MemoryStore.prototype.storeClientOptions.call(this, options);
|
|
||||||
return this.backend.storeClientOptions(options);
|
|
||||||
}, "storeClientOptions");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All member functions of `IndexedDBStore` that access the backend use this wrapper to
|
|
||||||
* watch for failures after initial store startup, including `QuotaExceededError` as
|
|
||||||
* free disk space changes, etc.
|
|
||||||
*
|
|
||||||
* When IndexedDB fails via any of these paths, we degrade this back to a `MemoryStore`
|
|
||||||
* in place so that the current operation and all future ones are in-memory only.
|
|
||||||
*
|
|
||||||
* @param {Function} func The degradable work to do.
|
|
||||||
* @param {String} fallback The method name for fallback.
|
|
||||||
* @returns {Function} A wrapped member function.
|
|
||||||
*/
|
|
||||||
function degradable(func, fallback) {
|
|
||||||
return async function(...args) {
|
|
||||||
try {
|
|
||||||
return await func.call(this, ...args);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("IndexedDBStore failure, degrading to MemoryStore", e);
|
|
||||||
this.emit("degraded", e);
|
|
||||||
try {
|
|
||||||
// We try to delete IndexedDB after degrading since this store is only a
|
|
||||||
// cache (the app will still function correctly without the data).
|
|
||||||
// It's possible that deleting repair IndexedDB for the next app load,
|
|
||||||
// potenially by making a little more space available.
|
|
||||||
logger.log("IndexedDBStore trying to delete degraded data");
|
|
||||||
await this.backend.clearDatabase();
|
|
||||||
logger.log("IndexedDBStore delete after degrading succeeeded");
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn("IndexedDBStore delete after degrading failed", e);
|
|
||||||
}
|
|
||||||
// Degrade the store from being an instance of `IndexedDBStore` to instead be
|
|
||||||
// an instance of `MemoryStore` so that future API calls use the memory path
|
|
||||||
// directly and skip IndexedDB entirely. This should be safe as
|
|
||||||
// `IndexedDBStore` already extends from `MemoryStore`, so we are making the
|
|
||||||
// store become its parent type in a way. The mutator methods of
|
|
||||||
// `IndexedDBStore` also maintain the state that `MemoryStore` uses (many are
|
|
||||||
// not overridden at all).
|
|
||||||
Object.setPrototypeOf(this, MemoryStore.prototype);
|
|
||||||
if (fallback) {
|
|
||||||
return await MemoryStore.prototype[fallback].call(this, ...args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
330
src/store/indexeddb.ts
Normal file
330
src/store/indexeddb.ts
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 - 2021 Vector Creations Ltd
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @babel/no-invalid-this */
|
||||||
|
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
import { MemoryStore, IOpts as IBaseOpts } from "./memory";
|
||||||
|
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend.js";
|
||||||
|
import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend.js";
|
||||||
|
import { User } from "../models/user";
|
||||||
|
import { MatrixEvent } from "../models/event";
|
||||||
|
import { logger } from '../logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an internal module. See {@link IndexedDBStore} for the public class.
|
||||||
|
* @module store/indexeddb
|
||||||
|
*/
|
||||||
|
|
||||||
|
// If this value is too small we'll be writing very often which will cause
|
||||||
|
// noticeable stop-the-world pauses. If this value is too big we'll be writing
|
||||||
|
// so infrequently that the /sync size gets bigger on reload. Writing more
|
||||||
|
// often does not affect the length of the pause since the entire /sync
|
||||||
|
// response is persisted each time.
|
||||||
|
const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
|
||||||
|
|
||||||
|
interface IOpts extends IBaseOpts {
|
||||||
|
indexedDB: IDBFactory;
|
||||||
|
dbName?: string;
|
||||||
|
workerScript?: string;
|
||||||
|
workerApi?: typeof Worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IndexedDBStore extends MemoryStore {
|
||||||
|
static exists(indexedDB: IDBFactory, dbName: string): boolean {
|
||||||
|
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO these should conform to one interface
|
||||||
|
public readonly backend: LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend;
|
||||||
|
|
||||||
|
private startedUp = false;
|
||||||
|
private syncTs = 0;
|
||||||
|
// Records the last-modified-time of each user at the last point we saved
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new Indexed Database store, which extends MemoryStore.
|
||||||
|
*
|
||||||
|
* This store functions like a MemoryStore except it periodically persists
|
||||||
|
* the contents of the store to an IndexedDB backend.
|
||||||
|
*
|
||||||
|
* All data is still kept in-memory but can be loaded from disk by calling
|
||||||
|
* <code>startup()</code>. This can make startup times quicker as a complete
|
||||||
|
* sync from the server is not required. This does not reduce memory usage as all
|
||||||
|
* the data is eagerly fetched when <code>startup()</code> is called.
|
||||||
|
* <pre>
|
||||||
|
* let opts = { indexedDB: window.indexedDB, localStorage: window.localStorage };
|
||||||
|
* let store = new IndexedDBStore(opts);
|
||||||
|
* await store.startup(); // load from indexed db
|
||||||
|
* let client = sdk.createClient({
|
||||||
|
* store: store,
|
||||||
|
* });
|
||||||
|
* client.startClient();
|
||||||
|
* client.on("sync", function(state, prevState, data) {
|
||||||
|
* if (state === "PREPARED") {
|
||||||
|
* console.log("Started up, now with go faster stripes!");
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @extends MemoryStore
|
||||||
|
* @param {Object} opts Options object.
|
||||||
|
* @param {Object} opts.indexedDB The Indexed DB interface e.g.
|
||||||
|
* <code>window.indexedDB</code>
|
||||||
|
* @param {string=} opts.dbName Optional database name. The same name must be used
|
||||||
|
* to open the same database.
|
||||||
|
* @param {string=} opts.workerScript Optional URL to a script to invoke a web
|
||||||
|
* worker with to run IndexedDB queries on the web worker. The IndexedDbStoreWorker
|
||||||
|
* class is provided for this purpose and requires the application to provide a
|
||||||
|
* trivial wrapper script around it.
|
||||||
|
* @param {Object=} opts.workerApi The webWorker API object. If omitted, the global Worker
|
||||||
|
* object will be used if it exists.
|
||||||
|
* @prop {IndexedDBStoreBackend} backend The backend instance. Call through to
|
||||||
|
* this API if you need to perform specific indexeddb actions like deleting the
|
||||||
|
* database.
|
||||||
|
*/
|
||||||
|
constructor(opts: IOpts) {
|
||||||
|
super(opts);
|
||||||
|
|
||||||
|
if (!opts.indexedDB) {
|
||||||
|
throw new Error('Missing required option: indexedDB');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.workerScript) {
|
||||||
|
// try & find a webworker-compatible API
|
||||||
|
let workerApi = opts.workerApi;
|
||||||
|
if (!workerApi) {
|
||||||
|
// default to the global Worker object (which is where it in a browser)
|
||||||
|
workerApi = global.Worker;
|
||||||
|
}
|
||||||
|
this.backend = new RemoteIndexedDBStoreBackend(
|
||||||
|
opts.workerScript, opts.dbName, workerApi,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.backend = new LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public on = this.emitter.on.bind(this.emitter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise} Resolved when loaded from indexed db.
|
||||||
|
*/
|
||||||
|
public startup(): Promise<void> {
|
||||||
|
if (this.startedUp) {
|
||||||
|
logger.log(`IndexedDBStore.startup: already started`);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log(`IndexedDBStore.startup: connecting to backend`);
|
||||||
|
return this.backend.connect().then(() => {
|
||||||
|
logger.log(`IndexedDBStore.startup: loading presence events`);
|
||||||
|
return this.backend.getUserPresenceEvents();
|
||||||
|
}).then((userPresenceEvents) => {
|
||||||
|
logger.log(`IndexedDBStore.startup: processing presence events`);
|
||||||
|
userPresenceEvents.forEach(([userId, rawEvent]) => {
|
||||||
|
const u = new User(userId);
|
||||||
|
if (rawEvent) {
|
||||||
|
u.setPresenceEvent(new MatrixEvent(rawEvent));
|
||||||
|
}
|
||||||
|
this.userModifiedMap[u.userId] = u.getLastModifiedTime();
|
||||||
|
this.storeUser(u);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise} Resolves with a sync response to restore the
|
||||||
|
* client state to where it was at the last save, or null if there
|
||||||
|
* is no saved sync data.
|
||||||
|
*/
|
||||||
|
public getSavedSync = this.degradable((): Promise<object> => {
|
||||||
|
return this.backend.getSavedSync();
|
||||||
|
}, "getSavedSync");
|
||||||
|
|
||||||
|
/** @return {Promise<boolean>} whether or not the database was newly created in this session. */
|
||||||
|
public isNewlyCreated = this.degradable((): Promise<boolean> => {
|
||||||
|
return this.backend.isNewlyCreated();
|
||||||
|
}, "isNewlyCreated");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise} If there is a saved sync, the nextBatch token
|
||||||
|
* for this sync, otherwise null.
|
||||||
|
*/
|
||||||
|
public getSavedSyncToken = this.degradable((): Promise<string | null> => {
|
||||||
|
return this.backend.getNextBatchToken();
|
||||||
|
}, "getSavedSyncToken");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all data from this store.
|
||||||
|
* @return {Promise} Resolves if the data was deleted from the database.
|
||||||
|
*/
|
||||||
|
public deleteAllData = this.degradable((): Promise<void> => {
|
||||||
|
super.deleteAllData();
|
||||||
|
return this.backend.clearDatabase().then(() => {
|
||||||
|
logger.log("Deleted indexeddb data.");
|
||||||
|
}, (err) => {
|
||||||
|
logger.error(`Failed to delete indexeddb data: ${err}`);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this store would like to save its data
|
||||||
|
* Note that obviously whether the store wants to save or
|
||||||
|
* not could change between calling this function and calling
|
||||||
|
* save().
|
||||||
|
*
|
||||||
|
* @return {boolean} True if calling save() will actually save
|
||||||
|
* (at the time this function is called).
|
||||||
|
*/
|
||||||
|
public wantsSave(): boolean {
|
||||||
|
const now = Date.now();
|
||||||
|
return now - this.syncTs > WRITE_DELAY_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possibly write data to the database.
|
||||||
|
*
|
||||||
|
* @param {boolean} force True to force a save to happen
|
||||||
|
* @return {Promise} Promise resolves after the write completes
|
||||||
|
* (or immediately if no write is performed)
|
||||||
|
*/
|
||||||
|
public save(force = false): Promise<void> {
|
||||||
|
if (force || this.wantsSave()) {
|
||||||
|
return this.reallySave();
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
private reallySave = this.degradable((): Promise<void> => {
|
||||||
|
this.syncTs = Date.now(); // set now to guard against multi-writes
|
||||||
|
|
||||||
|
// work out changed users (this doesn't handle deletions but you
|
||||||
|
// can't 'delete' users as they are just presence events).
|
||||||
|
const userTuples = [];
|
||||||
|
for (const u of this.getUsers()) {
|
||||||
|
if (this.userModifiedMap[u.userId] === u.getLastModifiedTime()) continue;
|
||||||
|
if (!u.events.presence) continue;
|
||||||
|
|
||||||
|
userTuples.push([u.userId, u.events.presence.event]);
|
||||||
|
|
||||||
|
// note that we've saved this version of the user
|
||||||
|
this.userModifiedMap[u.userId] = u.getLastModifiedTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.backend.syncToDatabase(userTuples);
|
||||||
|
});
|
||||||
|
|
||||||
|
public setSyncData = this.degradable((syncData: object): Promise<void> => {
|
||||||
|
return this.backend.setSyncData(syncData);
|
||||||
|
}, "setSyncData");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the out-of-band membership events for this room that
|
||||||
|
* were previously loaded.
|
||||||
|
* @param {string} roomId
|
||||||
|
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||||
|
* @returns {null} in case the members for this room haven't been stored yet
|
||||||
|
*/
|
||||||
|
public getOutOfBandMembers = this.degradable((roomId: string): Promise<MatrixEvent[]> => {
|
||||||
|
return this.backend.getOutOfBandMembers(roomId);
|
||||||
|
}, "getOutOfBandMembers");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the out-of-band membership events for this room. Note that
|
||||||
|
* it still makes sense to store an empty array as the OOB status for the room is
|
||||||
|
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
|
||||||
|
* @param {string} roomId
|
||||||
|
* @param {event[]} membershipEvents the membership events to store
|
||||||
|
* @returns {Promise} when all members have been stored
|
||||||
|
*/
|
||||||
|
public setOutOfBandMembers = this.degradable((roomId: string, membershipEvents: MatrixEvent[]): Promise<void> => {
|
||||||
|
super.setOutOfBandMembers(roomId, membershipEvents);
|
||||||
|
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
|
||||||
|
}, "setOutOfBandMembers");
|
||||||
|
|
||||||
|
public clearOutOfBandMembers = this.degradable((roomId: string) => {
|
||||||
|
super.clearOutOfBandMembers(roomId);
|
||||||
|
return this.backend.clearOutOfBandMembers(roomId);
|
||||||
|
}, "clearOutOfBandMembers");
|
||||||
|
|
||||||
|
public getClientOptions = this.degradable((): Promise<object> => {
|
||||||
|
return this.backend.getClientOptions();
|
||||||
|
}, "getClientOptions");
|
||||||
|
|
||||||
|
public storeClientOptions = this.degradable((options: object): Promise<void> => {
|
||||||
|
super.storeClientOptions(options);
|
||||||
|
return this.backend.storeClientOptions(options);
|
||||||
|
}, "storeClientOptions");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All member functions of `IndexedDBStore` that access the backend use this wrapper to
|
||||||
|
* watch for failures after initial store startup, including `QuotaExceededError` as
|
||||||
|
* free disk space changes, etc.
|
||||||
|
*
|
||||||
|
* When IndexedDB fails via any of these paths, we degrade this back to a `MemoryStore`
|
||||||
|
* in place so that the current operation and all future ones are in-memory only.
|
||||||
|
*
|
||||||
|
* @param {Function} func The degradable work to do.
|
||||||
|
* @param {String} fallback The method name for fallback.
|
||||||
|
* @returns {Function} A wrapped member function.
|
||||||
|
*/
|
||||||
|
private degradable<A extends Array<any>, R = void>(
|
||||||
|
func: DegradableFn<A, R>,
|
||||||
|
fallback?: string,
|
||||||
|
): DegradableFn<A, R> {
|
||||||
|
const fallbackFn = super[fallback];
|
||||||
|
|
||||||
|
return async (...args) => {
|
||||||
|
try {
|
||||||
|
return func.call(this, ...args);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("IndexedDBStore failure, degrading to MemoryStore", e);
|
||||||
|
this.emitter.emit("degraded", e);
|
||||||
|
try {
|
||||||
|
// We try to delete IndexedDB after degrading since this store is only a
|
||||||
|
// cache (the app will still function correctly without the data).
|
||||||
|
// It's possible that deleting repair IndexedDB for the next app load,
|
||||||
|
// potentially by making a little more space available.
|
||||||
|
logger.log("IndexedDBStore trying to delete degraded data");
|
||||||
|
await this.backend.clearDatabase();
|
||||||
|
logger.log("IndexedDBStore delete after degrading succeeded");
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("IndexedDBStore delete after degrading failed", e);
|
||||||
|
}
|
||||||
|
// Degrade the store from being an instance of `IndexedDBStore` to instead be
|
||||||
|
// an instance of `MemoryStore` so that future API calls use the memory path
|
||||||
|
// directly and skip IndexedDB entirely. This should be safe as
|
||||||
|
// `IndexedDBStore` already extends from `MemoryStore`, so we are making the
|
||||||
|
// store become its parent type in a way. The mutator methods of
|
||||||
|
// `IndexedDBStore` also maintain the state that `MemoryStore` uses (many are
|
||||||
|
// not overridden at all).
|
||||||
|
if (fallbackFn) {
|
||||||
|
return fallbackFn(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DegradableFn<A extends Array<any>, T> = (...args: A) => Promise<T>;
|
@@ -1,8 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -22,9 +19,18 @@ limitations under the License.
|
|||||||
* @module store/memory
|
* @module store/memory
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { EventType } from "../@types/event";
|
||||||
|
import { Group } from "../models/group";
|
||||||
|
import { Room } from "../models/room";
|
||||||
import { User } from "../models/user";
|
import { User } from "../models/user";
|
||||||
|
import { MatrixEvent } from "../models/event";
|
||||||
|
import { RoomState } from "../models/room-state";
|
||||||
|
import { RoomMember } from "../models/room-member";
|
||||||
|
import { Filter } from "../filter";
|
||||||
|
import { IStore } from "./index";
|
||||||
|
import { RoomSummary } from "../models/room-summary";
|
||||||
|
|
||||||
function isValidFilterId(filterId) {
|
function isValidFilterId(filterId: string): boolean {
|
||||||
const isValidStr = typeof filterId === "string" &&
|
const isValidStr = typeof filterId === "string" &&
|
||||||
!!filterId &&
|
!!filterId &&
|
||||||
filterId !== "undefined" && // exclude these as we've serialized undefined in localStorage before
|
filterId !== "undefined" && // exclude these as we've serialized undefined in localStorage before
|
||||||
@@ -33,6 +39,10 @@ function isValidFilterId(filterId) {
|
|||||||
return isValidStr || typeof filterId === "number";
|
return isValidStr || typeof filterId === "number";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IOpts {
|
||||||
|
localStorage?: Storage;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a new in-memory data store for the Matrix Client.
|
* Construct a new in-memory data store for the Matrix Client.
|
||||||
* @constructor
|
* @constructor
|
||||||
@@ -40,96 +50,84 @@ function isValidFilterId(filterId) {
|
|||||||
* @param {LocalStorage} opts.localStorage The local storage instance to persist
|
* @param {LocalStorage} opts.localStorage The local storage instance to persist
|
||||||
* some forms of data such as tokens. Rooms will NOT be stored.
|
* some forms of data such as tokens. Rooms will NOT be stored.
|
||||||
*/
|
*/
|
||||||
export function MemoryStore(opts) {
|
export class MemoryStore implements IStore {
|
||||||
opts = opts || {};
|
private rooms: Record<string, Room> = {}; // roomId: Room
|
||||||
this.rooms = {
|
private groups: Record<string, Group> = {}; // groupId: Group
|
||||||
// roomId: Room
|
private users: Record<string, User> = {}; // userId: User
|
||||||
};
|
private syncToken: string = null;
|
||||||
this.groups = {
|
// userId: {
|
||||||
// groupId: Group
|
// filterId: Filter
|
||||||
};
|
// }
|
||||||
this.users = {
|
private filters: Record<string, Record<string, Filter>> = {};
|
||||||
// userId: User
|
private accountData: Record<string, MatrixEvent> = {}; // type : content
|
||||||
};
|
private readonly localStorage: Storage;
|
||||||
this.syncToken = null;
|
private oobMembers: Record<string, MatrixEvent[]> = {}; // roomId: [member events]
|
||||||
this.filters = {
|
private clientOptions = {};
|
||||||
// userId: {
|
|
||||||
// filterId: Filter
|
|
||||||
// }
|
|
||||||
};
|
|
||||||
this.accountData = {
|
|
||||||
// type : content
|
|
||||||
};
|
|
||||||
this.localStorage = opts.localStorage;
|
|
||||||
this._oobMembers = {
|
|
||||||
// roomId: [member events]
|
|
||||||
};
|
|
||||||
this._clientOptions = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
MemoryStore.prototype = {
|
constructor(opts: IOpts = {}) {
|
||||||
|
this.localStorage = opts.localStorage;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the token to stream from.
|
* Retrieve the token to stream from.
|
||||||
* @return {string} The token or null.
|
* @return {string} The token or null.
|
||||||
*/
|
*/
|
||||||
getSyncToken: function() {
|
public getSyncToken(): string | null {
|
||||||
return this.syncToken;
|
return this.syncToken;
|
||||||
},
|
}
|
||||||
|
|
||||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
/** @return {Promise<boolean>} whether or not the database was newly created in this session. */
|
||||||
isNewlyCreated: function() {
|
public isNewlyCreated(): Promise<boolean> {
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the token to stream from.
|
* Set the token to stream from.
|
||||||
* @param {string} token The token to stream from.
|
* @param {string} token The token to stream from.
|
||||||
*/
|
*/
|
||||||
setSyncToken: function(token) {
|
public setSyncToken(token: string) {
|
||||||
this.syncToken = token;
|
this.syncToken = token;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store the given room.
|
* Store the given room.
|
||||||
* @param {Group} group The group to be stored
|
* @param {Group} group The group to be stored
|
||||||
*/
|
*/
|
||||||
storeGroup: function(group) {
|
public storeGroup(group: Group) {
|
||||||
this.groups[group.groupId] = group;
|
this.groups[group.groupId] = group;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a group by its group ID.
|
* Retrieve a group by its group ID.
|
||||||
* @param {string} groupId The group ID.
|
* @param {string} groupId The group ID.
|
||||||
* @return {Group} The group or null.
|
* @return {Group} The group or null.
|
||||||
*/
|
*/
|
||||||
getGroup: function(groupId) {
|
public getGroup(groupId: string): Group | null {
|
||||||
return this.groups[groupId] || null;
|
return this.groups[groupId] || null;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all known groups.
|
* Retrieve all known groups.
|
||||||
* @return {Group[]} A list of groups, which may be empty.
|
* @return {Group[]} A list of groups, which may be empty.
|
||||||
*/
|
*/
|
||||||
getGroups: function() {
|
public getGroups(): Group[] {
|
||||||
return Object.values(this.groups);
|
return Object.values(this.groups);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store the given room.
|
* Store the given room.
|
||||||
* @param {Room} room The room to be stored. All properties must be stored.
|
* @param {Room} room The room to be stored. All properties must be stored.
|
||||||
*/
|
*/
|
||||||
storeRoom: function(room) {
|
public storeRoom(room: Room) {
|
||||||
this.rooms[room.roomId] = room;
|
this.rooms[room.roomId] = room;
|
||||||
// add listeners for room member changes so we can keep the room member
|
// add listeners for room member changes so we can keep the room member
|
||||||
// map up-to-date.
|
// map up-to-date.
|
||||||
room.currentState.on("RoomState.members", this._onRoomMember.bind(this));
|
room.currentState.on("RoomState.members", this.onRoomMember);
|
||||||
// add existing members
|
// add existing members
|
||||||
const self = this;
|
room.currentState.getMembers().forEach((m) => {
|
||||||
room.currentState.getMembers().forEach(function(m) {
|
this.onRoomMember(null, room.currentState, m);
|
||||||
self._onRoomMember(null, room.currentState, m);
|
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a room member in a room being tracked by this store has been
|
* Called when a room member in a room being tracked by this store has been
|
||||||
@@ -138,7 +136,7 @@ MemoryStore.prototype = {
|
|||||||
* @param {RoomState} state
|
* @param {RoomState} state
|
||||||
* @param {RoomMember} member
|
* @param {RoomMember} member
|
||||||
*/
|
*/
|
||||||
_onRoomMember: function(event, state, member) {
|
private onRoomMember = (event: MatrixEvent, state: RoomState, member: RoomMember) => {
|
||||||
if (member.membership === "invite") {
|
if (member.membership === "invite") {
|
||||||
// We do NOT add invited members because people love to typo user IDs
|
// We do NOT add invited members because people love to typo user IDs
|
||||||
// which would then show up in these lists (!)
|
// which would then show up in these lists (!)
|
||||||
@@ -158,70 +156,70 @@ MemoryStore.prototype = {
|
|||||||
user.setAvatarUrl(member.events.member.getContent().avatar_url);
|
user.setAvatarUrl(member.events.member.getContent().avatar_url);
|
||||||
}
|
}
|
||||||
this.users[user.userId] = user;
|
this.users[user.userId] = user;
|
||||||
},
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a room by its' room ID.
|
* Retrieve a room by its' room ID.
|
||||||
* @param {string} roomId The room ID.
|
* @param {string} roomId The room ID.
|
||||||
* @return {Room} The room or null.
|
* @return {Room} The room or null.
|
||||||
*/
|
*/
|
||||||
getRoom: function(roomId) {
|
public getRoom(roomId: string): Room | null {
|
||||||
return this.rooms[roomId] || null;
|
return this.rooms[roomId] || null;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all known rooms.
|
* Retrieve all known rooms.
|
||||||
* @return {Room[]} A list of rooms, which may be empty.
|
* @return {Room[]} A list of rooms, which may be empty.
|
||||||
*/
|
*/
|
||||||
getRooms: function() {
|
public getRooms(): Room[] {
|
||||||
return Object.values(this.rooms);
|
return Object.values(this.rooms);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Permanently delete a room.
|
* Permanently delete a room.
|
||||||
* @param {string} roomId
|
* @param {string} roomId
|
||||||
*/
|
*/
|
||||||
removeRoom: function(roomId) {
|
public removeRoom(roomId: string): void {
|
||||||
if (this.rooms[roomId]) {
|
if (this.rooms[roomId]) {
|
||||||
this.rooms[roomId].removeListener("RoomState.members", this._onRoomMember);
|
this.rooms[roomId].removeListener("RoomState.members", this.onRoomMember);
|
||||||
}
|
}
|
||||||
delete this.rooms[roomId];
|
delete this.rooms[roomId];
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a summary of all the rooms.
|
* Retrieve a summary of all the rooms.
|
||||||
* @return {RoomSummary[]} A summary of each room.
|
* @return {RoomSummary[]} A summary of each room.
|
||||||
*/
|
*/
|
||||||
getRoomSummaries: function() {
|
public getRoomSummaries(): RoomSummary[] {
|
||||||
return Object.values(this.rooms).map(function(room) {
|
return Object.values(this.rooms).map(function(room) {
|
||||||
return room.summary;
|
return room.summary;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a User.
|
* Store a User.
|
||||||
* @param {User} user The user to store.
|
* @param {User} user The user to store.
|
||||||
*/
|
*/
|
||||||
storeUser: function(user) {
|
public storeUser(user: User): void {
|
||||||
this.users[user.userId] = user;
|
this.users[user.userId] = user;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a User by its' user ID.
|
* Retrieve a User by its' user ID.
|
||||||
* @param {string} userId The user ID.
|
* @param {string} userId The user ID.
|
||||||
* @return {User} The user or null.
|
* @return {User} The user or null.
|
||||||
*/
|
*/
|
||||||
getUser: function(userId) {
|
public getUser(userId: string): User | null {
|
||||||
return this.users[userId] || null;
|
return this.users[userId] || null;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all known users.
|
* Retrieve all known users.
|
||||||
* @return {User[]} A list of users, which may be empty.
|
* @return {User[]} A list of users, which may be empty.
|
||||||
*/
|
*/
|
||||||
getUsers: function() {
|
public getUsers(): User[] {
|
||||||
return Object.values(this.users);
|
return Object.values(this.users);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve scrollback for this room.
|
* Retrieve scrollback for this room.
|
||||||
@@ -230,9 +228,9 @@ MemoryStore.prototype = {
|
|||||||
* @return {Array<Object>} An array of objects which will be at most 'limit'
|
* @return {Array<Object>} An array of objects which will be at most 'limit'
|
||||||
* length and at least 0. The objects are the raw event JSON.
|
* length and at least 0. The objects are the raw event JSON.
|
||||||
*/
|
*/
|
||||||
scrollback: function(room, limit) {
|
public scrollback(room: Room, limit: number): MatrixEvent[] {
|
||||||
return [];
|
return [];
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store events for a room. The events have already been added to the timeline
|
* Store events for a room. The events have already been added to the timeline
|
||||||
@@ -241,15 +239,15 @@ MemoryStore.prototype = {
|
|||||||
* @param {string} token The token associated with these events.
|
* @param {string} token The token associated with these events.
|
||||||
* @param {boolean} toStart True if these are paginated results.
|
* @param {boolean} toStart True if these are paginated results.
|
||||||
*/
|
*/
|
||||||
storeEvents: function(room, events, token, toStart) {
|
public storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean) {
|
||||||
// no-op because they've already been added to the room instance.
|
// no-op because they've already been added to the room instance.
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a filter.
|
* Store a filter.
|
||||||
* @param {Filter} filter
|
* @param {Filter} filter
|
||||||
*/
|
*/
|
||||||
storeFilter: function(filter) {
|
public storeFilter(filter: Filter): void {
|
||||||
if (!filter) {
|
if (!filter) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -257,7 +255,7 @@ MemoryStore.prototype = {
|
|||||||
this.filters[filter.userId] = {};
|
this.filters[filter.userId] = {};
|
||||||
}
|
}
|
||||||
this.filters[filter.userId][filter.filterId] = filter;
|
this.filters[filter.userId][filter.filterId] = filter;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a filter.
|
* Retrieve a filter.
|
||||||
@@ -265,19 +263,19 @@ MemoryStore.prototype = {
|
|||||||
* @param {string} filterId
|
* @param {string} filterId
|
||||||
* @return {?Filter} A filter or null.
|
* @return {?Filter} A filter or null.
|
||||||
*/
|
*/
|
||||||
getFilter: function(userId, filterId) {
|
public getFilter(userId: string, filterId: string): Filter | null {
|
||||||
if (!this.filters[userId] || !this.filters[userId][filterId]) {
|
if (!this.filters[userId] || !this.filters[userId][filterId]) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return this.filters[userId][filterId];
|
return this.filters[userId][filterId];
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a filter ID with the given name.
|
* Retrieve a filter ID with the given name.
|
||||||
* @param {string} filterName The filter name.
|
* @param {string} filterName The filter name.
|
||||||
* @return {?string} The filter ID or null.
|
* @return {?string} The filter ID or null.
|
||||||
*/
|
*/
|
||||||
getFilterIdByName: function(filterName) {
|
public getFilterIdByName(filterName: string): string | null {
|
||||||
if (!this.localStorage) {
|
if (!this.localStorage) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -294,14 +292,14 @@ MemoryStore.prototype = {
|
|||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return null;
|
return null;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a filter name to ID mapping.
|
* Set a filter name to ID mapping.
|
||||||
* @param {string} filterName
|
* @param {string} filterName
|
||||||
* @param {string} filterId
|
* @param {string} filterId
|
||||||
*/
|
*/
|
||||||
setFilterIdByName: function(filterName, filterId) {
|
public setFilterIdByName(filterName: string, filterId: string) {
|
||||||
if (!this.localStorage) {
|
if (!this.localStorage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -313,7 +311,7 @@ MemoryStore.prototype = {
|
|||||||
this.localStorage.removeItem(key);
|
this.localStorage.removeItem(key);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store user-scoped account data events.
|
* Store user-scoped account data events.
|
||||||
@@ -321,21 +319,20 @@ MemoryStore.prototype = {
|
|||||||
* events with the same type will replace each other.
|
* events with the same type will replace each other.
|
||||||
* @param {Array<MatrixEvent>} events The events to store.
|
* @param {Array<MatrixEvent>} events The events to store.
|
||||||
*/
|
*/
|
||||||
storeAccountDataEvents: function(events) {
|
public storeAccountDataEvents(events: MatrixEvent[]): void {
|
||||||
const self = this;
|
events.forEach((event) => {
|
||||||
events.forEach(function(event) {
|
this.accountData[event.getType()] = event;
|
||||||
self.accountData[event.getType()] = event;
|
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get account data event by event type
|
* Get account data event by event type
|
||||||
* @param {string} eventType The event type being queried
|
* @param {string} eventType The event type being queried
|
||||||
* @return {?MatrixEvent} the user account_data event of given type, if any
|
* @return {?MatrixEvent} the user account_data event of given type, if any
|
||||||
*/
|
*/
|
||||||
getAccountData: function(eventType) {
|
public getAccountData(eventType: EventType | string): MatrixEvent | undefined {
|
||||||
return this.accountData[eventType];
|
return this.accountData[eventType];
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* setSyncData does nothing as there is no backing data store.
|
* setSyncData does nothing as there is no backing data store.
|
||||||
@@ -343,56 +340,56 @@ MemoryStore.prototype = {
|
|||||||
* @param {Object} syncData The sync data
|
* @param {Object} syncData The sync data
|
||||||
* @return {Promise} An immediately resolved promise.
|
* @return {Promise} An immediately resolved promise.
|
||||||
*/
|
*/
|
||||||
setSyncData: function(syncData) {
|
public setSyncData(syncData: object): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We never want to save becase we have nothing to save to.
|
* We never want to save becase we have nothing to save to.
|
||||||
*
|
*
|
||||||
* @return {boolean} If the store wants to save
|
* @return {boolean} If the store wants to save
|
||||||
*/
|
*/
|
||||||
wantsSave: function() {
|
public wantsSave(): boolean {
|
||||||
return false;
|
return false;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save does nothing as there is no backing data store.
|
* Save does nothing as there is no backing data store.
|
||||||
* @param {bool} force True to force a save (but the memory
|
* @param {bool} force True to force a save (but the memory
|
||||||
* store still can't save anything)
|
* store still can't save anything)
|
||||||
*/
|
*/
|
||||||
save: function(force) {},
|
public save(force: boolean): void {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Startup does nothing as this store doesn't require starting up.
|
* Startup does nothing as this store doesn't require starting up.
|
||||||
* @return {Promise} An immediately resolved promise.
|
* @return {Promise} An immediately resolved promise.
|
||||||
*/
|
*/
|
||||||
startup: function() {
|
public startup(): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {Promise} Resolves with a sync response to restore the
|
* @return {Promise} Resolves with a sync response to restore the
|
||||||
* client state to where it was at the last save, or null if there
|
* client state to where it was at the last save, or null if there
|
||||||
* is no saved sync data.
|
* is no saved sync data.
|
||||||
*/
|
*/
|
||||||
getSavedSync: function() {
|
public getSavedSync(): Promise<object> {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {Promise} If there is a saved sync, the nextBatch token
|
* @return {Promise} If there is a saved sync, the nextBatch token
|
||||||
* for this sync, otherwise null.
|
* for this sync, otherwise null.
|
||||||
*/
|
*/
|
||||||
getSavedSyncToken: function() {
|
public getSavedSyncToken(): Promise<string | null> {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all data from this store.
|
* Delete all data from this store.
|
||||||
* @return {Promise} An immediately resolved promise.
|
* @return {Promise} An immediately resolved promise.
|
||||||
*/
|
*/
|
||||||
deleteAllData: function() {
|
public deleteAllData(): Promise<void> {
|
||||||
this.rooms = {
|
this.rooms = {
|
||||||
// roomId: Room
|
// roomId: Room
|
||||||
};
|
};
|
||||||
@@ -409,7 +406,7 @@ MemoryStore.prototype = {
|
|||||||
// type : content
|
// type : content
|
||||||
};
|
};
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the out-of-band membership events for this room that
|
* Returns the out-of-band membership events for this room that
|
||||||
@@ -418,9 +415,9 @@ MemoryStore.prototype = {
|
|||||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||||
* @returns {null} in case the members for this room haven't been stored yet
|
* @returns {null} in case the members for this room haven't been stored yet
|
||||||
*/
|
*/
|
||||||
getOutOfBandMembers: function(roomId) {
|
public getOutOfBandMembers(roomId: string): Promise<MatrixEvent[] | null> {
|
||||||
return Promise.resolve(this._oobMembers[roomId] || null);
|
return Promise.resolve(this.oobMembers[roomId] || null);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the out-of-band membership events for this room. Note that
|
* Stores the out-of-band membership events for this room. Note that
|
||||||
@@ -430,22 +427,22 @@ MemoryStore.prototype = {
|
|||||||
* @param {event[]} membershipEvents the membership events to store
|
* @param {event[]} membershipEvents the membership events to store
|
||||||
* @returns {Promise} when all members have been stored
|
* @returns {Promise} when all members have been stored
|
||||||
*/
|
*/
|
||||||
setOutOfBandMembers: function(roomId, membershipEvents) {
|
public setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void> {
|
||||||
this._oobMembers[roomId] = membershipEvents;
|
this.oobMembers[roomId] = membershipEvents;
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
|
|
||||||
clearOutOfBandMembers: function() {
|
public clearOutOfBandMembers(roomId: string): Promise<void> {
|
||||||
this._oobMembers = {};
|
this.oobMembers = {};
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
|
|
||||||
getClientOptions: function() {
|
public getClientOptions(): Promise<object> {
|
||||||
return Promise.resolve(this._clientOptions);
|
return Promise.resolve(this.clientOptions);
|
||||||
},
|
}
|
||||||
|
|
||||||
storeClientOptions: function(options) {
|
public storeClientOptions(options: object): Promise<void> {
|
||||||
this._clientOptions = Object.assign({}, options);
|
this.clientOptions = Object.assign({}, options);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
};
|
}
|
@@ -1,8 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -22,124 +19,127 @@ limitations under the License.
|
|||||||
* @module store/stub
|
* @module store/stub
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { EventType } from "../@types/event";
|
||||||
|
import { Group } from "../models/group";
|
||||||
|
import { Room } from "../models/room";
|
||||||
|
import { User } from "../models/user";
|
||||||
|
import { MatrixEvent } from "../models/event";
|
||||||
|
import { Filter } from "../filter";
|
||||||
|
import { IStore } from "./index";
|
||||||
|
import { RoomSummary } from "../models/room-summary";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a stub store. This does no-ops on most store methods.
|
* Construct a stub store. This does no-ops on most store methods.
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export function StubStore() {
|
export class StubStore implements IStore {
|
||||||
this.fromToken = null;
|
private fromToken: string = null;
|
||||||
}
|
|
||||||
|
|
||||||
StubStore.prototype = {
|
|
||||||
|
|
||||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
||||||
isNewlyCreated: function() {
|
public isNewlyCreated(): Promise<boolean> {
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the sync token.
|
* Get the sync token.
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
getSyncToken: function() {
|
public getSyncToken(): string | null {
|
||||||
return this.fromToken;
|
return this.fromToken;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the sync token.
|
* Set the sync token.
|
||||||
* @param {string} token
|
* @param {string} token
|
||||||
*/
|
*/
|
||||||
setSyncToken: function(token) {
|
public setSyncToken(token: string) {
|
||||||
this.fromToken = token;
|
this.fromToken = token;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No-op.
|
* No-op.
|
||||||
* @param {Group} group
|
* @param {Group} group
|
||||||
*/
|
*/
|
||||||
storeGroup: function(group) {
|
public storeGroup(group: Group) {}
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No-op.
|
* No-op.
|
||||||
* @param {string} groupId
|
* @param {string} groupId
|
||||||
* @return {null}
|
* @return {null}
|
||||||
*/
|
*/
|
||||||
getGroup: function(groupId) {
|
public getGroup(groupId: string): Group | null {
|
||||||
return null;
|
return null;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No-op.
|
* No-op.
|
||||||
* @return {Array} An empty array.
|
* @return {Array} An empty array.
|
||||||
*/
|
*/
|
||||||
getGroups: function() {
|
public getGroups(): Group[] {
|
||||||
return [];
|
return [];
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No-op.
|
* No-op.
|
||||||
* @param {Room} room
|
* @param {Room} room
|
||||||
*/
|
*/
|
||||||
storeRoom: function(room) {
|
public storeRoom(room: Room) {}
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No-op.
|
* No-op.
|
||||||
* @param {string} roomId
|
* @param {string} roomId
|
||||||
* @return {null}
|
* @return {null}
|
||||||
*/
|
*/
|
||||||
getRoom: function(roomId) {
|
public getRoom(roomId: string): Room | null {
|
||||||
return null;
|
return null;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No-op.
|
* No-op.
|
||||||
* @return {Array} An empty array.
|
* @return {Array} An empty array.
|
||||||
*/
|
*/
|
||||||
getRooms: function() {
|
public getRooms(): Room[] {
|
||||||
return [];
|
return [];
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Permanently delete a room.
|
* Permanently delete a room.
|
||||||
* @param {string} roomId
|
* @param {string} roomId
|
||||||
*/
|
*/
|
||||||
removeRoom: function(roomId) {
|
public removeRoom(roomId: string) {
|
||||||
return;
|
return;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No-op.
|
* No-op.
|
||||||
* @return {Array} An empty array.
|
* @return {Array} An empty array.
|
||||||
*/
|
*/
|
||||||
getRoomSummaries: function() {
|
public getRoomSummaries(): RoomSummary[] {
|
||||||
return [];
|
return [];
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No-op.
|
* No-op.
|
||||||
* @param {User} user
|
* @param {User} user
|
||||||
*/
|
*/
|
||||||
storeUser: function(user) {
|
public storeUser(user: User) {}
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No-op.
|
* No-op.
|
||||||
* @param {string} userId
|
* @param {string} userId
|
||||||
* @return {null}
|
* @return {null}
|
||||||
*/
|
*/
|
||||||
getUser: function(userId) {
|
public getUser(userId: string): User | null {
|
||||||
return null;
|
return null;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No-op.
|
* No-op.
|
||||||
* @return {User[]}
|
* @return {User[]}
|
||||||
*/
|
*/
|
||||||
getUsers: function() {
|
public getUsers(): User[] {
|
||||||
return [];
|
return [];
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No-op.
|
* No-op.
|
||||||
@@ -147,9 +147,9 @@ StubStore.prototype = {
|
|||||||
* @param {integer} limit
|
* @param {integer} limit
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
scrollback: function(room, limit) {
|
public scrollback(room: Room, limit: number): MatrixEvent[] {
|
||||||
return [];
|
return [];
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store events for a room.
|
* Store events for a room.
|
||||||
@@ -158,15 +158,13 @@ StubStore.prototype = {
|
|||||||
* @param {string} token The token associated with these events.
|
* @param {string} token The token associated with these events.
|
||||||
* @param {boolean} toStart True if these are paginated results.
|
* @param {boolean} toStart True if these are paginated results.
|
||||||
*/
|
*/
|
||||||
storeEvents: function(room, events, token, toStart) {
|
public storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean) {}
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a filter.
|
* Store a filter.
|
||||||
* @param {Filter} filter
|
* @param {Filter} filter
|
||||||
*/
|
*/
|
||||||
storeFilter: function(filter) {
|
public storeFilter(filter: Filter) {}
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a filter.
|
* Retrieve a filter.
|
||||||
@@ -174,43 +172,39 @@ StubStore.prototype = {
|
|||||||
* @param {string} filterId
|
* @param {string} filterId
|
||||||
* @return {?Filter} A filter or null.
|
* @return {?Filter} A filter or null.
|
||||||
*/
|
*/
|
||||||
getFilter: function(userId, filterId) {
|
public getFilter(userId: string, filterId: string): Filter | null {
|
||||||
return null;
|
return null;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a filter ID with the given name.
|
* Retrieve a filter ID with the given name.
|
||||||
* @param {string} filterName The filter name.
|
* @param {string} filterName The filter name.
|
||||||
* @return {?string} The filter ID or null.
|
* @return {?string} The filter ID or null.
|
||||||
*/
|
*/
|
||||||
getFilterIdByName: function(filterName) {
|
public getFilterIdByName(filterName: string): string | null {
|
||||||
return null;
|
return null;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a filter name to ID mapping.
|
* Set a filter name to ID mapping.
|
||||||
* @param {string} filterName
|
* @param {string} filterName
|
||||||
* @param {string} filterId
|
* @param {string} filterId
|
||||||
*/
|
*/
|
||||||
setFilterIdByName: function(filterName, filterId) {
|
public setFilterIdByName(filterName: string, filterId: string) {}
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store user-scoped account data events
|
* Store user-scoped account data events
|
||||||
* @param {Array<MatrixEvent>} events The events to store.
|
* @param {Array<MatrixEvent>} events The events to store.
|
||||||
*/
|
*/
|
||||||
storeAccountDataEvents: function(events) {
|
public storeAccountDataEvents(events: MatrixEvent[]) {}
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get account data event by event type
|
* Get account data event by event type
|
||||||
* @param {string} eventType The event type being queried
|
* @param {string} eventType The event type being queried
|
||||||
*/
|
*/
|
||||||
getAccountData: function(eventType) {
|
public getAccountData(eventType: EventType | string): MatrixEvent | undefined {
|
||||||
|
return undefined;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* setSyncData does nothing as there is no backing data store.
|
* setSyncData does nothing as there is no backing data store.
|
||||||
@@ -218,75 +212,75 @@ StubStore.prototype = {
|
|||||||
* @param {Object} syncData The sync data
|
* @param {Object} syncData The sync data
|
||||||
* @return {Promise} An immediately resolved promise.
|
* @return {Promise} An immediately resolved promise.
|
||||||
*/
|
*/
|
||||||
setSyncData: function(syncData) {
|
public setSyncData(syncData: object): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We never want to save becase we have nothing to save to.
|
* We never want to save because we have nothing to save to.
|
||||||
*
|
*
|
||||||
* @return {boolean} If the store wants to save
|
* @return {boolean} If the store wants to save
|
||||||
*/
|
*/
|
||||||
wantsSave: function() {
|
public wantsSave(): boolean {
|
||||||
return false;
|
return false;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save does nothing as there is no backing data store.
|
* Save does nothing as there is no backing data store.
|
||||||
*/
|
*/
|
||||||
save: function() {},
|
public save() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Startup does nothing.
|
* Startup does nothing.
|
||||||
* @return {Promise} An immediately resolved promise.
|
* @return {Promise} An immediately resolved promise.
|
||||||
*/
|
*/
|
||||||
startup: function() {
|
public startup(): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {Promise} Resolves with a sync response to restore the
|
* @return {Promise} Resolves with a sync response to restore the
|
||||||
* client state to where it was at the last save, or null if there
|
* client state to where it was at the last save, or null if there
|
||||||
* is no saved sync data.
|
* is no saved sync data.
|
||||||
*/
|
*/
|
||||||
getSavedSync: function() {
|
public getSavedSync(): Promise<object> {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {Promise} If there is a saved sync, the nextBatch token
|
* @return {Promise} If there is a saved sync, the nextBatch token
|
||||||
* for this sync, otherwise null.
|
* for this sync, otherwise null.
|
||||||
*/
|
*/
|
||||||
getSavedSyncToken: function() {
|
public getSavedSyncToken(): Promise<string | null> {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all data from this store. Does nothing since this store
|
* Delete all data from this store. Does nothing since this store
|
||||||
* doesn't store anything.
|
* doesn't store anything.
|
||||||
* @return {Promise} An immediately resolved promise.
|
* @return {Promise} An immediately resolved promise.
|
||||||
*/
|
*/
|
||||||
deleteAllData: function() {
|
public deleteAllData(): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
|
|
||||||
getOutOfBandMembers: function() {
|
public getOutOfBandMembers(): Promise<MatrixEvent[]> {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
},
|
}
|
||||||
|
|
||||||
setOutOfBandMembers: function() {
|
public setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
|
|
||||||
clearOutOfBandMembers: function() {
|
public clearOutOfBandMembers(): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
|
|
||||||
getClientOptions: function() {
|
public getClientOptions(): Promise<object> {
|
||||||
return Promise.resolve();
|
return Promise.resolve({});
|
||||||
},
|
}
|
||||||
|
|
||||||
storeClientOptions: function() {
|
public storeClientOptions(options: object): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
};
|
}
|
@@ -1272,10 +1272,10 @@ SyncApi.prototype._processSyncResponse = async function(
|
|||||||
if (e.isState() && e.getType() === "im.vector.user_status") {
|
if (e.isState() && e.getType() === "im.vector.user_status") {
|
||||||
let user = client.store.getUser(e.getStateKey());
|
let user = client.store.getUser(e.getStateKey());
|
||||||
if (user) {
|
if (user) {
|
||||||
user._unstable_updateStatusMessage(e);
|
user.unstable_updateStatusMessage(e);
|
||||||
} else {
|
} else {
|
||||||
user = createNewUser(client, e.getStateKey());
|
user = createNewUser(client, e.getStateKey());
|
||||||
user._unstable_updateStatusMessage(e);
|
user.unstable_updateStatusMessage(e);
|
||||||
client.store.storeUser(user);
|
client.store.storeUser(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
src/utils.ts
23
src/utils.ts
@@ -20,7 +20,8 @@ limitations under the License.
|
|||||||
* @module utils
|
* @module utils
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import unhomoglyph from 'unhomoglyph';
|
import unhomoglyph from "unhomoglyph";
|
||||||
|
import promiseRetry from "p-retry";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode a dictionary of query parameters.
|
* Encode a dictionary of query parameters.
|
||||||
@@ -469,6 +470,26 @@ export async function chunkPromises<T>(fns: (() => Promise<T>)[], chunkSize: num
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retries the function until it succeeds or is interrupted. The given function must return
|
||||||
|
* a promise which throws/rejects on error, otherwise the retry will assume the request
|
||||||
|
* succeeded. The promise chain returned will contain the successful promise. The given function
|
||||||
|
* should always return a new promise.
|
||||||
|
* @param {Function} promiseFn The function to call to get a fresh promise instance. Takes an
|
||||||
|
* attempt count as an argument, for logging/debugging purposes.
|
||||||
|
* @returns {Promise<T>} The promise for the retried operation.
|
||||||
|
*/
|
||||||
|
export function simpleRetryOperation<T>(promiseFn: (attempt: number) => Promise<T>): Promise<T> {
|
||||||
|
return promiseRetry((attempt: number) => {
|
||||||
|
return promiseFn(attempt);
|
||||||
|
}, {
|
||||||
|
forever: true,
|
||||||
|
factor: 2,
|
||||||
|
minTimeout: 3000, // ms
|
||||||
|
maxTimeout: 15000, // ms
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// We need to be able to access the Node.js crypto library from within the
|
// We need to be able to access the Node.js crypto library from within the
|
||||||
// Matrix SDK without needing to `require("crypto")`, which will fail in
|
// Matrix SDK without needing to `require("crypto")`, which will fail in
|
||||||
// browsers. So `index.ts` will call `setCrypto` to store it, and when we need
|
// browsers. So `index.ts` will call `setCrypto` to store it, and when we need
|
||||||
|
@@ -24,7 +24,7 @@ limitations under the License.
|
|||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import * as utils from '../utils';
|
import * as utils from '../utils';
|
||||||
import MatrixEvent from '../models/event';
|
import { MatrixEvent } from '../models/event';
|
||||||
import { EventType } from '../@types/event';
|
import { EventType } from '../@types/event';
|
||||||
import { RoomMember } from '../models/room-member';
|
import { RoomMember } from '../models/room-member';
|
||||||
import { randomString } from '../randomstring';
|
import { randomString } from '../randomstring';
|
||||||
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import MatrixEvent from '../models/event';
|
import { MatrixEvent } from '../models/event';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { createNewMatrixCall, MatrixCall, CallErrorCode, CallState, CallDirection } from './call';
|
import { createNewMatrixCall, MatrixCall, CallErrorCode, CallState, CallDirection } from './call';
|
||||||
import { EventType } from '../@types/event';
|
import { EventType } from '../@types/event';
|
||||||
@@ -244,7 +244,7 @@ export class CallEventHandler {
|
|||||||
} else {
|
} else {
|
||||||
call.onRemoteIceCandidatesReceived(event);
|
call.onRemoteIceCandidatesReceived(event);
|
||||||
}
|
}
|
||||||
} else if ([EventType.CallHangup, EventType.CallReject].includes(event.getType())) {
|
} else if ([EventType.CallHangup, EventType.CallReject].includes(event.getType() as EventType)) {
|
||||||
// Note that we also observe our own hangups here so we can see
|
// Note that we also observe our own hangups here so we can see
|
||||||
// if we've already rejected a call that would otherwise be valid
|
// if we've already rejected a call that would otherwise be valid
|
||||||
if (!call) {
|
if (!call) {
|
||||||
|
26
yarn.lock
26
yarn.lock
@@ -1132,6 +1132,7 @@
|
|||||||
|
|
||||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz":
|
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz":
|
||||||
version "3.2.3"
|
version "3.2.3"
|
||||||
|
uid cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4
|
||||||
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4"
|
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4"
|
||||||
|
|
||||||
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents":
|
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents":
|
||||||
@@ -1305,6 +1306,11 @@
|
|||||||
"@types/tough-cookie" "*"
|
"@types/tough-cookie" "*"
|
||||||
form-data "^2.5.0"
|
form-data "^2.5.0"
|
||||||
|
|
||||||
|
"@types/retry@^0.12.0":
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
|
||||||
|
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
|
||||||
|
|
||||||
"@types/stack-utils@^2.0.0":
|
"@types/stack-utils@^2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff"
|
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff"
|
||||||
@@ -4699,12 +4705,7 @@ lodash.sortby@^4.7.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||||
|
|
||||||
lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.20:
|
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4:
|
||||||
version "4.17.20"
|
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
|
||||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
|
||||||
|
|
||||||
lodash@^4.17.15, lodash@^4.17.4:
|
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
@@ -5220,6 +5221,14 @@ p-locate@^4.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-limit "^2.2.0"
|
p-limit "^2.2.0"
|
||||||
|
|
||||||
|
p-retry@^4.5.0:
|
||||||
|
version "4.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.5.0.tgz#6685336b3672f9ee8174d3769a660cb5e488521d"
|
||||||
|
integrity sha512-5Hwh4aVQSu6BEP+w2zKlVXtFAaYQe1qWuVADSgoeVlLjwe/Q/AMSoRR4MDeaAfu8llT+YNbEijWu/YF3m6avkg==
|
||||||
|
dependencies:
|
||||||
|
"@types/retry" "^0.12.0"
|
||||||
|
retry "^0.12.0"
|
||||||
|
|
||||||
p-try@^2.0.0:
|
p-try@^2.0.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
||||||
@@ -5943,6 +5952,11 @@ ret@~0.1.10:
|
|||||||
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
||||||
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
|
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
|
||||||
|
|
||||||
|
retry@^0.12.0:
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
|
||||||
|
integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
|
||||||
|
|
||||||
reusify@^1.0.4:
|
reusify@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||||
|
Reference in New Issue
Block a user