From e775bcac3c9cc2257537b3a75851326e216d6f05 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 7 Jul 2021 11:08:54 +0100 Subject: [PATCH 01/65] Convert SearchResult, InteractiveAuth, PushProcessor and Scheduler to Typescript --- src/client.ts | 4 +- ...nteractive-auth.js => interactive-auth.ts} | 400 +++++++------ src/models/event.ts | 7 +- src/models/search-result.js | 60 -- src/models/search-result.ts | 77 +++ src/pushprocessor.js | 469 --------------- src/pushprocessor.ts | 561 ++++++++++++++++++ src/scheduler.js | 327 ---------- src/scheduler.ts | 327 ++++++++++ src/sync.ts | 7 +- src/utils.ts | 4 +- 11 files changed, 1210 insertions(+), 1033 deletions(-) rename src/{interactive-auth.js => interactive-auth.ts} (60%) delete mode 100644 src/models/search-result.js create mode 100644 src/models/search-result.ts delete mode 100644 src/pushprocessor.js create mode 100644 src/pushprocessor.ts delete mode 100644 src/scheduler.js create mode 100644 src/scheduler.ts diff --git a/src/client.ts b/src/client.ts index 637cf9cba..6acc6c1bd 100644 --- a/src/client.ts +++ b/src/client.ts @@ -30,7 +30,7 @@ import * as utils from './utils'; import { sleep } from './utils'; import { Group } from "./models/group"; import { EventTimeline } from "./models/event-timeline"; -import { PushAction, PushProcessor } from "./pushprocessor"; +import { IActionsObject, PushProcessor } from "./pushprocessor"; import { AutoDiscovery } from "./autodiscovery"; import * as olmlib from "./crypto/olmlib"; import { decodeBase64, encodeBase64 } from "./crypto/olmlib"; @@ -4092,7 +4092,7 @@ export class MatrixClient extends EventEmitter { * @param {MatrixEvent} event The event to get push actions for. * @return {module:pushprocessor~PushAction} A dict of actions to perform. */ - public getPushActionsForEvent(event: MatrixEvent): PushAction { + public getPushActionsForEvent(event: MatrixEvent): IActionsObject { if (!event.getPushActions()) { event.setPushActions(this.pushProcessor.actionsForEvent(event)); } diff --git a/src/interactive-auth.js b/src/interactive-auth.ts similarity index 60% rename from src/interactive-auth.js rename to src/interactive-auth.ts index c1537fe85..3c8b0c168 100644 --- a/src/interactive-auth.js +++ b/src/interactive-auth.ts @@ -19,12 +19,84 @@ limitations under the License. /** @module interactive-auth */ import url from "url"; + import * as utils from "./utils"; import { logger } from './logger'; +import { MatrixClient } from "./client"; +import { defer, IDeferred } from "./utils"; +import { MatrixError } from "./http-api"; const EMAIL_STAGE_TYPE = "m.login.email.identity"; const MSISDN_STAGE_TYPE = "m.login.msisdn"; +interface IFlow { + stages: AuthType[]; +} + +export interface IInputs { + emailAddress?: string; + phoneCountry?: string; + phoneNumber?: string; +} + +export interface IStageStatus { + emailSid?: string; + errcode?: string; + error?: string; +} + +export interface IAuthData { + session?: string; + completed?: string[]; + flows?: IFlow[]; + params?: Record>; + errcode?: string; + error?: MatrixError; +} + +export enum AuthType { + Password = "m.login.password", + Recaptcha = "m.login.recaptcha", + Terms = "m.login.terms", + Email = "m.login.email.identity", + Msisdn = "m.login.msisdn", + Sso = "m.login.sso", + SsoUnstable = "org.matrix.login.sso", + Dummy = "m.login.dummy", +} + +export interface IAuthDict { + // [key: string]: any; + type?: string; + // session?: string; // TODO + // TODO: Remove `user` once servers support proper UIA + // See https://github.com/vector-im/element-web/issues/10312 + user?: string; + identifier?: any; + password?: string; + response?: string; + // TODO: Remove `threepid_creds` once servers support proper UIA + // See https://github.com/vector-im/element-web/issues/10312 + // See https://github.com/matrix-org/matrix-doc/issues/2220 + // eslint-disable-next-line camelcase + threepid_creds?: any; + threepidCreds?: any; +} + +interface IOpts { + matrixClient: MatrixClient; + authData?: IAuthData; + inputs?: IInputs; + sessionId?: string; + clientSecret?: string; + emailSid?: string; + doRequest(auth: IAuthData, background: boolean): Promise; + stateUpdated(nextStage: AuthType, status: IStageStatus): void; + requestEmailToken(email: string, secret: string, attempt: number, session: string): Promise<{ sid: string }>; + busyChanged?(busy: boolean): void; + startAuthStage?(nextStage: string): Promise; // LEGACY +} + /** * Abstracts the logic used to drive the interactive auth process. * @@ -51,12 +123,12 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn"; * called with the new auth dict to submit the request. Also passes a * second deprecated arg which is a flag set to true if this request * is a background request. The busyChanged callback should be used - * instead of the backfround flag. Should return a promise which resolves + * instead of the background flag. Should return a promise which resolves * to the successful response or rejects with a MatrixError. * - * @param {function(bool): Promise} opts.busyChanged + * @param {function(boolean): Promise} opts.busyChanged * called whenever the interactive auth logic becomes busy submitting - * information provided by the user or finsihes. After this has been + * information provided by the user or finishes. After this has been * called with true the UI should indicate that a request is in progress * until it is called again with false. * @@ -102,33 +174,39 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn"; * attemptAuth promise. * */ -export function InteractiveAuth(opts) { - this._matrixClient = opts.matrixClient; - this._data = opts.authData || {}; - this._requestCallback = opts.doRequest; - this._busyChangedCallback = opts.busyChanged; - // startAuthStage included for backwards compat - this._stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage; - this._resolveFunc = null; - this._rejectFunc = null; - this._inputs = opts.inputs || {}; - this._requestEmailTokenCallback = opts.requestEmailToken; +export class InteractiveAuth { + private readonly matrixClient: MatrixClient; + private readonly inputs: IInputs; + private readonly clientSecret: string; + private readonly requestCallback: IOpts["doRequest"]; + private readonly busyChangedCallback?: IOpts["busyChanged"]; + private readonly stateUpdatedCallback: IOpts["stateUpdated"]; + private readonly requestEmailTokenCallback: IOpts["requestEmailToken"]; - if (opts.sessionId) this._data.session = opts.sessionId; - this._clientSecret = opts.clientSecret || this._matrixClient.generateClientSecret(); - this._emailSid = opts.emailSid; - if (this._emailSid === undefined) this._emailSid = null; - this._requestingEmailToken = false; - - this._chosenFlow = null; - this._currentStage = null; + private data: IAuthData; + private emailSid?: string; + private requestingEmailToken = false; + private attemptAuthDeferred: IDeferred = null; + private chosenFlow: IFlow = null; + private currentStage: string = null; // if we are currently trying to submit an auth dict (which includes polling) // the promise the will resolve/reject when it completes - this._submitPromise = null; -} + private submitPromise: Promise = null; + + constructor(opts: IOpts) { + this.data = opts.authData || {}; + this.requestCallback = opts.doRequest; + this.busyChangedCallback = opts.busyChanged; + // startAuthStage included for backwards compat + this.stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage; + this.requestEmailTokenCallback = opts.requestEmailToken; + + if (opts.sessionId) this.data.session = opts.sessionId; + this.clientSecret = opts.clientSecret || this.matrixClient.generateClientSecret(); + this.emailSid = opts.emailSid ?? null; + } -InteractiveAuth.prototype = { /** * begin the authentication process. * @@ -136,59 +214,58 @@ InteractiveAuth.prototype = { * or rejects with the error on failure. Rejects with NoAuthFlowFoundError if * no suitable authentication flow can be found */ - attemptAuth: function() { + public attemptAuth(): Promise { // This promise will be quite long-lived and will resolve when the // request is authenticated and completes successfully. - return new Promise((resolve, reject) => { - this._resolveFunc = resolve; - this._rejectFunc = reject; + this.attemptAuthDeferred = defer(); - const hasFlows = this._data && this._data.flows; + const hasFlows = this.data && this.data.flows; - // if we have no flows, try a request to acquire the flows - if (!hasFlows) { - if (this._busyChangedCallback) this._busyChangedCallback(true); - // use the existing sessionid, if one is present. - let auth = null; - if (this._data.session) { - auth = { - session: this._data.session, - }; - } - this._doRequest(auth).finally(() => { - if (this._busyChangedCallback) this._busyChangedCallback(false); - }); - } else { - this._startNextAuthStage(); + // if we have no flows, try a request to acquire the flows + if (!hasFlows) { + this.busyChangedCallback?.(true); + // use the existing sessionid, if one is present. + let auth = null; + if (this.data.session) { + auth = { + session: this.data.session, + }; } - }); - }, + this.doRequest(auth).finally(() => { + this.busyChangedCallback?.(false); + }); + } else { + this.startNextAuthStage(); + } + + return this.attemptAuthDeferred.promise; + } /** * Poll to check if the auth session or current stage has been * completed out-of-band. If so, the attemptAuth promise will * be resolved. */ - poll: async function() { - if (!this._data.session) return; + public async poll(): Promise { + if (!this.data.session) return; // likewise don't poll if there is no auth session in progress - if (!this._resolveFunc) return; + if (!this.attemptAuthDeferred) return; // if we currently have a request in flight, there's no point making // another just to check what the status is - if (this._submitPromise) return; + if (this.submitPromise) return; - let authDict = {}; - if (this._currentStage == EMAIL_STAGE_TYPE) { + let authDict: IAuthDict = {}; + if (this.currentStage == EMAIL_STAGE_TYPE) { // The email can be validated out-of-band, but we need to provide the // creds so the HS can go & check it. - if (this._emailSid) { - const creds = { - sid: this._emailSid, - client_secret: this._clientSecret, + if (this.emailSid) { + const creds: Record = { + sid: this.emailSid, + client_secret: this.clientSecret, }; - if (await this._matrixClient.doesServerRequireIdServerParam()) { + if (await this.matrixClient.doesServerRequireIdServerParam()) { const idServerParsedUrl = url.parse( - this._matrixClient.getIdentityServerUrl(), + this.matrixClient.getIdentityServerUrl(), ); creds.id_server = idServerParsedUrl.host; } @@ -204,16 +281,16 @@ InteractiveAuth.prototype = { } this.submitAuthDict(authDict, true); - }, + } /** * get the auth session ID * * @return {string} session id */ - getSessionId: function() { - return this._data ? this._data.session : undefined; - }, + public getSessionId(): string { + return this.data ? this.data.session : undefined; + } /** * get the client secret used for validation sessions @@ -221,9 +298,9 @@ InteractiveAuth.prototype = { * * @return {string} client secret */ - getClientSecret: function() { - return this._clientSecret; - }, + public getClientSecret(): string { + return this.clientSecret; + } /** * get the server params for a given stage @@ -231,17 +308,13 @@ InteractiveAuth.prototype = { * @param {string} loginType login type for the stage * @return {object?} any parameters from the server for this stage */ - getStageParams: function(loginType) { - let params = {}; - if (this._data && this._data.params) { - params = this._data.params; - } - return params[loginType]; - }, + public getStageParams(loginType: string): Record { + return this.data.params?.[loginType]; + } - getChosenFlow() { - return this._chosenFlow; - }, + public getChosenFlow(): IFlow { + return this.chosenFlow; + } /** * submit a new auth dict and fire off the request. This will either @@ -249,38 +322,38 @@ InteractiveAuth.prototype = { * to be called for a new stage. * * @param {object} authData new auth dict to send to the server. Should - * include a `type` propterty denoting the login type, as well as any + * include a `type` property denoting the login type, as well as any * other params for that stage. - * @param {bool} background If true, this request failing will not result + * @param {boolean} background If true, this request failing will not result * in the attemptAuth promise being rejected. This can be set to true * for requests that just poll to see if auth has been completed elsewhere. */ - submitAuthDict: async function(authData, background) { - if (!this._resolveFunc) { + public async submitAuthDict(authData: IAuthDict, background = false): Promise { + if (!this.attemptAuthDeferred) { throw new Error("submitAuthDict() called before attemptAuth()"); } - if (!background && this._busyChangedCallback) { - this._busyChangedCallback(true); + if (!background) { + this.busyChangedCallback?.(true); } // if we're currently trying a request, wait for it to finish // as otherwise we can get multiple 200 responses which can mean // things like multiple logins for register requests. - // (but discard any expections as we only care when its done, + // (but discard any exceptions as we only care when its done, // not whether it worked or not) - while (this._submitPromise) { + while (this.submitPromise) { try { - await this._submitPromise; + await this.submitPromise; } catch (e) { } } // use the sessionid from the last request, if one is present. let auth; - if (this._data.session) { + if (this.data.session) { auth = { - session: this._data.session, + session: this.data.session, }; utils.extend(auth, authData); } else { @@ -290,15 +363,15 @@ InteractiveAuth.prototype = { try { // NB. the 'background' flag is deprecated by the busyChanged // callback and is here for backwards compat - this._submitPromise = this._doRequest(auth, background); - await this._submitPromise; + this.submitPromise = this.doRequest(auth, background); + await this.submitPromise; } finally { - this._submitPromise = null; - if (!background && this._busyChangedCallback) { - this._busyChangedCallback(false); + this.submitPromise = null; + if (!background) { + this.busyChangedCallback?.(false); } } - }, + } /** * Gets the sid for the email validation session @@ -306,9 +379,9 @@ InteractiveAuth.prototype = { * * @returns {string} The sid of the email auth session */ - getEmailSid: function() { - return this._emailSid; - }, + public getEmailSid(): string { + return this.emailSid; + } /** * Sets the sid for the email validation session @@ -318,9 +391,9 @@ InteractiveAuth.prototype = { * * @param {string} sid The sid for the email validation session */ - setEmailSid: function(sid) { - this._emailSid = sid; - }, + public setEmailSid(sid: string): void { + this.emailSid = sid; + } /** * Fire off a request, and either resolve the promise, or call @@ -328,33 +401,28 @@ InteractiveAuth.prototype = { * * @private * @param {object?} auth new auth dict, including session id - * @param {bool?} background If true, this request is a background poll, so it + * @param {boolean?} background If true, this request is a background poll, so it * failing will not result in the attemptAuth promise being rejected. * This can be set to true for requests that just poll to see if auth has * been completed elsewhere. */ - _doRequest: async function(auth, background) { + private async doRequest(auth: IAuthData, background = false): Promise { try { - const result = await this._requestCallback(auth, background); - this._resolveFunc(result); - this._resolveFunc = null; - this._rejectFunc = null; + const result = await this.requestCallback(auth, background); + this.attemptAuthDeferred.resolve(result); } catch (error) { // sometimes UI auth errors don't come with flows const errorFlows = error.data ? error.data.flows : null; - const haveFlows = this._data.flows || Boolean(errorFlows); + const haveFlows = this.data.flows || Boolean(errorFlows); if (error.httpStatus !== 401 || !error.data || !haveFlows) { // doesn't look like an interactive-auth failure. if (!background) { - this._rejectFunc(error); + this.attemptAuthDeferred?.reject(error); } else { // We ignore all failures here (even non-UI auth related ones) // since we don't want to suddenly fail if the internet connection // had a blip whilst we were polling - logger.log( - "Background poll request failed doing UI auth: ignoring", - error, - ); + logger.log("Background poll request failed doing UI auth: ignoring", error); } } // if the error didn't come with flows, completed flows or session ID, @@ -363,37 +431,35 @@ InteractiveAuth.prototype = { // has not yet been validated). This appears to be a Synapse bug, which // we workaround here. if (!error.data.flows && !error.data.completed && !error.data.session) { - error.data.flows = this._data.flows; - error.data.completed = this._data.completed; - error.data.session = this._data.session; + error.data.flows = this.data.flows; + error.data.completed = this.data.completed; + error.data.session = this.data.session; } - this._data = error.data; + this.data = error.data; try { - this._startNextAuthStage(); + this.startNextAuthStage(); } catch (e) { - this._rejectFunc(e); - this._resolveFunc = null; - this._rejectFunc = null; + this.attemptAuthDeferred.reject(e); } if ( - !this._emailSid && - !this._requestingEmailToken && - this._chosenFlow.stages.includes('m.login.email.identity') + !this.emailSid && + !this.requestingEmailToken && + this.chosenFlow.stages.includes(AuthType.Email) ) { // If we've picked a flow with email auth, we send the email // now because we want the request to fail as soon as possible // if the email address is not valid (ie. already taken or not // registered, depending on what the operation is). - this._requestingEmailToken = true; + this.requestingEmailToken = true; try { - const requestTokenResult = await this._requestEmailTokenCallback( - this._inputs.emailAddress, - this._clientSecret, + const requestTokenResult = await this.requestEmailTokenCallback( + this.inputs.emailAddress, + this.clientSecret, 1, // TODO: Multiple send attempts? - this._data.session, + this.data.session, ); - this._emailSid = requestTokenResult.sid; + this.emailSid = requestTokenResult.sid; // NB. promise is not resolved here - at some point, doRequest // will be called again and if the user has jumped through all // the hoops correctly, auth will be complete and the request @@ -407,15 +473,15 @@ InteractiveAuth.prototype = { // to do) or it could be a network failure. Either way, pass // the failure up as the user can't complete auth if we can't // send the email, for whatever reason. - this._rejectFunc(e); - this._resolveFunc = null; - this._rejectFunc = null; + this.attemptAuthDeferred.reject(e); } finally { - this._requestingEmailToken = false; + this.requestingEmailToken = false; } } + } finally { + this.attemptAuthDeferred = null; // TODO } - }, + } /** * Pick the next stage and call the callback @@ -423,34 +489,34 @@ InteractiveAuth.prototype = { * @private * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found */ - _startNextAuthStage: function() { - const nextStage = this._chooseStage(); + private startNextAuthStage(): void { + const nextStage = this.chooseStage(); if (!nextStage) { throw new Error("No incomplete flows from the server"); } - this._currentStage = nextStage; + this.currentStage = nextStage; - if (nextStage === 'm.login.dummy') { + if (nextStage === AuthType.Dummy) { this.submitAuthDict({ type: 'm.login.dummy', }); return; } - if (this._data && this._data.errcode || this._data.error) { - this._stateUpdatedCallback(nextStage, { - errcode: this._data.errcode || "", - error: this._data.error || "", + if (this.data && this.data.errcode || this.data.error) { + this.stateUpdatedCallback(nextStage, { + errcode: this.data.errcode || "", + error: this.data.error || "", }); return; } - const stageStatus = {}; + const stageStatus: IStageStatus = {}; if (nextStage == EMAIL_STAGE_TYPE) { - stageStatus.emailSid = this._emailSid; + stageStatus.emailSid = this.emailSid; } - this._stateUpdatedCallback(nextStage, stageStatus); - }, + this.stateUpdatedCallback(nextStage, stageStatus); + } /** * Pick the next auth stage @@ -459,15 +525,15 @@ InteractiveAuth.prototype = { * @return {string?} login type * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found */ - _chooseStage: function() { - if (this._chosenFlow === null) { - this._chosenFlow = this._chooseFlow(); + private chooseStage(): AuthType { + if (this.chosenFlow === null) { + this.chosenFlow = this.chooseFlow(); } - logger.log("Active flow => %s", JSON.stringify(this._chosenFlow)); - const nextStage = this._firstUncompletedStage(this._chosenFlow); + logger.log("Active flow => %s", JSON.stringify(this.chosenFlow)); + const nextStage = this.firstUncompletedStage(this.chosenFlow); logger.log("Next stage: %s", nextStage); return nextStage; - }, + } /** * Pick one of the flows from the returned list @@ -475,7 +541,7 @@ InteractiveAuth.prototype = { * be returned, otherwise, null will be returned. * * Only flows using all given inputs are chosen because it - * is likley to be surprising if the user provides a + * is likely to be surprising if the user provides a * credential and it is not used. For example, for registration, * this could result in the email not being used which would leave * the account with no means to reset a password. @@ -484,14 +550,14 @@ InteractiveAuth.prototype = { * @return {object} flow * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found */ - _chooseFlow: function() { - const flows = this._data.flows || []; + private chooseFlow(): IFlow { + const flows = this.data.flows || []; // we've been given an email or we've already done an email part - const haveEmail = Boolean(this._inputs.emailAddress) || Boolean(this._emailSid); + const haveEmail = Boolean(this.inputs.emailAddress) || Boolean(this.emailSid); const haveMsisdn = ( - Boolean(this._inputs.phoneCountry) && - Boolean(this._inputs.phoneNumber) + Boolean(this.inputs.phoneCountry) && + Boolean(this.inputs.phoneNumber) ); for (const flow of flows) { @@ -513,12 +579,13 @@ InteractiveAuth.prototype = { // information such that the app can give a better one if so desired. const err = new Error("No appropriate authentication flow found"); err.name = 'NoAuthFlowFoundError'; - err.required_stages = []; - if (haveEmail) err.required_stages.push(EMAIL_STAGE_TYPE); - if (haveMsisdn) err.required_stages.push(MSISDN_STAGE_TYPE); - err.available_flows = flows; + const requiredStages: string[] = []; + if (haveEmail) requiredStages.push(EMAIL_STAGE_TYPE); + if (haveMsisdn) requiredStages.push(MSISDN_STAGE_TYPE); + (err as any).required_stages = requiredStages; + (err as any).available_flows = flows; throw err; - }, + } /** * Get the first uncompleted stage in the given flow @@ -527,14 +594,13 @@ InteractiveAuth.prototype = { * @param {object} flow * @return {string} login type */ - _firstUncompletedStage: function(flow) { - const completed = (this._data || {}).completed || []; + private firstUncompletedStage(flow: IFlow): AuthType { + const completed = this.data.completed || []; for (let i = 0; i < flow.stages.length; ++i) { const stageType = flow.stages[i]; if (completed.indexOf(stageType) === -1) { return stageType; } } - }, -}; - + } +} diff --git a/src/models/event.ts b/src/models/event.ts index 9050f4440..9650f9026 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -28,6 +28,7 @@ import { EventType, MsgType, RelationType } from "../@types/event"; import { Crypto } from "../crypto"; import { deepSortedObjectEntries } from "../utils"; import { RoomMember } from "./room-member"; +import { IActionsObject } from '../pushprocessor'; /** * Enum for event statuses. @@ -148,7 +149,7 @@ export interface IDecryptOptions { } export class MatrixEvent extends EventEmitter { - private pushActions: object = null; + private pushActions: IActionsObject = null; private _replacingEvent: MatrixEvent = null; private _localRedactionEvent: MatrixEvent = null; private _isCancelled = false; @@ -935,7 +936,7 @@ export class MatrixEvent extends EventEmitter { * * @return {?Object} push actions */ - public getPushActions(): object | null { + public getPushActions(): IActionsObject | null { return this.pushActions; } @@ -944,7 +945,7 @@ export class MatrixEvent extends EventEmitter { * * @param {Object} pushActions push actions */ - public setPushActions(pushActions: object): void { + public setPushActions(pushActions: IActionsObject): void { this.pushActions = pushActions; } diff --git a/src/models/search-result.js b/src/models/search-result.js deleted file mode 100644 index fcf8bb43f..000000000 --- a/src/models/search-result.js +++ /dev/null @@ -1,60 +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/search-result - */ - -import { EventContext } from "./event-context"; - -/** - * Construct a new SearchResult - * - * @param {number} rank where this SearchResult ranks in the results - * @param {event-context.EventContext} eventContext the matching event and its - * context - * - * @constructor - */ -export function SearchResult(rank, eventContext) { - this.rank = rank; - this.context = eventContext; -} - -/** - * Create a SearchResponse from the response to /search - * @static - * @param {Object} jsonObj - * @param {function} eventMapper - * @return {SearchResult} - */ - -SearchResult.fromJson = function(jsonObj, eventMapper) { - const jsonContext = jsonObj.context || {}; - const events_before = jsonContext.events_before || []; - const events_after = jsonContext.events_after || []; - - const context = new EventContext(eventMapper(jsonObj.result)); - - context.setPaginateToken(jsonContext.start, true); - context.addEvents(events_before.map(eventMapper), true); - context.addEvents(events_after.map(eventMapper), false); - context.setPaginateToken(jsonContext.end, false); - - return new SearchResult(jsonObj.rank, context); -}; - diff --git a/src/models/search-result.ts b/src/models/search-result.ts new file mode 100644 index 000000000..7d3cc81c0 --- /dev/null +++ b/src/models/search-result.ts @@ -0,0 +1,77 @@ +/* +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/search-result + */ + +import { EventContext } from "./event-context"; +import { EventMapper } from "../event-mapper"; +import { IRoomEvent } from "../sync-accumulator"; + +/* eslint-disable camelcase */ +interface IContext { + events_before?: IRoomEvent[]; + events_after?: IRoomEvent[]; + start?: string; + end?: string; + profile_info?: Record; +} +/* eslint-enable camelcase */ + +interface ISearchResult { + rank: number; + result: IRoomEvent; + context: IContext; +} + +export class SearchResult { + /** + * Create a SearchResponse from the response to /search + * @static + * @param {Object} jsonObj + * @param {function} eventMapper + * @return {SearchResult} + */ + public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult { + const jsonContext: IContext = jsonObj.context || {}; + const eventsBefore = jsonContext.events_before || []; + const eventsAfter = jsonContext.events_after || []; + + const context = new EventContext(eventMapper(jsonObj.result)); + + context.setPaginateToken(jsonContext.start, true); + context.addEvents(eventsBefore.map(eventMapper), true); + context.addEvents(eventsAfter.map(eventMapper), false); + context.setPaginateToken(jsonContext.end, false); + + return new SearchResult(jsonObj.rank, context); + } + + /** + * Construct a new SearchResult + * + * @param {number} rank where this SearchResult ranks in the results + * @param {event-context.EventContext} context the matching event and its + * context + * + * @constructor + */ + constructor(public readonly rank: number, public readonly context: EventContext) {} +} diff --git a/src/pushprocessor.js b/src/pushprocessor.js deleted file mode 100644 index 4ab881623..000000000 --- a/src/pushprocessor.js +++ /dev/null @@ -1,469 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 New Vector 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. -*/ - -import { escapeRegExp, globToRegexp, isNullOrUndefined } from "./utils"; -import { logger } from './logger'; - -/** - * @module pushprocessor - */ - -const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride']; - -// The default override rules to apply to the push rules that arrive from the server. -// We do this for two reasons: -// 1. Synapse is unlikely to send us the push rule in an incremental sync - see -// https://github.com/matrix-org/synapse/pull/4867#issuecomment-481446072 for -// more details. -// 2. We often want to start using push rules ahead of the server supporting them, -// and so we can put them here. -const DEFAULT_OVERRIDE_RULES = [ - { - // For homeservers which don't support MSC1930 yet - rule_id: ".m.rule.tombstone", - default: true, - enabled: true, - conditions: [ - { - kind: "event_match", - key: "type", - pattern: "m.room.tombstone", - }, - { - kind: "event_match", - key: "state_key", - pattern: "", - }, - ], - actions: [ - "notify", - { - set_tweak: "highlight", - value: true, - }, - ], - }, - { - // For homeservers which don't support MSC2153 yet - rule_id: ".m.rule.reaction", - default: true, - enabled: true, - conditions: [ - { - kind: "event_match", - key: "type", - pattern: "m.reaction", - }, - ], - actions: [ - "dont_notify", - ], - }, -]; - -/** - * Construct a Push Processor. - * @constructor - * @param {Object} client The Matrix client object to use - */ -export function PushProcessor(client) { - const cachedGlobToRegex = { - // $glob: RegExp, - }; - - const matchingRuleFromKindSet = (ev, kindset) => { - for (let ruleKindIndex = 0; - ruleKindIndex < RULEKINDS_IN_ORDER.length; - ++ruleKindIndex) { - const kind = RULEKINDS_IN_ORDER[ruleKindIndex]; - const ruleset = kindset[kind]; - if (!ruleset) { - continue; - } - - for (let ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) { - const rule = ruleset[ruleIndex]; - if (!rule.enabled) { - continue; - } - - const rawrule = templateRuleToRaw(kind, rule); - if (!rawrule) { - continue; - } - - if (this.ruleMatchesEvent(rawrule, ev)) { - rule.kind = kind; - return rule; - } - } - } - return null; - }; - - const templateRuleToRaw = function(kind, tprule) { - const rawrule = { - 'rule_id': tprule.rule_id, - 'actions': tprule.actions, - 'conditions': [], - }; - switch (kind) { - case 'underride': - case 'override': - rawrule.conditions = tprule.conditions; - break; - case 'room': - if (!tprule.rule_id) { - return null; - } - rawrule.conditions.push({ - 'kind': 'event_match', - 'key': 'room_id', - 'value': tprule.rule_id, - }); - break; - case 'sender': - if (!tprule.rule_id) { - return null; - } - rawrule.conditions.push({ - 'kind': 'event_match', - 'key': 'user_id', - 'value': tprule.rule_id, - }); - break; - case 'content': - if (!tprule.pattern) { - return null; - } - rawrule.conditions.push({ - 'kind': 'event_match', - 'key': 'content.body', - 'pattern': tprule.pattern, - }); - break; - } - return rawrule; - }; - - const eventFulfillsCondition = function(cond, ev) { - const condition_functions = { - "event_match": eventFulfillsEventMatchCondition, - "contains_display_name": eventFulfillsDisplayNameCondition, - "room_member_count": eventFulfillsRoomMemberCountCondition, - "sender_notification_permission": eventFulfillsSenderNotifPermCondition, - }; - if (condition_functions[cond.kind]) { - return condition_functions[cond.kind](cond, ev); - } - // unknown conditions: we previously matched all unknown conditions, - // but given that rules can be added to the base rules on a server, - // it's probably better to not match unknown conditions. - return false; - }; - - const eventFulfillsSenderNotifPermCondition = function(cond, ev) { - const notifLevelKey = cond['key']; - if (!notifLevelKey) { - return false; - } - - const room = client.getRoom(ev.getRoomId()); - if (!room || !room.currentState) { - return false; - } - - // Note that this should not be the current state of the room but the state at - // the point the event is in the DAG. Unfortunately the js-sdk does not store - // this. - return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender()); - }; - - const eventFulfillsRoomMemberCountCondition = function(cond, ev) { - if (!cond.is) { - return false; - } - - const room = client.getRoom(ev.getRoomId()); - if (!room || !room.currentState || !room.currentState.members) { - return false; - } - - const memberCount = room.currentState.getJoinedMemberCount(); - - const m = cond.is.match(/^([=<>]*)([0-9]*)$/); - if (!m) { - return false; - } - const ineq = m[1]; - const rhs = parseInt(m[2]); - if (isNaN(rhs)) { - return false; - } - switch (ineq) { - case '': - case '==': - return memberCount == rhs; - case '<': - return memberCount < rhs; - case '>': - return memberCount > rhs; - case '<=': - return memberCount <= rhs; - case '>=': - return memberCount >= rhs; - default: - return false; - } - }; - - const eventFulfillsDisplayNameCondition = function(cond, ev) { - let content = ev.getContent(); - if (ev.isEncrypted() && ev.getClearContent()) { - content = ev.getClearContent(); - } - if (!content || !content.body || typeof content.body != 'string') { - return false; - } - - const room = client.getRoom(ev.getRoomId()); - if (!room || !room.currentState || !room.currentState.members || - !room.currentState.getMember(client.credentials.userId)) { - return false; - } - - const displayName = room.currentState.getMember(client.credentials.userId).name; - - // N.B. we can't use \b as it chokes on unicode. however \W seems to be okay - // as shorthand for [^0-9A-Za-z_]. - const pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i'); - return content.body.search(pat) > -1; - }; - - const eventFulfillsEventMatchCondition = function(cond, ev) { - if (!cond.key) { - return false; - } - - const val = valueForDottedKey(cond.key, ev); - if (typeof val !== 'string') { - return false; - } - - if (cond.value) { - return cond.value === val; - } - - if (typeof cond.pattern !== 'string') { - return false; - } - - let regex; - - if (cond.key == 'content.body') { - regex = createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)'); - } else { - regex = createCachedRegex('^', cond.pattern, '$'); - } - - return !!val.match(regex); - }; - - const createCachedRegex = function(prefix, glob, suffix) { - if (cachedGlobToRegex[glob]) { - return cachedGlobToRegex[glob]; - } - cachedGlobToRegex[glob] = new RegExp( - prefix + globToRegexp(glob) + suffix, - 'i', // Case insensitive - ); - return cachedGlobToRegex[glob]; - }; - - const valueForDottedKey = function(key, ev) { - const parts = key.split('.'); - let val; - - // special-case the first component to deal with encrypted messages - const firstPart = parts[0]; - if (firstPart === 'content') { - val = ev.getContent(); - parts.shift(); - } else if (firstPart === 'type') { - val = ev.getType(); - parts.shift(); - } else { - // use the raw event for any other fields - val = ev.event; - } - - while (parts.length > 0) { - const thisPart = parts.shift(); - if (isNullOrUndefined(val[thisPart])) { - return null; - } - val = val[thisPart]; - } - return val; - }; - - const matchingRuleForEventWithRulesets = function(ev, rulesets) { - if (!rulesets) { - return null; - } - if (ev.getSender() === client.credentials.userId) { - return null; - } - - return matchingRuleFromKindSet(ev, rulesets.global); - }; - - const pushActionsForEventAndRulesets = function(ev, rulesets) { - const rule = matchingRuleForEventWithRulesets(ev, rulesets); - if (!rule) { - return {}; - } - - const actionObj = PushProcessor.actionListToActionsObject(rule.actions); - - // Some actions are implicit in some situations: we add those here - if (actionObj.tweaks.highlight === undefined) { - // if it isn't specified, highlight if it's a content - // rule but otherwise not - actionObj.tweaks.highlight = (rule.kind == 'content'); - } - - return actionObj; - }; - - this.ruleMatchesEvent = function(rule, ev) { - let ret = true; - for (let i = 0; i < rule.conditions.length; ++i) { - const cond = rule.conditions[i]; - ret &= eventFulfillsCondition(cond, ev); - } - //console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match")); - return ret; - }; - - /** - * Get the user's push actions for the given event - * - * @param {module:models/event.MatrixEvent} ev - * - * @return {PushAction} - */ - this.actionsForEvent = function(ev) { - return pushActionsForEventAndRulesets(ev, client.pushRules); - }; - - /** - * Get one of the users push rules by its ID - * - * @param {string} ruleId The ID of the rule to search for - * @return {object} The push rule, or null if no such rule was found - */ - this.getPushRuleById = function(ruleId) { - for (const scope of ['global']) { - if (client.pushRules[scope] === undefined) continue; - - for (const kind of RULEKINDS_IN_ORDER) { - if (client.pushRules[scope][kind] === undefined) continue; - - for (const rule of client.pushRules[scope][kind]) { - if (rule.rule_id === ruleId) return rule; - } - } - } - return null; - }; -} - -/** - * Convert a list of actions into a object with the actions as keys and their values - * eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ] - * becomes { notify: true, tweaks: { sound: 'default' } } - * @param {array} actionlist The actions list - * - * @return {object} A object with key 'notify' (true or false) and an object of actions - */ -PushProcessor.actionListToActionsObject = function(actionlist) { - const actionobj = { 'notify': false, 'tweaks': {} }; - for (let i = 0; i < actionlist.length; ++i) { - const action = actionlist[i]; - if (action === 'notify') { - actionobj.notify = true; - } else if (typeof action === 'object') { - if (action.value === undefined) { - action.value = true; - } - actionobj.tweaks[action.set_tweak] = action.value; - } - } - return actionobj; -}; - -/** - * Rewrites conditions on a client's push rules to match the defaults - * where applicable. Useful for upgrading push rules to more strict - * conditions when the server is falling behind on defaults. - * @param {object} incomingRules The client's existing push rules - * @returns {object} The rewritten rules - */ -PushProcessor.rewriteDefaultRules = function(incomingRules) { - let newRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone - - // These lines are mostly to make the tests happy. We shouldn't run into these - // properties missing in practice. - if (!newRules) newRules = {}; - if (!newRules.global) newRules.global = {}; - if (!newRules.global.override) newRules.global.override = []; - - // Merge the client-level defaults with the ones from the server - const globalOverrides = newRules.global.override; - for (const override of DEFAULT_OVERRIDE_RULES) { - const existingRule = globalOverrides - .find((r) => r.rule_id === override.rule_id); - - if (existingRule) { - // Copy over the actions, default, and conditions. Don't touch the user's - // preference. - existingRule.default = override.default; - existingRule.conditions = override.conditions; - existingRule.actions = override.actions; - } else { - // Add the rule - const ruleId = override.rule_id; - logger.warn(`Adding default global override for ${ruleId}`); - globalOverrides.push(override); - } - } - - return newRules; -}; - -/** - * @typedef {Object} PushAction - * @type {Object} - * @property {boolean} notify Whether this event should notify the user or not. - * @property {Object} tweaks How this event should be notified. - * @property {boolean} tweaks.highlight Whether this event should be highlighted - * on the UI. - * @property {boolean} tweaks.sound Whether this notification should produce a - * noise. - */ - diff --git a/src/pushprocessor.ts b/src/pushprocessor.ts new file mode 100644 index 000000000..51e2c6316 --- /dev/null +++ b/src/pushprocessor.ts @@ -0,0 +1,561 @@ +/* +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 { escapeRegExp, globToRegexp, isNullOrUndefined } from "./utils"; +import { logger } from './logger'; +import { MatrixClient } from "./client"; +import { MatrixEvent } from "./models/event"; + +/** + * @module pushprocessor + */ + +enum RuleKind { + Override = "override", + Content = "content", + Room = "room", + Sender = "sender", + Underride = "underride", +} + +const RULEKINDS_IN_ORDER = [RuleKind.Override, RuleKind.Content, RuleKind.Room, RuleKind.Sender, RuleKind.Underride]; + +enum ConditionKind { + EventMatch = "event_match", + ContainsDisplayName = "contains_display_name", + RoomMemberCount = "room_member_count", + SenderNotificationPermission = "sender_notification_permission", +} + +enum Tweak { + Sound = "sound", + Highlight = "highlight", +} + +interface ISetTweak { + // eslint-disable-next-line camelcase + set_tweak: Tweak; + value: any; +} + +export enum Action { + Notify = "notify", + DontNotify = "dont_notify", + Coalesce = "coalesce", +} + +type PushAction = Action | ISetTweak; + +interface IEventMatchCondition { + kind: ConditionKind.EventMatch; + key: string; + pattern: string; + value?: string; // Legacy field? +} + +interface IContainsDisplayNameCondition { + kind: ConditionKind.ContainsDisplayName; +} + +interface IRoomMemberCountCondition { + kind: ConditionKind.RoomMemberCount; + is: string; +} + +interface ISenderNotificationPermissionCondition { + kind: ConditionKind.SenderNotificationPermission; + key: string; +} + +type PushCondition = IEventMatchCondition + | IContainsDisplayNameCondition + | IRoomMemberCountCondition + | ISenderNotificationPermissionCondition; + +interface IPushRule { + // eslint-disable-next-line camelcase + rule_id: string; + default?: boolean; + enabled?: boolean; + conditions: PushCondition[]; + actions: PushAction[]; + kind?: RuleKind; // this is a locally monkey wrenched field +} + +interface IRuleset { + [RuleKind.Content]: IPushRule[]; + [RuleKind.Override]: IPushRule[]; + [RuleKind.Room]: IPushRule[]; + [RuleKind.Sender]: IPushRule[]; + [RuleKind.Underride]: IPushRule[]; +} + +export interface IRulesets { + global: IRuleset; +} + +// The default override rules to apply to the push rules that arrive from the server. +// We do this for two reasons: +// 1. Synapse is unlikely to send us the push rule in an incremental sync - see +// https://github.com/matrix-org/synapse/pull/4867#issuecomment-481446072 for +// more details. +// 2. We often want to start using push rules ahead of the server supporting them, +// and so we can put them here. +const DEFAULT_OVERRIDE_RULES: IPushRule[] = [ + { + // For homeservers which don't support MSC1930 yet + rule_id: ".m.rule.tombstone", + default: true, + enabled: true, + conditions: [ + { + kind: ConditionKind.EventMatch, + key: "type", + pattern: "m.room.tombstone", + }, + { + kind: ConditionKind.EventMatch, + key: "state_key", + pattern: "", + }, + ], + actions: [ + Action.Notify, + { + set_tweak: Tweak.Highlight, + value: true, + }, + ], + }, + { + // For homeservers which don't support MSC2153 yet + rule_id: ".m.rule.reaction", + default: true, + enabled: true, + conditions: [ + { + kind: ConditionKind.EventMatch, + key: "type", + pattern: "m.reaction", + }, + ], + actions: [ + Action.DontNotify, + ], + }, +]; + +export interface IActionsObject { + notify: boolean; + tweaks: Partial>; +} + +export class PushProcessor { + /** + * Convert a list of actions into a object with the actions as keys and their values + * eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ] + * becomes { notify: true, tweaks: { sound: 'default' } } + * @param {array} actionList The actions list + * + * @return {object} A object with key 'notify' (true or false) and an object of actions + */ + public static actionListToActionsObject(actionList: PushAction[]): IActionsObject { + const actionObj: IActionsObject = { notify: false, tweaks: {} }; + for (let i = 0; i < actionList.length; ++i) { + const action = actionList[i]; + if (action === Action.Notify) { + actionObj.notify = true; + } else if (typeof action === 'object') { + if (action.value === undefined) { + action.value = true; + } + actionObj.tweaks[action.set_tweak] = action.value; + } + } + return actionObj; + } + + /** + * Rewrites conditions on a client's push rules to match the defaults + * where applicable. Useful for upgrading push rules to more strict + * conditions when the server is falling behind on defaults. + * @param {object} incomingRules The client's existing push rules + * @returns {object} The rewritten rules + */ + public static rewriteDefaultRules(incomingRules: IRulesets): IRulesets { + let newRules: IRulesets = JSON.parse(JSON.stringify(incomingRules)); // deep clone + + // These lines are mostly to make the tests happy. We shouldn't run into these + // properties missing in practice. + if (!newRules) newRules = {} as IRulesets; + if (!newRules.global) newRules.global = {} as IRuleset; + if (!newRules.global.override) newRules.global.override = []; + + // Merge the client-level defaults with the ones from the server + const globalOverrides = newRules.global.override; + for (const override of DEFAULT_OVERRIDE_RULES) { + const existingRule = globalOverrides + .find((r) => r.rule_id === override.rule_id); + + if (existingRule) { + // Copy over the actions, default, and conditions. Don't touch the user's + // preference. + existingRule.default = override.default; + existingRule.conditions = override.conditions; + existingRule.actions = override.actions; + } else { + // Add the rule + const ruleId = override.rule_id; + logger.warn(`Adding default global override for ${ruleId}`); + globalOverrides.push(override); + } + } + + return newRules; + } + + private static cachedGlobToRegex: Record = {}; // $glob: RegExp + + /** + * Construct a Push Processor. + * @constructor + * @param {Object} client The Matrix client object to use + */ + constructor(private readonly client: MatrixClient) {} + + private matchingRuleFromKindSet(ev: MatrixEvent, kindset: IRuleset): IPushRule { + for (let ruleKindIndex = 0; ruleKindIndex < RULEKINDS_IN_ORDER.length; ++ruleKindIndex) { + const kind = RULEKINDS_IN_ORDER[ruleKindIndex]; + const ruleset = kindset[kind]; + if (!ruleset) { + continue; + } + + for (let ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) { + const rule = ruleset[ruleIndex]; + if (!rule.enabled) { + continue; + } + + const rawrule = this.templateRuleToRaw(kind, rule); + if (!rawrule) { + continue; + } + + if (this.ruleMatchesEvent(rawrule, ev)) { + rule.kind = kind; + return rule; + } + } + } + return null; + } + + private templateRuleToRaw(kind: RuleKind, tprule: any): any { + const rawrule = { + 'rule_id': tprule.rule_id, + 'actions': tprule.actions, + 'conditions': [], + }; + switch (kind) { + case RuleKind.Underride: + case RuleKind.Override: + rawrule.conditions = tprule.conditions; + break; + case RuleKind.Room: + if (!tprule.rule_id) { + return null; + } + rawrule.conditions.push({ + 'kind': ConditionKind.EventMatch, + 'key': 'room_id', + 'value': tprule.rule_id, + }); + break; + case RuleKind.Sender: + if (!tprule.rule_id) { + return null; + } + rawrule.conditions.push({ + 'kind': ConditionKind.EventMatch, + 'key': 'user_id', + 'value': tprule.rule_id, + }); + break; + case RuleKind.Content: + if (!tprule.pattern) { + return null; + } + rawrule.conditions.push({ + 'kind': ConditionKind.EventMatch, + 'key': 'content.body', + 'pattern': tprule.pattern, + }); + break; + } + return rawrule; + } + + private eventFulfillsCondition(cond: PushCondition, ev: MatrixEvent): boolean { + switch (cond.kind) { + case ConditionKind.EventMatch: + return this.eventFulfillsEventMatchCondition(cond, ev); + case ConditionKind.ContainsDisplayName: + return this.eventFulfillsDisplayNameCondition(cond, ev); + case ConditionKind.RoomMemberCount: + return this.eventFulfillsRoomMemberCountCondition(cond, ev); + case ConditionKind.SenderNotificationPermission: + return this.eventFulfillsSenderNotifPermCondition(cond, ev); + } + + // unknown conditions: we previously matched all unknown conditions, + // but given that rules can be added to the base rules on a server, + // it's probably better to not match unknown conditions. + return false; + } + + private eventFulfillsSenderNotifPermCondition( + cond: ISenderNotificationPermissionCondition, + ev: MatrixEvent, + ): boolean { + const notifLevelKey = cond['key']; + if (!notifLevelKey) { + return false; + } + + const room = this.client.getRoom(ev.getRoomId()); + if (!room?.currentState) { + return false; + } + + // Note that this should not be the current state of the room but the state at + // the point the event is in the DAG. Unfortunately the js-sdk does not store + // this. + return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender()); + } + + private eventFulfillsRoomMemberCountCondition(cond: IRoomMemberCountCondition, ev: MatrixEvent): boolean { + if (!cond.is) { + return false; + } + + const room = this.client.getRoom(ev.getRoomId()); + if (!room || !room.currentState || !room.currentState.members) { + return false; + } + + const memberCount = room.currentState.getJoinedMemberCount(); + + const m = cond.is.match(/^([=<>]*)([0-9]*)$/); + if (!m) { + return false; + } + const ineq = m[1]; + const rhs = parseInt(m[2]); + if (isNaN(rhs)) { + return false; + } + switch (ineq) { + case '': + case '==': + return memberCount == rhs; + case '<': + return memberCount < rhs; + case '>': + return memberCount > rhs; + case '<=': + return memberCount <= rhs; + case '>=': + return memberCount >= rhs; + default: + return false; + } + } + + private eventFulfillsDisplayNameCondition(cond: IContainsDisplayNameCondition, ev: MatrixEvent): boolean { + let content = ev.getContent(); + if (ev.isEncrypted() && ev.getClearContent()) { + content = ev.getClearContent(); + } + if (!content || !content.body || typeof content.body != 'string') { + return false; + } + + const room = this.client.getRoom(ev.getRoomId()); + if (!room || !room.currentState || !room.currentState.members || + !room.currentState.getMember(this.client.credentials.userId)) { + return false; + } + + const displayName = room.currentState.getMember(this.client.credentials.userId).name; + + // N.B. we can't use \b as it chokes on unicode. however \W seems to be okay + // as shorthand for [^0-9A-Za-z_]. + const pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i'); + return content.body.search(pat) > -1; + } + + private eventFulfillsEventMatchCondition(cond: IEventMatchCondition, ev: MatrixEvent): boolean { + if (!cond.key) { + return false; + } + + const val = this.valueForDottedKey(cond.key, ev); + if (typeof val !== 'string') { + return false; + } + + if (cond.value) { + return cond.value === val; + } + + if (typeof cond.pattern !== 'string') { + return false; + } + + let regex; + + if (cond.key == 'content.body') { + regex = this.createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)'); + } else { + regex = this.createCachedRegex('^', cond.pattern, '$'); + } + + return !!val.match(regex); + } + + private createCachedRegex(prefix: string, glob: string, suffix: string): RegExp { + if (PushProcessor.cachedGlobToRegex[glob]) { + return PushProcessor.cachedGlobToRegex[glob]; + } + PushProcessor.cachedGlobToRegex[glob] = new RegExp( + prefix + globToRegexp(glob) + suffix, + 'i', // Case insensitive + ); + return PushProcessor.cachedGlobToRegex[glob]; + } + + private valueForDottedKey(key: string, ev: MatrixEvent): any { + const parts = key.split('.'); + let val; + + // special-case the first component to deal with encrypted messages + const firstPart = parts[0]; + if (firstPart === 'content') { + val = ev.getContent(); + parts.shift(); + } else if (firstPart === 'type') { + val = ev.getType(); + parts.shift(); + } else { + // use the raw event for any other fields + val = ev.event; + } + + while (parts.length > 0) { + const thisPart = parts.shift(); + if (isNullOrUndefined(val[thisPart])) { + return null; + } + val = val[thisPart]; + } + return val; + } + + private matchingRuleForEventWithRulesets(ev: MatrixEvent, rulesets): IPushRule { + if (!rulesets) { + return null; + } + if (ev.getSender() === this.client.credentials.userId) { + return null; + } + + return this.matchingRuleFromKindSet(ev, rulesets.global); + } + + private pushActionsForEventAndRulesets(ev: MatrixEvent, rulesets): IActionsObject { + const rule = this.matchingRuleForEventWithRulesets(ev, rulesets); + if (!rule) { + return {} as IActionsObject; + } + + const actionObj = PushProcessor.actionListToActionsObject(rule.actions); + + // Some actions are implicit in some situations: we add those here + if (actionObj.tweaks.highlight === undefined) { + // if it isn't specified, highlight if it's a content + // rule but otherwise not + actionObj.tweaks.highlight = (rule.kind == 'content'); + } + + return actionObj; + } + + public ruleMatchesEvent(rule: IPushRule, ev: MatrixEvent): boolean { + let ret = true; + for (let i = 0; i < rule.conditions.length; ++i) { + const cond = rule.conditions[i]; + // @ts-ignore + ret &= this.eventFulfillsCondition(cond, ev); + } + //console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match")); + return ret; + } + + /** + * Get the user's push actions for the given event + * + * @param {module:models/event.MatrixEvent} ev + * + * @return {PushAction} + */ + public actionsForEvent(ev: MatrixEvent): IActionsObject { + return this.pushActionsForEventAndRulesets(ev, this.client.pushRules); + } + + /** + * Get one of the users push rules by its ID + * + * @param {string} ruleId The ID of the rule to search for + * @return {object} The push rule, or null if no such rule was found + */ + public getPushRuleById(ruleId: string): IPushRule { + for (const scope of ['global']) { + if (this.client.pushRules[scope] === undefined) continue; + + for (const kind of RULEKINDS_IN_ORDER) { + if (this.client.pushRules[scope][kind] === undefined) continue; + + for (const rule of this.client.pushRules[scope][kind]) { + if (rule.rule_id === ruleId) return rule; + } + } + } + return null; + } +} + +/** + * @typedef {Object} PushAction + * @type {Object} + * @property {boolean} notify Whether this event should notify the user or not. + * @property {Object} tweaks How this event should be notified. + * @property {boolean} tweaks.highlight Whether this event should be highlighted + * on the UI. + * @property {boolean} tweaks.sound Whether this notification should produce a + * noise. + */ + diff --git a/src/scheduler.js b/src/scheduler.js deleted file mode 100644 index 37e231ce0..000000000 --- a/src/scheduler.js +++ /dev/null @@ -1,327 +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. -*/ - -/** - * This is an internal module which manages queuing, scheduling and retrying - * of requests. - * @module scheduler - */ -import * as utils from "./utils"; -import { logger } from './logger'; - -const DEBUG = false; // set true to enable console logging. - -/** - * Construct a scheduler for Matrix. Requires - * {@link module:scheduler~MatrixScheduler#setProcessFunction} to be provided - * with a way of processing events. - * @constructor - * @param {module:scheduler~retryAlgorithm} retryAlgorithm Optional. The retry - * algorithm to apply when determining when to try to send an event again. - * Defaults to {@link module:scheduler~MatrixScheduler.RETRY_BACKOFF_RATELIMIT}. - * @param {module:scheduler~queueAlgorithm} queueAlgorithm Optional. The queuing - * algorithm to apply when determining which events should be sent before the - * given event. Defaults to {@link module:scheduler~MatrixScheduler.QUEUE_MESSAGES}. - */ -export function MatrixScheduler(retryAlgorithm, queueAlgorithm) { - this.retryAlgorithm = retryAlgorithm || MatrixScheduler.RETRY_BACKOFF_RATELIMIT; - this.queueAlgorithm = queueAlgorithm || MatrixScheduler.QUEUE_MESSAGES; - this._queues = { - // queueName: [{ - // event: MatrixEvent, // event to send - // defer: Deferred, // defer to resolve/reject at the END of the retries - // attempts: Number // number of times we've called processFn - // }, ...] - }; - this._activeQueues = []; - this._procFn = null; -} - -/** - * Retrieve a queue based on an event. The event provided does not need to be in - * the queue. - * @param {MatrixEvent} event An event to get the queue for. - * @return {?Array} A shallow copy of events in the queue or null. - * Modifying this array will not modify the list itself. Modifying events in - * this array will modify the underlying event in the queue. - * @see MatrixScheduler.removeEventFromQueue To remove an event from the queue. - */ -MatrixScheduler.prototype.getQueueForEvent = function(event) { - const name = this.queueAlgorithm(event); - if (!name || !this._queues[name]) { - return null; - } - return this._queues[name].map(function(obj) { - return obj.event; - }); -}; - -/** - * Remove this event from the queue. The event is equal to another event if they - * have the same ID returned from event.getId(). - * @param {MatrixEvent} event The event to remove. - * @return {boolean} True if this event was removed. - */ -MatrixScheduler.prototype.removeEventFromQueue = function(event) { - const name = this.queueAlgorithm(event); - if (!name || !this._queues[name]) { - return false; - } - let removed = false; - utils.removeElement(this._queues[name], function(element) { - if (element.event.getId() === event.getId()) { - // XXX we should probably reject the promise? - // https://github.com/matrix-org/matrix-js-sdk/issues/496 - removed = true; - return true; - } - }); - return removed; -}; - -/** - * Set the process function. Required for events in the queue to be processed. - * If set after events have been added to the queue, this will immediately start - * processing them. - * @param {module:scheduler~processFn} fn The function that can process events - * in the queue. - */ -MatrixScheduler.prototype.setProcessFunction = function(fn) { - this._procFn = fn; - _startProcessingQueues(this); -}; - -/** - * Queue an event if it is required and start processing queues. - * @param {MatrixEvent} event The event that may be queued. - * @return {?Promise} A promise if the event was queued, which will be - * resolved or rejected in due time, else null. - */ -MatrixScheduler.prototype.queueEvent = function(event) { - const queueName = this.queueAlgorithm(event); - if (!queueName) { - return null; - } - // add the event to the queue and make a deferred for it. - if (!this._queues[queueName]) { - this._queues[queueName] = []; - } - const defer = utils.defer(); - this._queues[queueName].push({ - event: event, - defer: defer, - attempts: 0, - }); - debuglog( - "Queue algorithm dumped event %s into queue '%s'", - event.getId(), queueName, - ); - _startProcessingQueues(this); - return defer.promise; -}; - -/** - * Retries events up to 4 times using exponential backoff. This produces wait - * times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the - * failure was due to a rate limited request, the time specified in the error is - * waited before being retried. - * @param {MatrixEvent} event - * @param {Number} attempts - * @param {MatrixError} err - * @return {Number} - * @see module:scheduler~retryAlgorithm - */ -MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) { - if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) { - // client error; no amount of retrying with save you now. - return -1; - } - // we ship with browser-request which returns { cors: rejected } when trying - // with no connection, so if we match that, give up since they have no conn. - if (err.cors === "rejected") { - return -1; - } - - // if event that we are trying to send is too large in any way then retrying won't help - if (err.name === "M_TOO_LARGE") { - return -1; - } - - if (err.name === "M_LIMIT_EXCEEDED") { - const waitTime = err.data.retry_after_ms; - if (waitTime > 0) { - return waitTime; - } - } - if (attempts > 4) { - return -1; // give up - } - return (1000 * Math.pow(2, attempts)); -}; - -/** - * Queues m.room.message events and lets other events continue - * concurrently. - * @param {MatrixEvent} event - * @return {string} - * @see module:scheduler~queueAlgorithm - */ -MatrixScheduler.QUEUE_MESSAGES = function(event) { - // enqueue messages or events that associate with another event (redactions and relations) - if (event.getType() === "m.room.message" || event.hasAssocation()) { - // put these events in the 'message' queue. - return "message"; - } - // allow all other events continue concurrently. - return null; -}; - -function _startProcessingQueues(scheduler) { - if (!scheduler._procFn) { - return; - } - // for each inactive queue with events in them - Object.keys(scheduler._queues) - .filter(function(queueName) { - return scheduler._activeQueues.indexOf(queueName) === -1 && - scheduler._queues[queueName].length > 0; - }) - .forEach(function(queueName) { - // mark the queue as active - scheduler._activeQueues.push(queueName); - // begin processing the head of the queue - debuglog("Spinning up queue: '%s'", queueName); - _processQueue(scheduler, queueName); - }); -} - -function _processQueue(scheduler, queueName) { - // get head of queue - const obj = _peekNextEvent(scheduler, queueName); - if (!obj) { - // queue is empty. Mark as inactive and stop recursing. - const index = scheduler._activeQueues.indexOf(queueName); - if (index >= 0) { - scheduler._activeQueues.splice(index, 1); - } - debuglog("Stopping queue '%s' as it is now empty", queueName); - return; - } - debuglog( - "Queue '%s' has %s pending events", - queueName, scheduler._queues[queueName].length, - ); - // fire the process function and if it resolves, resolve the deferred. Else - // invoke the retry algorithm. - - // First wait for a resolved promise, so the resolve handlers for - // the deferred of the previously sent event can run. - // This way enqueued relations/redactions to enqueued events can receive - // the remove id of their target before being sent. - Promise.resolve().then(() => { - return scheduler._procFn(obj.event); - }).then(function(res) { - // remove this from the queue - _removeNextEvent(scheduler, queueName); - debuglog("Queue '%s' sent event %s", queueName, obj.event.getId()); - obj.defer.resolve(res); - // keep processing - _processQueue(scheduler, queueName); - }, function(err) { - obj.attempts += 1; - // ask the retry algorithm when/if we should try again - const waitTimeMs = scheduler.retryAlgorithm(obj.event, obj.attempts, err); - debuglog( - "retry(%s) err=%s event_id=%s waitTime=%s", - obj.attempts, err, obj.event.getId(), waitTimeMs, - ); - if (waitTimeMs === -1) { // give up (you quitter!) - debuglog( - "Queue '%s' giving up on event %s", queueName, obj.event.getId(), - ); - // remove this from the queue - _removeNextEvent(scheduler, queueName); - obj.defer.reject(err); - // process next event - _processQueue(scheduler, queueName); - } else { - setTimeout(function() { - _processQueue(scheduler, queueName); - }, waitTimeMs); - } - }); -} - -function _peekNextEvent(scheduler, queueName) { - const queue = scheduler._queues[queueName]; - if (!Array.isArray(queue)) { - return null; - } - return queue[0]; -} - -function _removeNextEvent(scheduler, queueName) { - const queue = scheduler._queues[queueName]; - if (!Array.isArray(queue)) { - return null; - } - return queue.shift(); -} - -function debuglog() { - if (DEBUG) { - logger.log(...arguments); - } -} - -/** - * The retry algorithm to apply when retrying events. To stop retrying, return - * -1. If this event was part of a queue, it will be removed from - * the queue. - * @callback retryAlgorithm - * @param {MatrixEvent} event The event being retried. - * @param {Number} attempts The number of failed attempts. This will always be - * >= 1. - * @param {MatrixError} err The most recent error message received when trying - * to send this event. - * @return {Number} The number of milliseconds to wait before trying again. If - * this is 0, the request will be immediately retried. If this is - * -1, the event will be marked as - * {@link module:models/event.EventStatus.NOT_SENT} and will not be retried. - */ - -/** - * The queuing algorithm to apply to events. This function must be idempotent as - * it may be called multiple times with the same event. All queues created are - * serviced in a FIFO manner. To send the event ASAP, return null - * which will not put this event in a queue. Events that fail to send that form - * part of a queue will be removed from the queue and the next event in the - * queue will be sent. - * @callback queueAlgorithm - * @param {MatrixEvent} event The event to be sent. - * @return {string} The name of the queue to put the event into. If a queue with - * this name does not exist, it will be created. If this is null, - * the event is not put into a queue and will be sent concurrently. - */ - - /** - * The function to invoke to process (send) events in the queue. - * @callback processFn - * @param {MatrixEvent} event The event to send. - * @return {Promise} Resolved/rejected depending on the outcome of the request. - */ - diff --git a/src/scheduler.ts b/src/scheduler.ts new file mode 100644 index 000000000..845ea1c44 --- /dev/null +++ b/src/scheduler.ts @@ -0,0 +1,327 @@ +/* +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. +*/ + +/** + * This is an internal module which manages queuing, scheduling and retrying + * of requests. + * @module scheduler + */ +import * as utils from "./utils"; +import { logger } from './logger'; +import { MatrixEvent } from "./models/event"; +import { EventType } from "./@types/event"; +import { IDeferred } from "./utils"; +import { MatrixError } from "./http-api"; +import { ISendEventResponse } from "./@types/requests"; + +const DEBUG = false; // set true to enable console logging. + +interface IQueueEntry { + event: MatrixEvent; + defer: IDeferred; + attempts: number; +} + +type ProcessFunction = (event: MatrixEvent) => Promise; + +/** + * Construct a scheduler for Matrix. Requires + * {@link module:scheduler~MatrixScheduler#setProcessFunction} to be provided + * with a way of processing events. + * @constructor + * @param {module:scheduler~retryAlgorithm} retryAlgorithm Optional. The retry + * algorithm to apply when determining when to try to send an event again. + * Defaults to {@link module:scheduler~MatrixScheduler.RETRY_BACKOFF_RATELIMIT}. + * @param {module:scheduler~queueAlgorithm} queueAlgorithm Optional. The queuing + * algorithm to apply when determining which events should be sent before the + * given event. Defaults to {@link module:scheduler~MatrixScheduler.QUEUE_MESSAGES}. + */ +// eslint-disable-next-line camelcase +export class MatrixScheduler { + /** + * Retries events up to 4 times using exponential backoff. This produces wait + * times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the + * failure was due to a rate limited request, the time specified in the error is + * waited before being retried. + * @param {MatrixEvent} event + * @param {Number} attempts + * @param {MatrixError} err + * @return {Number} + * @see module:scheduler~retryAlgorithm + */ + public static RETRY_BACKOFF_RATELIMIT(event: MatrixEvent, attempts: number, err: MatrixError): number { + if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) { + // client error; no amount of retrying with save you now. + return -1; + } + // we ship with browser-request which returns { cors: rejected } when trying + // with no connection, so if we match that, give up since they have no conn. + if (err.cors === "rejected") { + return -1; + } + + // if event that we are trying to send is too large in any way then retrying won't help + if (err.name === "M_TOO_LARGE") { + return -1; + } + + if (err.name === "M_LIMIT_EXCEEDED") { + const waitTime = err.data.retry_after_ms; + if (waitTime > 0) { + return waitTime; + } + } + if (attempts > 4) { + return -1; // give up + } + return (1000 * Math.pow(2, attempts)); + } + + /** + * Queues m.room.message events and lets other events continue + * concurrently. + * @param {MatrixEvent} event + * @return {string} + * @see module:scheduler~queueAlgorithm + */ + public static QUEUE_MESSAGES(event: MatrixEvent) { + // enqueue messages or events that associate with another event (redactions and relations) + if (event.getType() === EventType.RoomMessage || event.hasAssocation()) { + // put these events in the 'message' queue. + return "message"; + } + // allow all other events continue concurrently. + return null; + } + + // queueName: [{ + // event: MatrixEvent, // event to send + // defer: Deferred, // defer to resolve/reject at the END of the retries + // attempts: Number // number of times we've called processFn + // }, ...] + private readonly queues: Record[]> = {}; + private activeQueues: string[] = []; + private procFn: ProcessFunction = null; + + constructor( + public readonly retryAlgorithm = MatrixScheduler.RETRY_BACKOFF_RATELIMIT, + public readonly queueAlgorithm = MatrixScheduler.QUEUE_MESSAGES, + ) {} + + /** + * Retrieve a queue based on an event. The event provided does not need to be in + * the queue. + * @param {MatrixEvent} event An event to get the queue for. + * @return {?Array} A shallow copy of events in the queue or null. + * Modifying this array will not modify the list itself. Modifying events in + * this array will modify the underlying event in the queue. + * @see MatrixScheduler.removeEventFromQueue To remove an event from the queue. + */ + public getQueueForEvent(event: MatrixEvent): MatrixEvent[] { + const name = this.queueAlgorithm(event); + if (!name || !this.queues[name]) { + return null; + } + return this.queues[name].map(function(obj) { + return obj.event; + }); + } + + /** + * Remove this event from the queue. The event is equal to another event if they + * have the same ID returned from event.getId(). + * @param {MatrixEvent} event The event to remove. + * @return {boolean} True if this event was removed. + */ + public removeEventFromQueue(event: MatrixEvent): boolean { + const name = this.queueAlgorithm(event); + if (!name || !this.queues[name]) { + return false; + } + let removed = false; + utils.removeElement(this.queues[name], (element) => { + if (element.event.getId() === event.getId()) { + // XXX we should probably reject the promise? + // https://github.com/matrix-org/matrix-js-sdk/issues/496 + removed = true; + return true; + } + }); + return removed; + } + + /** + * Set the process function. Required for events in the queue to be processed. + * If set after events have been added to the queue, this will immediately start + * processing them. + * @param {module:scheduler~processFn} fn The function that can process events + * in the queue. + */ + public setProcessFunction(fn: ProcessFunction): void { + this.procFn = fn; + this.startProcessingQueues(); + } + + /** + * Queue an event if it is required and start processing queues. + * @param {MatrixEvent} event The event that may be queued. + * @return {?Promise} A promise if the event was queued, which will be + * resolved or rejected in due time, else null. + */ + public queueEvent(event: MatrixEvent): Promise | null { + const queueName = this.queueAlgorithm(event); + if (!queueName) { + return null; + } + // add the event to the queue and make a deferred for it. + if (!this.queues[queueName]) { + this.queues[queueName] = []; + } + const defer = utils.defer(); + this.queues[queueName].push({ + event: event, + defer: defer, + attempts: 0, + }); + debuglog("Queue algorithm dumped event %s into queue '%s'", event.getId(), queueName); + this.startProcessingQueues(); + return defer.promise; + } + + private startProcessingQueues(): void { + if (!this.procFn) return; + // for each inactive queue with events in them + Object.keys(this.queues) + .filter((queueName) => { + return this.activeQueues.indexOf(queueName) === -1 && + this.queues[queueName].length > 0; + }) + .forEach((queueName) => { + // mark the queue as active + this.activeQueues.push(queueName); + // begin processing the head of the queue + debuglog("Spinning up queue: '%s'", queueName); + this.processQueue(queueName); + }); + } + + private processQueue = (queueName: string): void => { + // get head of queue + const obj = this.peekNextEvent(queueName); + if (!obj) { + // queue is empty. Mark as inactive and stop recursing. + const index = this.activeQueues.indexOf(queueName); + if (index >= 0) { + this.activeQueues.splice(index, 1); + } + debuglog("Stopping queue '%s' as it is now empty", queueName); + return; + } + debuglog("Queue '%s' has %s pending events", queueName, this.queues[queueName].length); + // fire the process function and if it resolves, resolve the deferred. Else + // invoke the retry algorithm. + + // First wait for a resolved promise, so the resolve handlers for + // the deferred of the previously sent event can run. + // This way enqueued relations/redactions to enqueued events can receive + // the remove id of their target before being sent. + Promise.resolve().then(() => { + return this.procFn(obj.event); + }).then((res) => { + // remove this from the queue + this.removeNextEvent(queueName); + debuglog("Queue '%s' sent event %s", queueName, obj.event.getId()); + obj.defer.resolve(res); + // keep processing + this.processQueue(queueName); + }, (err) => { + obj.attempts += 1; + // ask the retry algorithm when/if we should try again + const waitTimeMs = this.retryAlgorithm(obj.event, obj.attempts, err); + debuglog("retry(%s) err=%s event_id=%s waitTime=%s", obj.attempts, err, obj.event.getId(), waitTimeMs); + if (waitTimeMs === -1) { // give up (you quitter!) + debuglog("Queue '%s' giving up on event %s", queueName, obj.event.getId()); + // remove this from the queue + this.removeNextEvent(queueName); + obj.defer.reject(err); + // process next event + this.processQueue(queueName); + } else { + setTimeout(this.processQueue, waitTimeMs, queueName); + } + }); + }; + + private peekNextEvent(queueName: string): IQueueEntry { + const queue = this.queues[queueName]; + if (!Array.isArray(queue)) { + return null; + } + return queue[0]; + } + + private removeNextEvent(queueName: string): IQueueEntry { + const queue = this.queues[queueName]; + if (!Array.isArray(queue)) { + return null; + } + return queue.shift(); + } +} + +function debuglog(...args) { + if (DEBUG) { + logger.log(...args); + } +} + +/** + * The retry algorithm to apply when retrying events. To stop retrying, return + * -1. If this event was part of a queue, it will be removed from + * the queue. + * @callback retryAlgorithm + * @param {MatrixEvent} event The event being retried. + * @param {Number} attempts The number of failed attempts. This will always be + * >= 1. + * @param {MatrixError} err The most recent error message received when trying + * to send this event. + * @return {Number} The number of milliseconds to wait before trying again. If + * this is 0, the request will be immediately retried. If this is + * -1, the event will be marked as + * {@link module:models/event.EventStatus.NOT_SENT} and will not be retried. + */ + +/** + * The queuing algorithm to apply to events. This function must be idempotent as + * it may be called multiple times with the same event. All queues created are + * serviced in a FIFO manner. To send the event ASAP, return null + * which will not put this event in a queue. Events that fail to send that form + * part of a queue will be removed from the queue and the next event in the + * queue will be sent. + * @callback queueAlgorithm + * @param {MatrixEvent} event The event to be sent. + * @return {string} The name of the queue to put the event into. If a queue with + * this name does not exist, it will be created. If this is null, + * the event is not put into a queue and will be sent concurrently. + */ + +/** + * The function to invoke to process (send) events in the queue. + * @callback processFn + * @param {MatrixEvent} event The event to send. + * @return {Promise} Resolved/rejected depending on the outcome of the request. + */ + diff --git a/src/sync.ts b/src/sync.ts index 831c7b084..8049d03f6 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -30,7 +30,7 @@ import * as utils from "./utils"; import { IDeferred } from "./utils"; import { Filter } from "./filter"; import { EventTimeline } from "./models/event-timeline"; -import { PushProcessor } from "./pushprocessor"; +import { IRulesets, PushProcessor } from "./pushprocessor"; import { logger } from './logger'; import { InvalidStoreError } from './errors'; import { IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client"; @@ -52,6 +52,7 @@ import { import { MatrixEvent } from "./models/event"; import { MatrixError } from "./http-api"; import { ISavedSync } from "./store"; +import { EventType } from "./@types/event"; const DEBUG = true; @@ -1066,9 +1067,9 @@ export class SyncApi { // honour push rules that were previously cached. Base rules // will be updated when we receive push rules via getPushRules // (see sync) before syncing over the network. - if (accountDataEvent.getType() === 'm.push_rules') { + if (accountDataEvent.getType() === EventType.PushRules) { const rules = accountDataEvent.getContent(); - client.pushRules = PushProcessor.rewriteDefaultRules(rules); + client.pushRules = PushProcessor.rewriteDefaultRules(rules as IRulesets); } const prevEvent = prevEventsMap[accountDataEvent.getId()]; client.emit("accountData", accountDataEvent, prevEvent); diff --git a/src/utils.ts b/src/utils.ts index 008388509..003aa6986 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -398,7 +398,7 @@ export function escapeRegExp(string: string): string { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } -export function globToRegexp(glob: string, extended: any): string { +export function globToRegexp(glob: string, extended?: any): string { extended = typeof(extended) === 'boolean' ? extended : true; // From // https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132 @@ -443,7 +443,7 @@ export interface IDeferred { } // Returns a Deferred -export function defer(): IDeferred { +export function defer(): IDeferred { let resolve; let reject; From f1a0c46a29778c042b5eb2f8d786cef626485446 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 16 Jul 2021 17:45:07 +0100 Subject: [PATCH 02/65] Deprecate groups APIs --- src/client.ts | 26 ++++++++++++++++++++++++++ src/models/group.js | 4 ++++ src/store/index.ts | 3 +++ src/store/memory.ts | 3 +++ src/store/stub.ts | 3 +++ 5 files changed, 39 insertions(+) diff --git a/src/client.ts b/src/client.ts index 904958674..c7f241552 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2949,6 +2949,7 @@ export class MatrixClient extends EventEmitter { * has been emitted. * @param {string} groupId The group ID * @return {Group} The Group or null if the group is not known or there is no data store. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public getGroup(groupId: string): Group { return this.store.getGroup(groupId); @@ -2957,6 +2958,7 @@ export class MatrixClient extends EventEmitter { /** * Retrieve all known groups. * @return {Group[]} A list of groups, or an empty list if there is no data store. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public getGroups(): Group[] { return this.store.getGroups(); @@ -8094,6 +8096,7 @@ export class MatrixClient extends EventEmitter { * @param {string} groupId * @return {Promise} Resolves: Group summary object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public getGroupSummary(groupId: string): Promise { const path = utils.encodeUri("/groups/$groupId/summary", { $groupId: groupId }); @@ -8104,6 +8107,7 @@ export class MatrixClient extends EventEmitter { * @param {string} groupId * @return {Promise} Resolves: Group profile object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public getGroupProfile(groupId: string): Promise { const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId }); @@ -8119,6 +8123,7 @@ export class MatrixClient extends EventEmitter { * @param {string=} profile.long_description A longer HTML description of the room * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public setGroupProfile(groupId: string, profile: any): Promise { const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId }); @@ -8135,6 +8140,7 @@ export class MatrixClient extends EventEmitter { * required to join. * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public setGroupJoinPolicy(groupId: string, policy: any): Promise { const path = utils.encodeUri( @@ -8152,6 +8158,7 @@ export class MatrixClient extends EventEmitter { * @param {string} groupId * @return {Promise} Resolves: Group users list object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public getGroupUsers(groupId: string): Promise { const path = utils.encodeUri("/groups/$groupId/users", { $groupId: groupId }); @@ -8162,6 +8169,7 @@ export class MatrixClient extends EventEmitter { * @param {string} groupId * @return {Promise} Resolves: Group users list object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public getGroupInvitedUsers(groupId: string): Promise { const path = utils.encodeUri("/groups/$groupId/invited_users", { $groupId: groupId }); @@ -8172,6 +8180,7 @@ export class MatrixClient extends EventEmitter { * @param {string} groupId * @return {Promise} Resolves: Group rooms list object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public getGroupRooms(groupId: string): Promise { const path = utils.encodeUri("/groups/$groupId/rooms", { $groupId: groupId }); @@ -8183,6 +8192,7 @@ export class MatrixClient extends EventEmitter { * @param {string} userId * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public inviteUserToGroup(groupId: string, userId: string): Promise { const path = utils.encodeUri( @@ -8197,6 +8207,7 @@ export class MatrixClient extends EventEmitter { * @param {string} userId * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public removeUserFromGroup(groupId: string, userId: string): Promise { const path = utils.encodeUri( @@ -8212,6 +8223,7 @@ export class MatrixClient extends EventEmitter { * @param {string} roleId Optional. * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public addUserToGroupSummary(groupId: string, userId: string, roleId: string): Promise { const path = utils.encodeUri( @@ -8228,6 +8240,7 @@ export class MatrixClient extends EventEmitter { * @param {string} userId * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public removeUserFromGroupSummary(groupId: string, userId: string): Promise { const path = utils.encodeUri( @@ -8243,6 +8256,7 @@ export class MatrixClient extends EventEmitter { * @param {string} categoryId Optional. * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public addRoomToGroupSummary(groupId: string, roomId: string, categoryId: string): Promise { const path = utils.encodeUri( @@ -8259,6 +8273,7 @@ export class MatrixClient extends EventEmitter { * @param {string} roomId * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public removeRoomFromGroupSummary(groupId: string, roomId: string): Promise { const path = utils.encodeUri( @@ -8274,6 +8289,7 @@ export class MatrixClient extends EventEmitter { * @param {boolean} isPublic Whether the room-group association is visible to non-members * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public addRoomToGroup(groupId: string, roomId: string, isPublic: boolean): Promise { if (isPublic === undefined) { @@ -8295,6 +8311,7 @@ export class MatrixClient extends EventEmitter { * @param {boolean} isPublic Whether the room-group association is visible to non-members * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public updateGroupRoomVisibility(groupId: string, roomId: string, isPublic: boolean): Promise { // NB: The /config API is generic but there's not much point in exposing this yet as synapse @@ -8315,6 +8332,7 @@ export class MatrixClient extends EventEmitter { * @param {string} roomId * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public removeRoomFromGroup(groupId: string, roomId: string): Promise { const path = utils.encodeUri( @@ -8329,6 +8347,7 @@ export class MatrixClient extends EventEmitter { * @param {Object} opts Additional options to send alongside the acceptance. * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public acceptGroupInvite(groupId: string, opts = null): Promise { const path = utils.encodeUri( @@ -8342,6 +8361,7 @@ export class MatrixClient extends EventEmitter { * @param {string} groupId * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public joinGroup(groupId: string): Promise { const path = utils.encodeUri( @@ -8355,6 +8375,7 @@ export class MatrixClient extends EventEmitter { * @param {string} groupId * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public leaveGroup(groupId: string): Promise { const path = utils.encodeUri( @@ -8367,6 +8388,7 @@ export class MatrixClient extends EventEmitter { /** * @return {Promise} Resolves: The groups to which the user is joined * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public getJoinedGroups(): Promise { const path = utils.encodeUri("/joined_groups", {}); @@ -8379,6 +8401,7 @@ export class MatrixClient extends EventEmitter { * @param {Object} content.profile Group profile object * @return {Promise} Resolves: Object with key group_id: id of the created group * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public createGroup(content: any): Promise { const path = utils.encodeUri("/create_group", {}); @@ -8399,6 +8422,7 @@ export class MatrixClient extends EventEmitter { * } * } * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public getPublicisedGroups(userIds: string[]): Promise { const path = utils.encodeUri("/publicised_groups", {}); @@ -8412,6 +8436,7 @@ export class MatrixClient extends EventEmitter { * @param {boolean} isPublic Whether the user's membership of this group is made public * @return {Promise} Resolves: Empty object * @return {module:http-api.MatrixError} Rejects: with an error response. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public setGroupPublicity(groupId: string, isPublic: boolean): Promise { const path = utils.encodeUri( @@ -8576,6 +8601,7 @@ export class MatrixClient extends EventEmitter { * is experimental and may change. * @event module:client~MatrixClient#"Group" * @param {Group} group The newly created, fully populated group. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. * @example * matrixClient.on("Group", function(group){ * var groupId = group.groupId; diff --git a/src/models/group.js b/src/models/group.js index e228aa1be..250e37733 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -17,6 +17,7 @@ limitations under the License. /** * @module models/group + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ import * as utils from "../utils"; @@ -34,6 +35,7 @@ import { EventEmitter } from "events"; * @prop {Object} inviter Infomation about the user who invited the logged in user * to the group, if myMembership is 'invite'. * @prop {string} inviter.userId The user ID of the inviter + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ export function Group(groupId) { this.groupId = groupId; @@ -76,6 +78,7 @@ Group.prototype.setInviter = function(inviter) { * This means the 'name' and 'avatarUrl' properties. * @event module:client~MatrixClient#"Group.profile" * @param {Group} group The group whose profile was updated. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. * @example * matrixClient.on("Group.profile", function(group){ * var name = group.name; @@ -87,6 +90,7 @@ Group.prototype.setInviter = function(inviter) { * the group is updated. * @event module:client~MatrixClient#"Group.myMembership" * @param {Group} group The group in which the user's membership changed + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. * @example * matrixClient.on("Group.myMembership", function(group){ * var myMembership = group.myMembership; diff --git a/src/store/index.ts b/src/store/index.ts index 7e52d014e..ad25a1c7e 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -56,6 +56,7 @@ export interface IStore { /** * No-op. * @param {Group} group + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ storeGroup(group: Group); @@ -63,12 +64,14 @@ export interface IStore { * No-op. * @param {string} groupId * @return {null} + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ getGroup(groupId: string): Group | null; /** * No-op. * @return {Array} An empty array. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ getGroups(): Group[]; diff --git a/src/store/memory.ts b/src/store/memory.ts index 656bd1808..7effd9f61 100644 --- a/src/store/memory.ts +++ b/src/store/memory.ts @@ -93,6 +93,7 @@ export class MemoryStore implements IStore { /** * Store the given room. * @param {Group} group The group to be stored + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public storeGroup(group: Group) { this.groups[group.groupId] = group; @@ -102,6 +103,7 @@ export class MemoryStore implements IStore { * Retrieve a group by its group ID. * @param {string} groupId The group ID. * @return {Group} The group or null. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public getGroup(groupId: string): Group | null { return this.groups[groupId] || null; @@ -110,6 +112,7 @@ export class MemoryStore implements IStore { /** * Retrieve all known groups. * @return {Group[]} A list of groups, which may be empty. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public getGroups(): Group[] { return Object.values(this.groups); diff --git a/src/store/stub.ts b/src/store/stub.ts index c2b4ea933..95b231db1 100644 --- a/src/store/stub.ts +++ b/src/store/stub.ts @@ -61,6 +61,7 @@ export class StubStore implements IStore { /** * No-op. * @param {Group} group + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public storeGroup(group: Group) {} @@ -68,6 +69,7 @@ export class StubStore implements IStore { * No-op. * @param {string} groupId * @return {null} + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public getGroup(groupId: string): Group | null { return null; @@ -76,6 +78,7 @@ export class StubStore implements IStore { /** * No-op. * @return {Array} An empty array. + * @deprecated groups/communities never made it to the spec and support for them is being discontinued. */ public getGroups(): Group[] { return []; From b15ba8c5e3176b8b13008ea3fe55522dd74514f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 30 Jul 2021 08:53:18 +0200 Subject: [PATCH 03/65] Send hangup reason if the opponent supports it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/webrtc/call.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 6a9a8be76..8facc8826 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -734,9 +734,10 @@ export class MatrixCall extends EventEmitter { // We don't want to send hangup here if we didn't even get to sending an invite if (this.state === CallState.WaitLocalMedia) return; const content = {}; - // Continue to send no reason for user hangups temporarily, until - // clients understand the user_hangup reason (voip v1) - if (reason !== CallErrorCode.UserHangup) content['reason'] = reason; + // Don't send UserHangup reason to older clients + if ((this.opponentVersion && this.opponentVersion >= 1) || reason !== CallErrorCode.UserHangup) { + content["reason"] = reason; + } this.sendVoipEvent(EventType.CallHangup, content); } From c45bc9138990c5891fa13d0d44c1f7a70ef42e4b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 5 Aug 2021 11:16:28 +0200 Subject: [PATCH 04/65] Include all TypeScript @types listed in project dependency Fixes vector-im/element-web#17743 The package @types/jest was not discovered because if types is specified, only packages listed will be included in the global scope. --- tsconfig.json | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 548bbe7fb..896c4c3b3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,12 +9,10 @@ "noImplicitAny": false, "sourceMap": true, "outDir": "./lib", - "declaration": true, - "types": [ - "node" - ] + "declaration": true }, "include": [ - "./src/**/*.ts" + "./src/**/*.ts", + "./spec/**/*.ts", ] } From 026260502b1359e6f125fc52acb6543d1a0ef4a4 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 5 Aug 2021 11:37:27 +0200 Subject: [PATCH 05/65] Fix linting issues in TypeScript test files --- spec/unit/models/MSC3089Branch.spec.ts | 7 +++---- spec/unit/models/MSC3089TreeSpace.spec.ts | 10 +++++----- spec/unit/relations.spec.ts | 10 +++++----- src/models/room-state.ts | 6 +++--- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/spec/unit/models/MSC3089Branch.spec.ts b/spec/unit/models/MSC3089Branch.spec.ts index fc8b35815..72454cc54 100644 --- a/spec/unit/models/MSC3089Branch.spec.ts +++ b/spec/unit/models/MSC3089Branch.spec.ts @@ -16,7 +16,6 @@ limitations under the License. import { MatrixClient } from "../../../src"; import { Room } from "../../../src/models/room"; -import { MatrixEvent } from "../../../src/models/event"; import { UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event"; import { EventTimelineSet } from "../../../src/models/event-timeline-set"; import { EventTimeline } from "../../../src/models/event-timeline"; @@ -25,7 +24,7 @@ import { MSC3089Branch } from "../../../src/models/MSC3089Branch"; describe("MSC3089Branch", () => { let client: MatrixClient; // @ts-ignore - TS doesn't know that this is a type - let indexEvent: MatrixEvent; + let indexEvent: any; let branch: MSC3089Branch; const branchRoomId = "!room:example.org"; @@ -47,10 +46,10 @@ describe("MSC3089Branch", () => { } }, }; - indexEvent = { + indexEvent = ({ getRoomId: () => branchRoomId, getStateKey: () => fileEventId, - }; + }); branch = new MSC3089Branch(client, indexEvent); }); diff --git a/spec/unit/models/MSC3089TreeSpace.spec.ts b/spec/unit/models/MSC3089TreeSpace.spec.ts index 951ab4c0e..ae9652e7d 100644 --- a/spec/unit/models/MSC3089TreeSpace.spec.ts +++ b/spec/unit/models/MSC3089TreeSpace.spec.ts @@ -29,7 +29,7 @@ import { MatrixError } from "../../../src/http-api"; describe("MSC3089TreeSpace", () => { let client: MatrixClient; - let room: Room; + let room: any; let tree: MSC3089TreeSpace; const roomId = "!tree:localhost"; const targetUser = "@target:example.org"; @@ -170,7 +170,7 @@ describe("MSC3089TreeSpace", () => { expect(userIds).toMatchObject([target]); return Promise.resolve(); }); - client.invite = () => Promise.resolve(); // we're not testing this here - see other tests + client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests client.sendSharedHistoryKeys = sendKeysFn; // Mock the history check as best as possible @@ -198,7 +198,7 @@ describe("MSC3089TreeSpace", () => { expect(userIds).toMatchObject([target]); return Promise.resolve(); }); - client.invite = () => Promise.resolve(); // we're not testing this here - see other tests + client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests client.sendSharedHistoryKeys = sendKeysFn; const historyVis = "joined"; // NOTE: Changed. @@ -446,9 +446,9 @@ describe("MSC3089TreeSpace", () => { // Danger: these are partial implementations for testing purposes only // @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important - let childState: { [roomId: string]: MatrixEvent[] } = {}; + let childState: { [roomId: string]: any[] } = {}; // @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important - let parentState: MatrixEvent[] = []; + let parentState: any[] = []; let parentRoom: Room; let childTrees: MSC3089TreeSpace[]; let rooms: { [roomId: string]: Room }; diff --git a/spec/unit/relations.spec.ts b/spec/unit/relations.spec.ts index 45c20c00c..27370fba0 100644 --- a/spec/unit/relations.spec.ts +++ b/spec/unit/relations.spec.ts @@ -16,11 +16,13 @@ limitations under the License. import { EventTimelineSet } from "../../src/models/event-timeline-set"; import { MatrixEvent } from "../../src/models/event"; +import { Room } from "../../src/models/room"; import { Relations } from "../../src/models/relations"; describe("Relations", function() { it("should deduplicate annotations", function() { - const relations = new Relations("m.annotation", "m.reaction"); + const room = new Room("room123", null, null); + const relations = new Relations("m.annotation", "m.reaction", room); // Create an instance of an annotation const eventData = { @@ -95,10 +97,8 @@ describe("Relations", function() { }); // Stub the room - const room = { - getPendingEvent() { return null; }, - getUnfilteredTimelineSet() { return null; }, - }; + + const room = new Room("room123", null, null); // Add the target event first, then the relation event { diff --git a/src/models/room-state.ts b/src/models/room-state.ts index 2b67ee79e..79ad48f8e 100644 --- a/src/models/room-state.ts +++ b/src/models/room-state.ts @@ -204,9 +204,9 @@ export class RoomState extends EventEmitter { * @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was * undefined, else a single event (or null if no match found). */ - public getStateEvents(eventType: string): MatrixEvent[]; - public getStateEvents(eventType: string, stateKey: string): MatrixEvent; - public getStateEvents(eventType: string, stateKey?: string) { + public getStateEvents(eventType: EventType | string): MatrixEvent[]; + public getStateEvents(eventType: EventType | string, stateKey: string): MatrixEvent; + public getStateEvents(eventType: EventType | string, stateKey?: string) { if (!this.events.has(eventType)) { // no match return stateKey === undefined ? [] : null; From b63af00579b65049d5fc473ace1f9c38ffa23cca Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 5 Aug 2021 11:56:26 +0200 Subject: [PATCH 06/65] Exclude tests when building browserify package --- package.json | 2 +- tsconfig-build.json | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 tsconfig-build.json diff --git a/package.json b/package.json index 607b21edb..63c9e731e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types", "build:types": "tsc --emitDeclarationOnly", "build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src", - "build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js", + "build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js", "build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js", "gendoc": "jsdoc -c jsdoc.json -P package.json", "lint": "yarn lint:types && yarn lint:js", diff --git a/tsconfig-build.json b/tsconfig-build.json new file mode 100644 index 000000000..50f5536d9 --- /dev/null +++ b/tsconfig-build.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "./spec/**/*.ts" + ] +} From 2bdb570f3cdcdd9b6f615987b8192e8d1355423f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 09:38:50 +0100 Subject: [PATCH 07/65] Fix type documentation on MatrixClient::setRoomMutePushRule --- src/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index ac7647479..2bcda09e4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5184,11 +5184,11 @@ export class MatrixClient extends EventEmitter { * The operation also updates MatrixClient.pushRules at the end. * @param {string} scope "global" or device-specific. * @param {string} roomId the id of the room. - * @param {string} mute the mute state. + * @param {boolean} mute the mute state. * @return {Promise} Resolves: result object * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public setRoomMutePushRule(scope: string, roomId: string, mute: string): Promise | void { + public setRoomMutePushRule(scope: string, roomId: string, mute: boolean): Promise | void { let deferred; let hasDontNotifyRule; From 5482a19979d08304dd98fb3c5a9cadd92927578e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 6 Aug 2021 09:09:03 -0600 Subject: [PATCH 08/65] Expose a getFileEvent() function off a MSC3089Branch --- src/models/MSC3089Branch.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/models/MSC3089Branch.ts b/src/models/MSC3089Branch.ts index 7bb8c356e..875f9bd7d 100644 --- a/src/models/MSC3089Branch.ts +++ b/src/models/MSC3089Branch.ts @@ -82,6 +82,19 @@ export class MSC3089Branch { * @returns {Promise<{info: IEncryptedFile, httpUrl: string}>} Information about the file. */ public async getFileInfo(): Promise<{ info: IEncryptedFile, httpUrl: string }> { + const event = await this.getFileEvent(); + + const file = event.getContent()['file']; + const httpUrl = this.client.mxcUrlToHttp(file['url']); + + return { info: file, httpUrl: httpUrl }; + } + + /** + * Gets the event the file points to. + * @returns {Promise} Resolves to the file's event. + */ + public async getFileEvent(): Promise { const room = this.client.getRoom(this.roomId); if (!room) throw new Error("Unknown room"); @@ -94,9 +107,6 @@ export class MSC3089Branch { // Sometimes the event context doesn't decrypt for us, so do that. await this.client.decryptEventIfNeeded(event, { emit: false, isRetry: false }); - const file = event.getContent()['file']; - const httpUrl = this.client.mxcUrlToHttp(file['url']); - - return { info: file, httpUrl: httpUrl }; + return event; } } From ea9459d330543215495fef9b8b91b2589604ff99 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 6 Aug 2021 23:05:00 +0100 Subject: [PATCH 09/65] Add changelog preview action --- .github/workflows/preview_changelog.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/preview_changelog.yaml diff --git a/.github/workflows/preview_changelog.yaml b/.github/workflows/preview_changelog.yaml new file mode 100644 index 000000000..d68d19361 --- /dev/null +++ b/.github/workflows/preview_changelog.yaml @@ -0,0 +1,12 @@ +name: Preview Changelog +on: + pull_request_target: + types: [ opened, edited, labeled ] +jobs: + changelog: + runs-on: ubuntu-latest + steps: + - name: Preview Changelog + uses: matrix-org/allchange@main + with: + ghToken: ${{ secrets.GITHUB_TOKEN }} From b61312feca7b9f395da6d9a56bd0886e84f31cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 8 Aug 2021 11:08:36 +0200 Subject: [PATCH 10/65] Fix screensharing crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We have to first stop the track and only then delete the feed as stopping the tracks depends on the feed being defined Signed-off-by: Šimon Brandner --- src/webrtc/call.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index ed687a69e..da529c08a 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -837,10 +837,10 @@ export class MatrixCall extends EventEmitter { for (const sender of this.screensharingSenders) { this.peerConn.removeTrack(sender); } - this.deleteFeedByStream(this.localScreensharingStream); for (const track of this.localScreensharingStream.getTracks()) { track.stop(); } + this.deleteFeedByStream(this.localScreensharingStream); return false; } } @@ -888,10 +888,10 @@ export class MatrixCall extends EventEmitter { }); sender.replaceTrack(track); - this.deleteFeedByStream(this.localScreensharingStream); for (const track of this.localScreensharingStream.getTracks()) { track.stop(); } + this.deleteFeedByStream(this.localScreensharingStream); return false; } From 8da2f91e2202f9bff68dbb875a1388140cb2917d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 9 Aug 2021 08:43:46 -0600 Subject: [PATCH 11/65] Add support for reading permissions from tree spaces This was originally thought to be able to be performed in another way, but unfortunately isn't as easy as originally predicted. --- src/models/MSC3089TreeSpace.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/models/MSC3089TreeSpace.ts b/src/models/MSC3089TreeSpace.ts index 20585eab0..80ea2b365 100644 --- a/src/models/MSC3089TreeSpace.ts +++ b/src/models/MSC3089TreeSpace.ts @@ -193,6 +193,28 @@ export class MSC3089TreeSpace { await this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, ""); } + /** + * Gets the current permissions of a user. Note that any users missing explicit permissions (or not + * in the space) will be considered Viewers. Appropriate membership checks need to be performed + * elsewhere. + * @param {string} userId The user ID to check permissions of. + * @returns {TreePermissions} The permissions for the user, defaulting to Viewer. + */ + public getPermissions(userId: string): TreePermissions { + const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); + if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels"); + + const pls = currentPls.getContent() || {}; + const viewLevel = pls['users_default'] || 0; + const editLevel = pls['events_default'] || 50; + const adminLevel = pls['events']?.[EventType.RoomPowerLevels] || 100; + + const userLevel = pls['users']?.[userId] || viewLevel; + if (userLevel >= adminLevel) return TreePermissions.Owner; + if (userLevel >= editLevel) return TreePermissions.Editor; + return TreePermissions.Viewer; + } + /** * Creates a directory under this tree space, represented as another tree space. * @param {string} name The name for the directory. From 1bcec53c6b4772658998bcc4670d76d9d8bc6de2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Aug 2021 10:22:40 +0100 Subject: [PATCH 12/65] delint --- src/interactive-auth.ts | 2 -- src/models/search-result.ts | 1 - src/scheduler.ts | 2 ++ 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/interactive-auth.ts b/src/interactive-auth.ts index 4ad0f4dd0..5d6dcd83c 100644 --- a/src/interactive-auth.ts +++ b/src/interactive-auth.ts @@ -18,8 +18,6 @@ limitations under the License. /** @module interactive-auth */ -import url from "url"; - import * as utils from "./utils"; import { logger } from './logger'; import { MatrixClient } from "./client"; diff --git a/src/models/search-result.ts b/src/models/search-result.ts index 486e30abf..1dc16ea84 100644 --- a/src/models/search-result.ts +++ b/src/models/search-result.ts @@ -21,7 +21,6 @@ limitations under the License. import { EventContext } from "./event-context"; import { EventMapper } from "../event-mapper"; import { IResultContext, ISearchResult } from "../@types/search"; -import { IRoomEvent } from "../sync-accumulator"; export class SearchResult { /** diff --git a/src/scheduler.ts b/src/scheduler.ts index 845ea1c44..b83a57eba 100644 --- a/src/scheduler.ts +++ b/src/scheduler.ts @@ -62,6 +62,7 @@ export class MatrixScheduler { * @return {Number} * @see module:scheduler~retryAlgorithm */ + // eslint-disable-next-line @typescript-eslint/naming-convention public static RETRY_BACKOFF_RATELIMIT(event: MatrixEvent, attempts: number, err: MatrixError): number { if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) { // client error; no amount of retrying with save you now. @@ -97,6 +98,7 @@ export class MatrixScheduler { * @return {string} * @see module:scheduler~queueAlgorithm */ + // eslint-disable-next-line @typescript-eslint/naming-convention public static QUEUE_MESSAGES(event: MatrixEvent) { // enqueue messages or events that associate with another event (redactions and relations) if (event.getType() === EventType.RoomMessage || event.hasAssocation()) { From f876c3528307f38d3a03c04e8298cca3ff91a067 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Aug 2021 10:34:05 +0100 Subject: [PATCH 13/65] Fix some bad conversion artifacts --- src/interactive-auth.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/interactive-auth.ts b/src/interactive-auth.ts index 5d6dcd83c..f74463409 100644 --- a/src/interactive-auth.ts +++ b/src/interactive-auth.ts @@ -193,12 +193,14 @@ export class InteractiveAuth { private submitPromise: Promise = null; constructor(opts: IOpts) { + this.matrixClient = opts.matrixClient; this.data = opts.authData || {}; this.requestCallback = opts.doRequest; this.busyChangedCallback = opts.busyChanged; // startAuthStage included for backwards compat this.stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage; this.requestEmailTokenCallback = opts.requestEmailToken; + this.inputs = opts.inputs || {}; if (opts.sessionId) this.data.session = opts.sessionId; this.clientSecret = opts.clientSecret || this.matrixClient.generateClientSecret(); @@ -216,6 +218,7 @@ export class InteractiveAuth { // This promise will be quite long-lived and will resolve when the // request is authenticated and completes successfully. this.attemptAuthDeferred = defer(); + const promise = this.attemptAuthDeferred.promise; const hasFlows = this.data && this.data.flows; @@ -236,7 +239,7 @@ export class InteractiveAuth { this.startNextAuthStage(); } - return this.attemptAuthDeferred.promise; + return promise; } /** @@ -406,6 +409,7 @@ export class InteractiveAuth { try { const result = await this.requestCallback(auth, background); this.attemptAuthDeferred.resolve(result); + this.attemptAuthDeferred = null; } catch (error) { // sometimes UI auth errors don't come with flows const errorFlows = error.data ? error.data.flows : null; @@ -436,6 +440,7 @@ export class InteractiveAuth { this.startNextAuthStage(); } catch (e) { this.attemptAuthDeferred.reject(e); + this.attemptAuthDeferred = null; } if ( @@ -470,12 +475,11 @@ export class InteractiveAuth { // the failure up as the user can't complete auth if we can't // send the email, for whatever reason. this.attemptAuthDeferred.reject(e); + this.attemptAuthDeferred = null; } finally { this.requestingEmailToken = false; } } - } finally { - this.attemptAuthDeferred = null; // TODO } } From a5314463964dc3ea41b7e46f92e8e7a66e1fb0f0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Aug 2021 10:46:13 +0100 Subject: [PATCH 14/65] tidy a bit more --- src/interactive-auth.ts | 9 ++++----- src/sync.ts | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/interactive-auth.ts b/src/interactive-auth.ts index f74463409..7070bb7d5 100644 --- a/src/interactive-auth.ts +++ b/src/interactive-auth.ts @@ -218,14 +218,13 @@ export class InteractiveAuth { // This promise will be quite long-lived and will resolve when the // request is authenticated and completes successfully. this.attemptAuthDeferred = defer(); + // pluck the promise out now, as doRequest may clear before we return const promise = this.attemptAuthDeferred.promise; - const hasFlows = this.data && this.data.flows; - // if we have no flows, try a request to acquire the flows - if (!hasFlows) { + if (!this.data?.flows) { this.busyChangedCallback?.(true); - // use the existing sessionid, if one is present. + // use the existing sessionId, if one is present. let auth = null; if (this.data.session) { auth = { @@ -412,7 +411,7 @@ export class InteractiveAuth { this.attemptAuthDeferred = null; } catch (error) { // sometimes UI auth errors don't come with flows - const errorFlows = error.data ? error.data.flows : null; + const errorFlows = error.data?.flows ?? null; const haveFlows = this.data.flows || Boolean(errorFlows); if (error.httpStatus !== 401 || !error.data || !haveFlows) { // doesn't look like an interactive-auth failure. diff --git a/src/sync.ts b/src/sync.ts index c2534387f..776e7847a 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1067,8 +1067,8 @@ export class SyncApi { // will be updated when we receive push rules via getPushRules // (see sync) before syncing over the network. if (accountDataEvent.getType() === EventType.PushRules) { - const rules = accountDataEvent.getContent(); - client.pushRules = PushProcessor.rewriteDefaultRules(rules as IRulesets); + const rules = accountDataEvent.getContent(); + client.pushRules = PushProcessor.rewriteDefaultRules(rules); } const prevEvent = prevEventsMap[accountDataEvent.getId()]; client.emit("accountData", accountDataEvent, prevEvent); From 9b50455049cff88283fd6cf79fb8d604d3fa1651 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Aug 2021 11:03:05 +0100 Subject: [PATCH 15/65] Iterate PR, merge types with @types/PushRules --- src/@types/PushRules.ts | 3 +- src/pushprocessor.ts | 140 +++++++++++++--------------------------- 2 files changed, 47 insertions(+), 96 deletions(-) diff --git a/src/@types/PushRules.ts b/src/@types/PushRules.ts index a4eda1b48..7864fe07c 100644 --- a/src/@types/PushRules.ts +++ b/src/@types/PushRules.ts @@ -90,8 +90,7 @@ export interface ISenderNotificationPermissionCondition key: string; } -export type PushRuleCondition = IPushRuleCondition - | IEventMatchCondition +export type PushRuleCondition = IEventMatchCondition | IContainsDisplayNameCondition | IRoomMemberCountCondition | ISenderNotificationPermissionCondition; diff --git a/src/pushprocessor.ts b/src/pushprocessor.ts index 51e2c6316..8a8d86a33 100644 --- a/src/pushprocessor.ts +++ b/src/pushprocessor.ts @@ -18,89 +18,39 @@ import { escapeRegExp, globToRegexp, isNullOrUndefined } from "./utils"; import { logger } from './logger'; import { MatrixClient } from "./client"; import { MatrixEvent } from "./models/event"; +import { + ConditionKind, + IAnnotatedPushRule, + IContainsDisplayNameCondition, + IEventMatchCondition, + IPushRule, + IRoomMemberCountCondition, + ISenderNotificationPermissionCondition, + PushRuleAction, + PushRuleActionName, + PushRuleCondition, + PushRuleKind, + TweakName, +} from "./@types/PushRules"; /** * @module pushprocessor */ -enum RuleKind { - Override = "override", - Content = "content", - Room = "room", - Sender = "sender", - Underride = "underride", -} - -const RULEKINDS_IN_ORDER = [RuleKind.Override, RuleKind.Content, RuleKind.Room, RuleKind.Sender, RuleKind.Underride]; - -enum ConditionKind { - EventMatch = "event_match", - ContainsDisplayName = "contains_display_name", - RoomMemberCount = "room_member_count", - SenderNotificationPermission = "sender_notification_permission", -} - -enum Tweak { - Sound = "sound", - Highlight = "highlight", -} - -interface ISetTweak { - // eslint-disable-next-line camelcase - set_tweak: Tweak; - value: any; -} - -export enum Action { - Notify = "notify", - DontNotify = "dont_notify", - Coalesce = "coalesce", -} - -type PushAction = Action | ISetTweak; - -interface IEventMatchCondition { - kind: ConditionKind.EventMatch; - key: string; - pattern: string; - value?: string; // Legacy field? -} - -interface IContainsDisplayNameCondition { - kind: ConditionKind.ContainsDisplayName; -} - -interface IRoomMemberCountCondition { - kind: ConditionKind.RoomMemberCount; - is: string; -} - -interface ISenderNotificationPermissionCondition { - kind: ConditionKind.SenderNotificationPermission; - key: string; -} - -type PushCondition = IEventMatchCondition - | IContainsDisplayNameCondition - | IRoomMemberCountCondition - | ISenderNotificationPermissionCondition; - -interface IPushRule { - // eslint-disable-next-line camelcase - rule_id: string; - default?: boolean; - enabled?: boolean; - conditions: PushCondition[]; - actions: PushAction[]; - kind?: RuleKind; // this is a locally monkey wrenched field -} +const RULEKINDS_IN_ORDER = [ + PushRuleKind.Override, + PushRuleKind.ContentSpecific, + PushRuleKind.RoomSpecific, + PushRuleKind.SenderSpecific, + PushRuleKind.Underride, +]; interface IRuleset { - [RuleKind.Content]: IPushRule[]; - [RuleKind.Override]: IPushRule[]; - [RuleKind.Room]: IPushRule[]; - [RuleKind.Sender]: IPushRule[]; - [RuleKind.Underride]: IPushRule[]; + [PushRuleKind.ContentSpecific]: IPushRule[]; + [PushRuleKind.Override]: IPushRule[]; + [PushRuleKind.RoomSpecific]: IPushRule[]; + [PushRuleKind.SenderSpecific]: IPushRule[]; + [PushRuleKind.Underride]: IPushRule[]; } export interface IRulesets { @@ -133,9 +83,9 @@ const DEFAULT_OVERRIDE_RULES: IPushRule[] = [ }, ], actions: [ - Action.Notify, + PushRuleActionName.Notify, { - set_tweak: Tweak.Highlight, + set_tweak: TweakName.Highlight, value: true, }, ], @@ -153,14 +103,14 @@ const DEFAULT_OVERRIDE_RULES: IPushRule[] = [ }, ], actions: [ - Action.DontNotify, + PushRuleActionName.DontNotify, ], }, ]; export interface IActionsObject { notify: boolean; - tweaks: Partial>; + tweaks: Partial>; } export class PushProcessor { @@ -172,11 +122,11 @@ export class PushProcessor { * * @return {object} A object with key 'notify' (true or false) and an object of actions */ - public static actionListToActionsObject(actionList: PushAction[]): IActionsObject { + public static actionListToActionsObject(actionList: PushRuleAction[]): IActionsObject { const actionObj: IActionsObject = { notify: false, tweaks: {} }; for (let i = 0; i < actionList.length; ++i) { const action = actionList[i]; - if (action === Action.Notify) { + if (action === PushRuleActionName.Notify) { actionObj.notify = true; } else if (typeof action === 'object') { if (action.value === undefined) { @@ -236,7 +186,7 @@ export class PushProcessor { */ constructor(private readonly client: MatrixClient) {} - private matchingRuleFromKindSet(ev: MatrixEvent, kindset: IRuleset): IPushRule { + private matchingRuleFromKindSet(ev: MatrixEvent, kindset: IRuleset): IAnnotatedPushRule { for (let ruleKindIndex = 0; ruleKindIndex < RULEKINDS_IN_ORDER.length; ++ruleKindIndex) { const kind = RULEKINDS_IN_ORDER[ruleKindIndex]; const ruleset = kindset[kind]; @@ -256,26 +206,28 @@ export class PushProcessor { } if (this.ruleMatchesEvent(rawrule, ev)) { - rule.kind = kind; - return rule; + return { + ...rule, + kind, + }; } } } return null; } - private templateRuleToRaw(kind: RuleKind, tprule: any): any { + private templateRuleToRaw(kind: PushRuleKind, tprule: any): any { const rawrule = { 'rule_id': tprule.rule_id, 'actions': tprule.actions, 'conditions': [], }; switch (kind) { - case RuleKind.Underride: - case RuleKind.Override: + case PushRuleKind.Underride: + case PushRuleKind.Override: rawrule.conditions = tprule.conditions; break; - case RuleKind.Room: + case PushRuleKind.RoomSpecific: if (!tprule.rule_id) { return null; } @@ -285,7 +237,7 @@ export class PushProcessor { 'value': tprule.rule_id, }); break; - case RuleKind.Sender: + case PushRuleKind.SenderSpecific: if (!tprule.rule_id) { return null; } @@ -295,7 +247,7 @@ export class PushProcessor { 'value': tprule.rule_id, }); break; - case RuleKind.Content: + case PushRuleKind.ContentSpecific: if (!tprule.pattern) { return null; } @@ -309,7 +261,7 @@ export class PushProcessor { return rawrule; } - private eventFulfillsCondition(cond: PushCondition, ev: MatrixEvent): boolean { + private eventFulfillsCondition(cond: PushRuleCondition, ev: MatrixEvent): boolean { switch (cond.kind) { case ConditionKind.EventMatch: return this.eventFulfillsEventMatchCondition(cond, ev); @@ -475,7 +427,7 @@ export class PushProcessor { return val; } - private matchingRuleForEventWithRulesets(ev: MatrixEvent, rulesets): IPushRule { + private matchingRuleForEventWithRulesets(ev: MatrixEvent, rulesets): IAnnotatedPushRule { if (!rulesets) { return null; } @@ -498,7 +450,7 @@ export class PushProcessor { if (actionObj.tweaks.highlight === undefined) { // if it isn't specified, highlight if it's a content // rule but otherwise not - actionObj.tweaks.highlight = (rule.kind == 'content'); + actionObj.tweaks.highlight = (rule.kind == PushRuleKind.ContentSpecific); } return actionObj; From 13cdca028fc7f701b01a37cb7baecb7823c8c315 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 10 Aug 2021 21:59:40 +0100 Subject: [PATCH 16/65] Fix blank profile in join events This definitely needs explanation, but hopefully the comment is sufficient. Type: defect Fixes https://github.com/vector-im/element-web/issues/18321 Regressed in https://github.com/matrix-org/matrix-js-sdk/pull/1765 --- src/models/event-timeline.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/models/event-timeline.ts b/src/models/event-timeline.ts index c89da5c0b..9458365a8 100644 --- a/src/models/event-timeline.ts +++ b/src/models/event-timeline.ts @@ -50,13 +50,15 @@ export class EventTimeline { * @param {boolean} toStartOfTimeline if true the event's forwardLooking flag is set false */ static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void { - // We always check if the event doesn't already have the property. We do - // this to avoid overriding non-sentinel members by sentinel ones when - // adding the event to a filtered timeline - if (!event.sender) { + // When we try to generate a sentinel member before we have that member + // in the members object, we still generate a sentinel but it doesn't + // have a membership event, so test to see if events.member is set. We + // check this to avoid overriding non-sentinel members by sentinel ones + // when adding the event to a filtered timeline + if (!event.sender?.events.member) { event.sender = stateContext.getSentinelMember(event.getSender()); } - if (!event.target && event.getType() === EventType.RoomMember) { + if (!event.target?.events.member && event.getType() === EventType.RoomMember) { event.target = stateContext.getSentinelMember(event.getStateKey()); } From 2375b00b5284957b2326347af71f76ffd65f77a9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 10 Aug 2021 22:09:01 +0100 Subject: [PATCH 17/65] more null checking --- src/models/event-timeline.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/event-timeline.ts b/src/models/event-timeline.ts index 9458365a8..819469bcd 100644 --- a/src/models/event-timeline.ts +++ b/src/models/event-timeline.ts @@ -55,10 +55,10 @@ export class EventTimeline { // have a membership event, so test to see if events.member is set. We // check this to avoid overriding non-sentinel members by sentinel ones // when adding the event to a filtered timeline - if (!event.sender?.events.member) { + if (!event.sender?.events?.member) { event.sender = stateContext.getSentinelMember(event.getSender()); } - if (!event.target?.events.member && event.getType() === EventType.RoomMember) { + if (!event.target?.events?.member && event.getType() === EventType.RoomMember) { event.target = stateContext.getSentinelMember(event.getStateKey()); } From 916f6a61265879225327926e57a1065883192c07 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 11 Aug 2021 04:42:32 +0100 Subject: [PATCH 18/65] fix TURN by fixing regression preventing multiple candidates from sending In https://github.com/matrix-org/matrix-js-sdk/commit/eafecd36bc860e79d9aa280a7e60455dbd7c6f67#diff-df33fb6c18a9b5896cd500875824d6c10980d42c92b21cedac6d8f570a8d52b7L1639 the candidateSendTries reset was accidentally lost, meaning that we never send more than one m.call.candidates, breaking trickle ICE. Also, we leak blank candidates out in m.call.candidates on Safari and FF, so fix that too. --- src/webrtc/call.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index da529c08a..9a0376e1b 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -1640,9 +1640,14 @@ export class MatrixCall extends EventEmitter { } queueCandidate(content: RTCIceCandidate) { - // Sends candidates with are sent in a special way because we try to amalgamate - // them into one message - this.candidateSendQueue.push(content); + // We partially de-trickle candidates by waiting for `delay` before sending them + // amalgamated, in order to avoid sending too many m.call.candidates events and hitting + // rate limits in Matrix. + // In practice, it'd be better to remove rate limits for m.call.* + // ensure the 'ICE complete' dummy event never leaks out into m.call.candidates + if (content.candidate !== '') { + this.candidateSendQueue.push(content); + } // Don't send the ICE candidates yet if the call is in the ringing state: this // means we tried to pick (ie. started generating candidates) and then failed to @@ -1786,6 +1791,9 @@ export class MatrixCall extends EventEmitter { logger.debug("Attempting to send " + candidates.length + " candidates"); try { await this.sendVoipEvent(EventType.CallCandidates, content); + // reset our retry count if we have successfully sent our candidates + // otherwise queueCandidate() will refuse to try to flush the queue + this.candidateSendTries = 0; } catch (error) { // don't retry this event: we'll send another one later as we might // have more candidates by then. From 913b2d148c4097113a987438e2e1adaae5d92334 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 11 Aug 2021 11:30:42 +0100 Subject: [PATCH 19/65] clarify blank candidate comment --- src/webrtc/call.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 9a0376e1b..ed4db165b 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -1644,10 +1644,12 @@ export class MatrixCall extends EventEmitter { // amalgamated, in order to avoid sending too many m.call.candidates events and hitting // rate limits in Matrix. // In practice, it'd be better to remove rate limits for m.call.* - // ensure the 'ICE complete' dummy event never leaks out into m.call.candidates - if (content.candidate !== '') { - this.candidateSendQueue.push(content); - } + + // N.B. this deliberately lets you queue and send blank candidates, which MSC2746 + // currently proposes as the way to indicate that candidate gathering is complete. + // This will hopefully be changed to an explicit rather than implicit notification + // shortly. + this.candidateSendQueue.push(content); // Don't send the ICE candidates yet if the call is in the ringing state: this // means we tried to pick (ie. started generating candidates) and then failed to From 97d718e8502a6e6f97774a398807667e973b97db Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 11 Aug 2021 11:31:40 +0100 Subject: [PATCH 20/65] tab->space --- src/webrtc/call.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index ed4db165b..f757168fe 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -1645,10 +1645,10 @@ export class MatrixCall extends EventEmitter { // rate limits in Matrix. // In practice, it'd be better to remove rate limits for m.call.* - // N.B. this deliberately lets you queue and send blank candidates, which MSC2746 - // currently proposes as the way to indicate that candidate gathering is complete. - // This will hopefully be changed to an explicit rather than implicit notification - // shortly. + // N.B. this deliberately lets you queue and send blank candidates, which MSC2746 + // currently proposes as the way to indicate that candidate gathering is complete. + // This will hopefully be changed to an explicit rather than implicit notification + // shortly. this.candidateSendQueue.push(content); // Don't send the ICE candidates yet if the call is in the ringing state: this From 9fe05e7d40c3151a04af9c7f80c6c1fa15d6b0f5 Mon Sep 17 00:00:00 2001 From: Dariusz Niemczyk Date: Mon, 9 Aug 2021 12:43:00 +0200 Subject: [PATCH 21/65] Fix temporary call messages being handled without call In cases where a rogue client, or just a simple bug, sends a temporary call message, like CallNegotiate the messages should just be discarded if there's no actual p2p connection created. --- src/webrtc/call.ts | 6 +++++- src/webrtc/callEventHandler.ts | 10 ++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index f757168fe..f1456e26c 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -1286,7 +1286,7 @@ export class MatrixCall extends EventEmitter { // https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation const offerCollision = ( (description.type === 'offer') && - (this.makingOffer || this.peerConn.signalingState != 'stable') + (this.makingOffer || this.peerConn.signalingState !== 'stable') ); this.ignoreOffer = !polite && offerCollision; @@ -1935,6 +1935,10 @@ export class MatrixCall extends EventEmitter { } } } + + get hasPeerConnection() { + return Boolean(this.peerConn); + } } async function getScreensharingStream( diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 3e080c0eb..8251cdf04 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -220,6 +220,7 @@ export class CallEventHandler { } else { this.client.emit("Call.incoming", call); } + return; } else if (type === EventType.CallCandidates) { if (weSentTheEvent) return; @@ -232,6 +233,7 @@ export class CallEventHandler { } else { call.onRemoteIceCandidatesReceived(event); } + return; } else if ([EventType.CallHangup, EventType.CallReject].includes(type)) { // 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 @@ -255,10 +257,14 @@ export class CallEventHandler { this.calls.delete(content.call_id); } } + return; } - // The following events need a call - if (!call) return; + // The following events need a call and a peer connection + if (!call && !call.hasPeerConnection) { + logger.warn('Discarding an event', type); + return; + } // Ignore remote echo if (event.getContent().party_id === call.ourPartyId) return; From 4fd77c2f0528aa90f56076d7e35898ef2a05c9a0 Mon Sep 17 00:00:00 2001 From: Dariusz Niemczyk <3636685+Palid@users.noreply.github.com> Date: Wed, 11 Aug 2021 14:38:08 +0200 Subject: [PATCH 22/65] Update src/webrtc/callEventHandler.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Šimon Brandner --- src/webrtc/callEventHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 8251cdf04..8ffe3efff 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -262,7 +262,7 @@ export class CallEventHandler { // The following events need a call and a peer connection if (!call && !call.hasPeerConnection) { - logger.warn('Discarding an event', type); + logger.warn('Discarding an event, we don't have a call/peerConn', type); return; } // Ignore remote echo From 3e94db1837ecead43f5bc85c9e697ae8e3fffac5 Mon Sep 17 00:00:00 2001 From: Dariusz Niemczyk <3636685+Palid@users.noreply.github.com> Date: Wed, 11 Aug 2021 14:38:13 +0200 Subject: [PATCH 23/65] Update src/webrtc/call.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Šimon Brandner --- src/webrtc/call.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index f1456e26c..72b927bd9 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -1936,7 +1936,7 @@ export class MatrixCall extends EventEmitter { } } - get hasPeerConnection() { + public get hasPeerConnection() { return Boolean(this.peerConn); } } From d7dbaeba46f5a17335fe60a227d40dc41a586f12 Mon Sep 17 00:00:00 2001 From: Dariusz Niemczyk <3636685+Palid@users.noreply.github.com> Date: Wed, 11 Aug 2021 14:38:20 +0200 Subject: [PATCH 24/65] Update src/webrtc/callEventHandler.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Šimon Brandner --- src/webrtc/callEventHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 8ffe3efff..4067c58d1 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -261,7 +261,7 @@ export class CallEventHandler { } // The following events need a call and a peer connection - if (!call && !call.hasPeerConnection) { + if (!call || !call.hasPeerConnection) { logger.warn('Discarding an event, we don't have a call/peerConn', type); return; } From 4607f0177cf7d95bb48df1372dbcfb5d5fee8767 Mon Sep 17 00:00:00 2001 From: Dariusz Niemczyk Date: Wed, 11 Aug 2021 14:39:00 +0200 Subject: [PATCH 25/65] Fix invalid string quotes --- src/webrtc/callEventHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 4067c58d1..2ad4bcc71 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -262,7 +262,7 @@ export class CallEventHandler { // The following events need a call and a peer connection if (!call || !call.hasPeerConnection) { - logger.warn('Discarding an event, we don't have a call/peerConn', type); + logger.warn("Discarding an event, we don't have a call/peerConn", type); return; } // Ignore remote echo From ad036104e622040b3a56b8cca3f2f683f5ef2edc Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Aug 2021 15:41:00 +0100 Subject: [PATCH 26/65] Changelog --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4f3eb273..8dcece0a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +Changes in [12.3.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0-rc.1) (2021-07-11) +============================================================================================================= + +## ✨ Features + * Support for MSC3291: Muting in VoIP calls ([\#1812](https://github.com/matrix-org/matrix-js-sdk/pull/1812)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Support for screen-sharing using multi-stream VoIP (MSC3077) ([\#1685](https://github.com/matrix-org/matrix-js-sdk/pull/1685)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Handle DTMF support ([\#1813](https://github.com/matrix-org/matrix-js-sdk/pull/1813)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + +## 🐛 Bug Fixes + * Fix error on turning off screensharing ([\#1833](https://github.com/matrix-org/matrix-js-sdk/pull/1833)). Fixes vector-im/element-web#18449. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix blank profile in join events ([\#1837](https://github.com/matrix-org/matrix-js-sdk/pull/1837)). Fixes vector-im/element-web#18321. + * fix TURN by fixing regression preventing multiple ICE candidates from sending. ([\#1838](https://github.com/matrix-org/matrix-js-sdk/pull/1838)). + * Send `user_hangup` reason if the opponent supports it ([\#1820](https://github.com/matrix-org/matrix-js-sdk/pull/1820)). Fixes vector-im/element-web#18219. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Apply hidden char check to rawDisplayName too ([\#1816](https://github.com/matrix-org/matrix-js-sdk/pull/1816)). + * Only clear bit 63 when we create the IV ([\#1819](https://github.com/matrix-org/matrix-js-sdk/pull/1819)). + Changes in [12.2.0](https://github.com/vector-im/element-desktop/releases/tag/v12.2.0) (2021-07-02) =================================================================================================== From 429f32dacbc3eae91035a308c924224821dbeca9 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 11 Aug 2021 15:41:40 +0100 Subject: [PATCH 27/65] v12.3.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 63c9e731e..87076cf26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "12.2.0", + "version": "12.3.0-rc.1", "description": "Matrix Client-Server SDK for Javascript", "scripts": { "prepublishOnly": "yarn build", @@ -29,7 +29,7 @@ "keywords": [ "matrix-org" ], - "main": "./src/index.ts", + "main": "./lib/index.js", "browser": "./lib/browser-index.js", "matrix_src_main": "./src/index.ts", "matrix_src_browser": "./src/browser-index.js", @@ -112,5 +112,6 @@ "coverageReporters": [ "text" ] - } + }, + "typings": "./lib/index.d.ts" } From 69ba32683cc42b343add11ae6092f2b71e12fdc2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Aug 2021 21:45:51 +0100 Subject: [PATCH 28/65] Iterate PR based on feedback --- src/@types/PushRules.ts | 2 ++ src/interactive-auth.ts | 20 +++++++++++++------- src/pushprocessor.ts | 38 ++++++++++++++------------------------ src/sync.ts | 5 +++-- 4 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/@types/PushRules.ts b/src/@types/PushRules.ts index 7864fe07c..fa404c43a 100644 --- a/src/@types/PushRules.ts +++ b/src/@types/PushRules.ts @@ -90,6 +90,8 @@ export interface ISenderNotificationPermissionCondition key: string; } +// XXX: custom conditions are possible but always fail, and break the typescript discriminated union so ignore them here +// IPushRuleCondition> unfortunately does not resolve this at the time of writing. export type PushRuleCondition = IEventMatchCondition | IContainsDisplayNameCondition | IRoomMemberCountCondition diff --git a/src/interactive-auth.ts b/src/interactive-auth.ts index 7070bb7d5..28829e3f1 100644 --- a/src/interactive-auth.ts +++ b/src/interactive-auth.ts @@ -81,6 +81,15 @@ export interface IAuthDict { threepidCreds?: any; } +class NoAuthFlowFoundError extends Error { + public name = "NoAuthFlowFoundError"; + + // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase + constructor(m: string, public readonly required_stages: string[], public readonly flows: IFlow[]) { + super(m); + } +} + interface IOpts { matrixClient: MatrixClient; authData?: IAuthData; @@ -574,16 +583,13 @@ export class InteractiveAuth { return flow; } } - // Throw an error with a fairly generic description, but with more - // information such that the app can give a better one if so desired. - const err = new Error("No appropriate authentication flow found"); - err.name = 'NoAuthFlowFoundError'; + const requiredStages: string[] = []; if (haveEmail) requiredStages.push(EMAIL_STAGE_TYPE); if (haveMsisdn) requiredStages.push(MSISDN_STAGE_TYPE); - (err as any).required_stages = requiredStages; - (err as any).available_flows = flows; - throw err; + // Throw an error with a fairly generic description, but with more + // information such that the app can give a better one if so desired. + throw new NoAuthFlowFoundError("No appropriate authentication flow found", requiredStages, flows); } /** diff --git a/src/pushprocessor.ts b/src/pushprocessor.ts index 8a8d86a33..7e551202c 100644 --- a/src/pushprocessor.ts +++ b/src/pushprocessor.ts @@ -24,12 +24,14 @@ import { IContainsDisplayNameCondition, IEventMatchCondition, IPushRule, + IPushRules, IRoomMemberCountCondition, ISenderNotificationPermissionCondition, PushRuleAction, PushRuleActionName, PushRuleCondition, PushRuleKind, + PushRuleSet, TweakName, } from "./@types/PushRules"; @@ -45,18 +47,6 @@ const RULEKINDS_IN_ORDER = [ PushRuleKind.Underride, ]; -interface IRuleset { - [PushRuleKind.ContentSpecific]: IPushRule[]; - [PushRuleKind.Override]: IPushRule[]; - [PushRuleKind.RoomSpecific]: IPushRule[]; - [PushRuleKind.SenderSpecific]: IPushRule[]; - [PushRuleKind.Underride]: IPushRule[]; -} - -export interface IRulesets { - global: IRuleset; -} - // The default override rules to apply to the push rules that arrive from the server. // We do this for two reasons: // 1. Synapse is unlikely to send us the push rule in an incremental sync - see @@ -114,6 +104,13 @@ export interface IActionsObject { } export class PushProcessor { + /** + * Construct a Push Processor. + * @constructor + * @param {Object} client The Matrix client object to use + */ + constructor(private readonly client: MatrixClient) {} + /** * Convert a list of actions into a object with the actions as keys and their values * eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ] @@ -145,13 +142,13 @@ export class PushProcessor { * @param {object} incomingRules The client's existing push rules * @returns {object} The rewritten rules */ - public static rewriteDefaultRules(incomingRules: IRulesets): IRulesets { - let newRules: IRulesets = JSON.parse(JSON.stringify(incomingRules)); // deep clone + public static rewriteDefaultRules(incomingRules: IPushRules): IPushRules { + let newRules: IPushRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone // These lines are mostly to make the tests happy. We shouldn't run into these // properties missing in practice. - if (!newRules) newRules = {} as IRulesets; - if (!newRules.global) newRules.global = {} as IRuleset; + if (!newRules) newRules = {} as IPushRules; + if (!newRules.global) newRules.global = {} as PushRuleSet; if (!newRules.global.override) newRules.global.override = []; // Merge the client-level defaults with the ones from the server @@ -179,14 +176,7 @@ export class PushProcessor { private static cachedGlobToRegex: Record = {}; // $glob: RegExp - /** - * Construct a Push Processor. - * @constructor - * @param {Object} client The Matrix client object to use - */ - constructor(private readonly client: MatrixClient) {} - - private matchingRuleFromKindSet(ev: MatrixEvent, kindset: IRuleset): IAnnotatedPushRule { + private matchingRuleFromKindSet(ev: MatrixEvent, kindset: PushRuleSet): IAnnotatedPushRule { for (let ruleKindIndex = 0; ruleKindIndex < RULEKINDS_IN_ORDER.length; ++ruleKindIndex) { const kind = RULEKINDS_IN_ORDER[ruleKindIndex]; const ruleset = kindset[kind]; diff --git a/src/sync.ts b/src/sync.ts index 776e7847a..70cefef7b 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -30,7 +30,7 @@ import * as utils from "./utils"; import { IDeferred } from "./utils"; import { Filter } from "./filter"; import { EventTimeline } from "./models/event-timeline"; -import { IRulesets, PushProcessor } from "./pushprocessor"; +import { PushProcessor } from "./pushprocessor"; import { logger } from './logger'; import { InvalidStoreError } from './errors'; import { IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client"; @@ -53,6 +53,7 @@ import { MatrixEvent } from "./models/event"; import { MatrixError } from "./http-api"; import { ISavedSync } from "./store"; import { EventType } from "./@types/event"; +import { IPushRules } from "./@types/PushRules"; const DEBUG = true; @@ -1067,7 +1068,7 @@ export class SyncApi { // will be updated when we receive push rules via getPushRules // (see sync) before syncing over the network. if (accountDataEvent.getType() === EventType.PushRules) { - const rules = accountDataEvent.getContent(); + const rules = accountDataEvent.getContent(); client.pushRules = PushProcessor.rewriteDefaultRules(rules); } const prevEvent = prevEventsMap[accountDataEvent.getId()]; From 6170796ed87076be17655db76eb5973a8a2b152f Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Aug 2021 21:50:13 +0100 Subject: [PATCH 29/65] Use changelog generator from npm --- package.json | 2 +- yarn.lock | 63 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 87076cf26..d4d52d9bc 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@types/request": "^2.48.5", "@typescript-eslint/eslint-plugin": "^4.17.0", "@typescript-eslint/parser": "^4.17.0", - "allchange": "github:matrix-org/allchange", + "allchange": "^0.0.1", "babel-jest": "^26.6.3", "babelify": "^10.0.0", "better-docs": "^2.4.0-beta.9", diff --git a/yarn.lock b/yarn.lock index 2a431ea8e..820b6eeca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,28 @@ # yarn lockfile v1 +"@actions/core@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.4.0.tgz#cf2e6ee317e314b03886adfeb20e448d50d6e524" + integrity sha512-CGx2ilGq5i7zSLgiiGUtBCxhRRxibJYU6Fim0Q1Wg2aQL2LTnF27zbqZOrxfvFQ55eSBW0L8uVStgtKMpa0Qlg== + +"@actions/github@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.0.0.tgz#1754127976c50bd88b2e905f10d204d76d1472f8" + integrity sha512-QvE9eAAfEsS+yOOk0cylLBIO/d6WyWIOvsxxzdrPFaud39G6BOkUwScXZn1iBzQzHyu9SBkkLSWlohDWdsasAQ== + dependencies: + "@actions/http-client" "^1.0.11" + "@octokit/core" "^3.4.0" + "@octokit/plugin-paginate-rest" "^2.13.3" + "@octokit/plugin-rest-endpoint-methods" "^5.1.1" + +"@actions/http-client@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-1.0.11.tgz#c58b12e9aa8b159ee39e7dd6cbd0e91d905633c0" + integrity sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg== + dependencies: + tunnel "0.0.6" + "@babel/cli@^7.12.10": version "7.14.8" resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.14.8.tgz#fac73c0e2328a8af9fd3560c06b096bfa3730933" @@ -1235,7 +1257,7 @@ dependencies: "@octokit/types" "^6.0.3" -"@octokit/core@^3.5.0": +"@octokit/core@^3.4.0", "@octokit/core@^3.5.0": version "3.5.1" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b" integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw== @@ -1271,6 +1293,18 @@ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.3.0.tgz#160347858d727527901c6aae7f7d5c2414cc1f2e" integrity sha512-oz60hhL+mDsiOWhEwrj5aWXTOMVtQgcvP+sRzX4C3cH7WOK9QSAoEtjWh0HdOf6V3qpdgAmUMxnQPluzDWR7Fw== +"@octokit/openapi-types@^9.5.0": + version "9.7.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.7.0.tgz#9897cdefd629cd88af67b8dbe2e5fb19c63426b2" + integrity sha512-TUJ16DJU8mekne6+KVcMV5g6g/rJlrnIKn7aALG9QrNpnEipFc1xjoarh0PKaAWf2Hf+HwthRKYt+9mCm5RsRg== + +"@octokit/plugin-paginate-rest@^2.13.3": + version "2.15.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.1.tgz#264189dd3ce881c6c33758824aac05a4002e056a" + integrity sha512-47r52KkhQDkmvUKZqXzA1lKvcyJEfYh3TKAIe5+EzMeyDM3d+/s5v11i2gTk8/n6No6DPi3k5Ind6wtDbo/AEg== + dependencies: + "@octokit/types" "^6.24.0" + "@octokit/plugin-paginate-rest@^2.6.2": version "2.15.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.0.tgz#9c956c3710b2bd786eb3814eaf5a2b17392c150d" @@ -1291,6 +1325,14 @@ "@octokit/types" "^6.23.0" deprecation "^2.3.1" +"@octokit/plugin-rest-endpoint-methods@^5.1.1": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.8.0.tgz#33b342fe41f2603fdf8b958e6652103bb3ea3f3b" + integrity sha512-qeLZZLotNkoq+it6F+xahydkkbnvSK0iDjlXFo3jNTB+Ss0qIbYQb9V/soKLMkgGw8Q2sHjY5YEXiA47IVPp4A== + dependencies: + "@octokit/types" "^6.25.0" + deprecation "^2.3.1" + "@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" @@ -1329,6 +1371,13 @@ dependencies: "@octokit/openapi-types" "^9.3.0" +"@octokit/types@^6.24.0", "@octokit/types@^6.25.0": + version "6.25.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.25.0.tgz#c8e37e69dbe7ce55ed98ee63f75054e7e808bf1a" + integrity sha512-bNvyQKfngvAd/08COlYIN54nRgxskmejgywodizQNyiKoXmWRAjKup2/LYwm+T9V0gsKH6tuld1gM0PzmOiB4Q== + dependencies: + "@octokit/openapi-types" "^9.5.0" + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -1673,10 +1722,13 @@ align-text@^0.1.1, align-text@^0.1.3: longest "^1.0.1" repeat-string "^1.5.2" -"allchange@github:matrix-org/allchange": +allchange@^0.0.1: version "0.0.1" - resolved "https://codeload.github.com/matrix-org/allchange/tar.gz/56b37b06339a3ac3fe771f3ec3d0bff798df8dab" + resolved "https://registry.yarnpkg.com/allchange/-/allchange-0.0.1.tgz#3f66f7b06314b942fd0c1630fef6ce2ac1b90972" + integrity sha512-YVg1ZcYzEE5/fEnexzlhfWIbPeGtjfFgJ73qUG2DNwl16Apt9sRJQE7z6dLbDPaL9tOlYpIgzf7Id74uS+ccjQ== dependencies: + "@actions/core" "^1.4.0" + "@actions/github" "^5.0.0" "@octokit/rest" "^18.6.7" cli-color "^2.0.0" js-yaml "^4.1.0" @@ -7267,6 +7319,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tunnel@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" From 1f9fab9a0c1ef6c0530f148108bce3372818a1d9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 11 Aug 2021 15:11:13 -0600 Subject: [PATCH 30/65] Fix conditional on returning file tree spaces If a tree space was deleted then it'll still hold a Room object for us, which is not much help if we're effectively trying to get the joined trees. --- spec/unit/matrix-client.spec.js | 23 +++++++++++++++++++++++ src/client.ts | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/spec/unit/matrix-client.spec.js b/spec/unit/matrix-client.spec.js index 090ffeed1..755abfb10 100644 --- a/spec/unit/matrix-client.spec.js +++ b/spec/unit/matrix-client.spec.js @@ -270,6 +270,29 @@ describe("MatrixClient", function() { expect(tree.room).toBe(mockRoom); }); + it("should not get (unstable) file trees if not joined", async () => { + const roomId = "!room:example.org"; + const mockRoom = { + getMyMembership: () => "leave", // "not join" + }; + client.getRoom = (getRoomId) => { + expect(getRoomId).toEqual(roomId); + return mockRoom; + }; + const tree = client.unstableGetFileTreeSpace(roomId); + expect(tree).toBeFalsy(); + }); + + it("should not get (unstable) file trees for unknown rooms", async () => { + const roomId = "!room:example.org"; + client.getRoom = (getRoomId) => { + expect(getRoomId).toEqual(roomId); + return null; // imply unknown + }; + const tree = client.unstableGetFileTreeSpace(roomId); + expect(tree).toBeFalsy(); + }); + it("should not get (unstable) file trees with invalid create contents", async () => { const roomId = "!room:example.org"; const mockRoom = { diff --git a/src/client.ts b/src/client.ts index 2bcda09e4..83b484c44 100644 --- a/src/client.ts +++ b/src/client.ts @@ -8060,7 +8060,7 @@ export class MatrixClient extends EventEmitter { */ public unstableGetFileTreeSpace(roomId: string): MSC3089TreeSpace { const room = this.getRoom(roomId); - if (!room) return null; + if (room?.getMyMembership() !== 'join') return null; const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, ""); const purposeEvent = room.currentState.getStateEvents( From 4e2ee3b3a817d457d35ca9f487f574a066c679cc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 11 Aug 2021 15:18:24 -0600 Subject: [PATCH 31/65] It helps to fix the other tests too --- spec/unit/matrix-client.spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/unit/matrix-client.spec.js b/spec/unit/matrix-client.spec.js index 755abfb10..82e166f67 100644 --- a/spec/unit/matrix-client.spec.js +++ b/spec/unit/matrix-client.spec.js @@ -237,6 +237,7 @@ describe("MatrixClient", function() { it("should get (unstable) file trees with valid state", async () => { const roomId = "!room:example.org"; const mockRoom = { + getMyMembership: () => "join", currentState: { getStateEvents: (eventType, stateKey) => { if (eventType === EventType.RoomCreate) { @@ -296,6 +297,7 @@ describe("MatrixClient", function() { it("should not get (unstable) file trees with invalid create contents", async () => { const roomId = "!room:example.org"; const mockRoom = { + getMyMembership: () => "join", currentState: { getStateEvents: (eventType, stateKey) => { if (eventType === EventType.RoomCreate) { @@ -330,6 +332,7 @@ describe("MatrixClient", function() { it("should not get (unstable) file trees with invalid purpose/subtype contents", async () => { const roomId = "!room:example.org"; const mockRoom = { + getMyMembership: () => "join", currentState: { getStateEvents: (eventType, stateKey) => { if (eventType === EventType.RoomCreate) { From b392dbf6f8e5a090cd128e7db8399ab5d70c9670 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 12 Aug 2021 11:46:49 +0100 Subject: [PATCH 32/65] Fix the types building 1829 added the spec files to 'src', presumably because they just were't type checked otherwise, but this chaqnged the implicit rootDir so the lib directory we shipped has types in src/ and spec/ which meant lib/index.d.ts was at lib/src/index.d.ts. Specify the rootDir on the build tsconfig to put it back in the right place, and also try to make the base tsconfig never emit to avoid it ever generating .d.ts files in silly places. Move all the optoins related to emitting to -build since they aren't relevant in the main one. Is this a sensible way to do this? It doesn't feel terribly elegant. Fixes https://github.com/vector-im/element-web/issues/18503 Regressed in https://github.com/matrix-org/matrix-js-sdk/pull/1829 --- package.json | 2 +- tsconfig-build.json | 8 ++++++++ tsconfig.json | 5 +---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 87076cf26..bfc4129a1 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "clean": "rimraf lib dist", "build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser", "build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types", - "build:types": "tsc --emitDeclarationOnly", + "build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly", "build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src", "build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js", "build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js", diff --git a/tsconfig-build.json b/tsconfig-build.json index 50f5536d9..ef76d3f47 100644 --- a/tsconfig-build.json +++ b/tsconfig-build.json @@ -1,5 +1,13 @@ { "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": true, + "noEmit": false, + "emitDecoratorMetadata": true, + "target": "es2016", + "outDir": "./lib", + "rootDir": "src" + }, "exclude": [ "./spec/**/*.ts" ] diff --git a/tsconfig.json b/tsconfig.json index 896c4c3b3..f4441c2a9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,11 @@ { "compilerOptions": { "experimentalDecorators": true, - "emitDecoratorMetadata": true, "esModuleInterop": true, "module": "commonjs", "moduleResolution": "node", - "target": "es2016", "noImplicitAny": false, - "sourceMap": true, - "outDir": "./lib", + "noEmit": true, "declaration": true }, "include": [ From 2bc1f2fe218ec8bb6817e6688c6f43a6581a26e3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 12 Aug 2021 11:54:19 +0100 Subject: [PATCH 33/65] Apparently target matters when not emitting --- tsconfig-build.json | 1 - tsconfig.json | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig-build.json b/tsconfig-build.json index ef76d3f47..2f59c9da8 100644 --- a/tsconfig-build.json +++ b/tsconfig-build.json @@ -4,7 +4,6 @@ "sourceMap": true, "noEmit": false, "emitDecoratorMetadata": true, - "target": "es2016", "outDir": "./lib", "rootDir": "src" }, diff --git a/tsconfig.json b/tsconfig.json index f4441c2a9..3a0e0cee7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "target": "es2016", "experimentalDecorators": true, "esModuleInterop": true, "module": "commonjs", From 167183775e0bcd6a7042d755d4e20da5e03ef8dc Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Thu, 12 Aug 2021 15:11:35 +0100 Subject: [PATCH 34/65] Prepare changelog for v12.3.0-rc.2 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dcece0a6..09e072a45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [12.3.0-rc.2](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0-rc.2) (2021-07-12) +============================================================================================================= + +## 🐛 Bug Fixes + * Fix the types in shipped package ([\#1842](https://github.com/matrix-org/matrix-js-sdk/pull/1842)). Fixes vector-im/element-web#18503 and vector-im/element-web#18503. + Changes in [12.3.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0-rc.1) (2021-07-11) ============================================================================================================= From 67434bc5d43e45e1fa86db8923e531090ab415e6 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Thu, 12 Aug 2021 15:11:35 +0100 Subject: [PATCH 35/65] v12.3.0-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 206eda2d7..6598287f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "12.3.0-rc.1", + "version": "12.3.0-rc.2", "description": "Matrix Client-Server SDK for Javascript", "scripts": { "prepublishOnly": "yarn build", From 62058e6d489bdf680ceba219b410ec936f1bfb8d Mon Sep 17 00:00:00 2001 From: Christian Paul Date: Fri, 13 Aug 2021 15:15:20 +0200 Subject: [PATCH 36/65] SSSSCryptoCallbacks.getSecretStorageKey may return Promise If no result is found, the function may reach the last line without `return` ever being called. --- src/crypto/EncryptionSetup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/EncryptionSetup.ts b/src/crypto/EncryptionSetup.ts index faa4bd2e8..1c6e31042 100644 --- a/src/crypto/EncryptionSetup.ts +++ b/src/crypto/EncryptionSetup.ts @@ -356,7 +356,7 @@ class SSSSCryptoCallbacks { public async getSecretStorageKey( { keys }: { keys: Record }, name: string, - ): Promise<[string, Uint8Array]> { + ): Promise<[string, Uint8Array]|void> { for (const keyId of Object.keys(keys)) { const privateKey = this.privateKeys.get(keyId); if (privateKey) { From 7c4f7c9dadf6903f1f05fc2878b9d4f0799b8d26 Mon Sep 17 00:00:00 2001 From: Christian Paul Date: Fri, 13 Aug 2021 15:23:47 +0200 Subject: [PATCH 37/65] SSSSCryptoCallbacks.getSecretStorageKey: Return null, if no key found --- src/crypto/EncryptionSetup.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/crypto/EncryptionSetup.ts b/src/crypto/EncryptionSetup.ts index 1c6e31042..1a36efb70 100644 --- a/src/crypto/EncryptionSetup.ts +++ b/src/crypto/EncryptionSetup.ts @@ -356,7 +356,7 @@ class SSSSCryptoCallbacks { public async getSecretStorageKey( { keys }: { keys: Record }, name: string, - ): Promise<[string, Uint8Array]|void> { + ): Promise<[string, Uint8Array]|null> { for (const keyId of Object.keys(keys)) { const privateKey = this.privateKeys.get(keyId); if (privateKey) { @@ -374,6 +374,7 @@ class SSSSCryptoCallbacks { } return result; } + return null; } public addPrivateKey(keyId: string, keyInfo: ISecretStorageKeyInfo, privKey: Uint8Array): void { From 75750ed760c9518518d171fad695b4ab7eb68dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 15 Aug 2021 12:56:48 +0200 Subject: [PATCH 38/65] Add a comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/webrtc/call.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 72b927bd9..028c6c898 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -535,6 +535,7 @@ export class MatrixCall extends EventEmitter { this.emit(CallEvent.FeedsChanged, this.feeds); } + // TODO: Find out what is going on here // why do we enable audio (and only audio) tracks here? -- matthew setTracksEnabled(stream.getAudioTracks(), true); From 4da49d926bf8354fab25dc51ac43cbf99dd3c1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 15 Aug 2021 12:57:27 +0200 Subject: [PATCH 39/65] Remove unnecessary logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/webrtc/call.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 028c6c898..931d06308 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -722,7 +722,6 @@ export class MatrixCall extends EventEmitter { * @param {MatrixCall} newCall The new call. */ replacedBy(newCall: MatrixCall) { - logger.debug(this.callId + " being replaced by " + newCall.callId); if (this.state === CallState.WaitLocalMedia) { logger.debug("Telling new call to wait for local media"); newCall.waitForLocalAVStream = true; @@ -1030,7 +1029,6 @@ export class MatrixCall extends EventEmitter { this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia); this.setState(CallState.CreateOffer); - logger.info("Got local AV stream with id " + this.localUsermediaStream.id); logger.debug("gotUserMediaForInvite -> " + this.type); // Now we wait for the negotiationneeded event }; @@ -1088,9 +1086,6 @@ export class MatrixCall extends EventEmitter { } this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia); - - logger.info("Got local AV stream with id " + this.localUsermediaStream.id); - this.setState(CallState.CreateAnswer); let myAnswer; From 408976a1998608fe2c0d1319e5b57de238b8842d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 15 Aug 2021 12:58:38 +0200 Subject: [PATCH 40/65] Simplifie some code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/webrtc/call.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 931d06308..56952eeb0 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -725,10 +725,7 @@ export class MatrixCall extends EventEmitter { if (this.state === CallState.WaitLocalMedia) { logger.debug("Telling new call to wait for local media"); newCall.waitForLocalAVStream = true; - } else if (this.state === CallState.CreateOffer) { - logger.debug("Handing local stream to new call"); - newCall.gotUserMediaForAnswer(this.localUsermediaStream); - } else if (this.state === CallState.InviteSent) { + } else if ([CallState.CreateOffer, CallState.InviteSent].includes(this.state)) { logger.debug("Handing local stream to new call"); newCall.gotUserMediaForAnswer(this.localUsermediaStream); } From e6d1909f0b0bffb005f25d772f6b6cb989f838fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 15 Aug 2021 13:02:39 +0200 Subject: [PATCH 41/65] Remove weird method call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If we can get localUsermediaStream gotUserMediaForAnswer() has alredy been called before Signed-off-by: Šimon Brandner --- src/webrtc/call.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 56952eeb0..cfc19b2fe 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -709,8 +709,6 @@ export class MatrixCall extends EventEmitter { this.getUserMediaFailed(e); return; } - } else if (this.localUsermediaStream) { - this.gotUserMediaForAnswer(this.localUsermediaStream); } else if (this.waitForLocalAVStream) { this.setState(CallState.WaitLocalMedia); } From f3856d569dfd594d08b53c9d375ecb2e5613c739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 15 Aug 2021 13:03:08 +0200 Subject: [PATCH 42/65] Reformat some code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/webrtc/callEventHandler.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 2ad4bcc71..8620a3f97 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -201,9 +201,11 @@ export class CallEventHandler { // we've got an invite, pick the incoming call because we know // we haven't sent our invite yet otherwise, pick whichever // call has the lowest call ID (by string comparison) - if (existingCall.state === CallState.WaitLocalMedia || - existingCall.state === CallState.CreateOffer || - existingCall.callId > call.callId) { + if ( + existingCall.state === CallState.WaitLocalMedia || + existingCall.state === CallState.CreateOffer || + existingCall.callId > call.callId + ) { logger.log( "Glare detected: answering incoming call " + call.callId + " and canceling outgoing call " + existingCall.callId, From 51e815f0cb166e411f44453c9cd3bb87974ec8e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 15 Aug 2021 13:03:18 +0200 Subject: [PATCH 43/65] Fix glare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/webrtc/callEventHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 8620a3f97..421cd621f 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -128,7 +128,7 @@ export class CallEventHandler { return type.startsWith("m.call.") || type.startsWith("org.matrix.call."); } - private handleCallEvent(event: MatrixEvent) { + private async handleCallEvent(event: MatrixEvent) { const content = event.getContent(); const type = event.getType() as EventType; const weSentTheEvent = event.getSender() === this.client.credentials.userId; @@ -169,7 +169,7 @@ export class CallEventHandler { } call.callId = content.call_id; - call.initWithInvite(event); + await call.initWithInvite(event); this.calls.set(call.callId, call); // if we stashed candidate events for that call ID, play them back now From 0b12e37459d2da0723d91ae30f2f398af79a8010 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 16 Aug 2021 09:01:38 +0100 Subject: [PATCH 44/65] Clarify usages of event's toJSON --- src/models/event.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/models/event.ts b/src/models/event.ts index 16383c9ae..2159c48ee 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -1235,10 +1235,15 @@ export class MatrixEvent extends EventEmitter { } /** - * Summarise the event as JSON for debugging. If encrypted, include both the - * decrypted and encrypted view of the event. This is named `toJSON` for use - * with `JSON.stringify` which checks objects for functions named `toJSON` - * and will call them to customise the output if they are defined. + * Summarise the event as JSON. This is currently used by React SDK's view + * event source feature and Seshat's event indexing, so take care when + * adjusting the output here. + * + * If encrypted, include both the decrypted and encrypted view of the event. + * + * This is named `toJSON` for use with `JSON.stringify` which checks objects + * for functions named `toJSON` and will call them to customise the output + * if they are defined. * * @return {Object} */ From d65b48204b88f92963be15f1baf2382dcb7e8e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 16 Aug 2021 13:14:04 +0200 Subject: [PATCH 45/65] Await handleCallEvent() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/webrtc/callEventHandler.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 421cd621f..0ce247263 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -80,7 +80,7 @@ export class CallEventHandler { continue; } try { - this.handleCallEvent(e); + await this.handleCallEvent(e); } catch (e) { logger.error("Caught exception handling call event", e); } @@ -100,7 +100,7 @@ export class CallEventHandler { if (event.isBeingDecrypted() || event.isDecryptionFailure()) { // add an event listener for once the event is decrypted. - event.once("Event.decrypted", () => { + event.once("Event.decrypted", async () => { if (!this.eventIsACall(event)) return; if (this.callEventBuffer.includes(event)) { @@ -110,7 +110,7 @@ export class CallEventHandler { // This one wasn't buffered so just run the event handler for it // straight away try { - this.handleCallEvent(event); + await this.handleCallEvent(event); } catch (e) { logger.error("Caught exception handling call event", e); } From fa265296e8a1df817c401bd49826f3995588f59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 15 Aug 2021 12:56:48 +0200 Subject: [PATCH 46/65] Add a comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/webrtc/call.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index f757168fe..cd2e19506 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -535,6 +535,7 @@ export class MatrixCall extends EventEmitter { this.emit(CallEvent.FeedsChanged, this.feeds); } + // TODO: Find out what is going on here // why do we enable audio (and only audio) tracks here? -- matthew setTracksEnabled(stream.getAudioTracks(), true); From da53241c4809fdf308120d0a3d5240268bcdcfe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 15 Aug 2021 12:57:27 +0200 Subject: [PATCH 47/65] Remove unnecessary logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/webrtc/call.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index cd2e19506..8e6be2703 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -722,7 +722,6 @@ export class MatrixCall extends EventEmitter { * @param {MatrixCall} newCall The new call. */ replacedBy(newCall: MatrixCall) { - logger.debug(this.callId + " being replaced by " + newCall.callId); if (this.state === CallState.WaitLocalMedia) { logger.debug("Telling new call to wait for local media"); newCall.waitForLocalAVStream = true; @@ -1030,7 +1029,6 @@ export class MatrixCall extends EventEmitter { this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia); this.setState(CallState.CreateOffer); - logger.info("Got local AV stream with id " + this.localUsermediaStream.id); logger.debug("gotUserMediaForInvite -> " + this.type); // Now we wait for the negotiationneeded event }; @@ -1088,9 +1086,6 @@ export class MatrixCall extends EventEmitter { } this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia); - - logger.info("Got local AV stream with id " + this.localUsermediaStream.id); - this.setState(CallState.CreateAnswer); let myAnswer; From 85f4651e9b2fb3349783e71a1d700b027471d661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 15 Aug 2021 12:58:38 +0200 Subject: [PATCH 48/65] Simplifie some code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/webrtc/call.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 8e6be2703..7c16aad9c 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -725,10 +725,7 @@ export class MatrixCall extends EventEmitter { if (this.state === CallState.WaitLocalMedia) { logger.debug("Telling new call to wait for local media"); newCall.waitForLocalAVStream = true; - } else if (this.state === CallState.CreateOffer) { - logger.debug("Handing local stream to new call"); - newCall.gotUserMediaForAnswer(this.localUsermediaStream); - } else if (this.state === CallState.InviteSent) { + } else if ([CallState.CreateOffer, CallState.InviteSent].includes(this.state)) { logger.debug("Handing local stream to new call"); newCall.gotUserMediaForAnswer(this.localUsermediaStream); } From f8ec3bc591d3bfec7f3843752eeab50ef1896a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 15 Aug 2021 13:02:39 +0200 Subject: [PATCH 49/65] Remove weird method call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If we can get localUsermediaStream gotUserMediaForAnswer() has alredy been called before Signed-off-by: Šimon Brandner --- src/webrtc/call.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 7c16aad9c..c891e25d2 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -709,8 +709,6 @@ export class MatrixCall extends EventEmitter { this.getUserMediaFailed(e); return; } - } else if (this.localUsermediaStream) { - this.gotUserMediaForAnswer(this.localUsermediaStream); } else if (this.waitForLocalAVStream) { this.setState(CallState.WaitLocalMedia); } From cdc87d371cfb7307ff89f4b7f5e1dd297244a4bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 15 Aug 2021 13:03:08 +0200 Subject: [PATCH 50/65] Reformat some code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/webrtc/callEventHandler.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 3e080c0eb..89c3228e5 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -201,9 +201,11 @@ export class CallEventHandler { // we've got an invite, pick the incoming call because we know // we haven't sent our invite yet otherwise, pick whichever // call has the lowest call ID (by string comparison) - if (existingCall.state === CallState.WaitLocalMedia || - existingCall.state === CallState.CreateOffer || - existingCall.callId > call.callId) { + if ( + existingCall.state === CallState.WaitLocalMedia || + existingCall.state === CallState.CreateOffer || + existingCall.callId > call.callId + ) { logger.log( "Glare detected: answering incoming call " + call.callId + " and canceling outgoing call " + existingCall.callId, From 082169a7562a32a55cd8ba2ecc0a34c20c33d6c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 15 Aug 2021 13:03:18 +0200 Subject: [PATCH 51/65] Fix glare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/webrtc/callEventHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 89c3228e5..874ca0a51 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -128,7 +128,7 @@ export class CallEventHandler { return type.startsWith("m.call.") || type.startsWith("org.matrix.call."); } - private handleCallEvent(event: MatrixEvent) { + private async handleCallEvent(event: MatrixEvent) { const content = event.getContent(); const type = event.getType() as EventType; const weSentTheEvent = event.getSender() === this.client.credentials.userId; @@ -169,7 +169,7 @@ export class CallEventHandler { } call.callId = content.call_id; - call.initWithInvite(event); + await call.initWithInvite(event); this.calls.set(call.callId, call); // if we stashed candidate events for that call ID, play them back now From b9e541721875b695f8d43889f26fedb7ad38faf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 16 Aug 2021 13:14:04 +0200 Subject: [PATCH 52/65] Await handleCallEvent() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/webrtc/callEventHandler.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 874ca0a51..024412695 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -80,7 +80,7 @@ export class CallEventHandler { continue; } try { - this.handleCallEvent(e); + await this.handleCallEvent(e); } catch (e) { logger.error("Caught exception handling call event", e); } @@ -100,7 +100,7 @@ export class CallEventHandler { if (event.isBeingDecrypted() || event.isDecryptionFailure()) { // add an event listener for once the event is decrypted. - event.once("Event.decrypted", () => { + event.once("Event.decrypted", async () => { if (!this.eventIsACall(event)) return; if (this.callEventBuffer.includes(event)) { @@ -110,7 +110,7 @@ export class CallEventHandler { // This one wasn't buffered so just run the event handler for it // straight away try { - this.handleCallEvent(event); + await this.handleCallEvent(event); } catch (e) { logger.error("Caught exception handling call event", e); } From ab524ad3b410de0d8ce67699471a6244d508aea0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 Aug 2021 13:18:06 +0100 Subject: [PATCH 53/65] Update changelog generator --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6598287f2..a5cfb7744 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@types/request": "^2.48.5", "@typescript-eslint/eslint-plugin": "^4.17.0", "@typescript-eslint/parser": "^4.17.0", - "allchange": "^0.0.1", + "allchange": "^1.0.0", "babel-jest": "^26.6.3", "babelify": "^10.0.0", "better-docs": "^2.4.0-beta.9", diff --git a/yarn.lock b/yarn.lock index 820b6eeca..737863b9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1722,10 +1722,10 @@ align-text@^0.1.1, align-text@^0.1.3: longest "^1.0.1" repeat-string "^1.5.2" -allchange@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/allchange/-/allchange-0.0.1.tgz#3f66f7b06314b942fd0c1630fef6ce2ac1b90972" - integrity sha512-YVg1ZcYzEE5/fEnexzlhfWIbPeGtjfFgJ73qUG2DNwl16Apt9sRJQE7z6dLbDPaL9tOlYpIgzf7Id74uS+ccjQ== +allchange@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/allchange/-/allchange-1.0.0.tgz#f5177b7d97f8e97a2d059a1524db9a72d94dc6d2" + integrity sha512-O0VIaMIORxOaReyYEijDfKdpudJhbzzVYLdJR1aROyUgOLBEp9e5V/TDXQpjX23W90IFCSRZxsDb3exLRD05HA== dependencies: "@actions/core" "^1.4.0" "@actions/github" "^5.0.0" From b56a4ee6ec8e247320b0e40f68a7c5832c88f064 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 Aug 2021 13:24:11 +0100 Subject: [PATCH 54/65] Fix dates --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09e072a45..60e57b5cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ -Changes in [12.3.0-rc.2](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0-rc.2) (2021-07-12) +Changes in [12.3.0-rc.2](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0-rc.2) (2021-08-12) ============================================================================================================= ## 🐛 Bug Fixes * Fix the types in shipped package ([\#1842](https://github.com/matrix-org/matrix-js-sdk/pull/1842)). Fixes vector-im/element-web#18503 and vector-im/element-web#18503. -Changes in [12.3.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0-rc.1) (2021-07-11) +Changes in [12.3.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0-rc.1) (2021-08-11) ============================================================================================================= ## ✨ Features @@ -20,7 +20,7 @@ Changes in [12.3.0-rc.1](https://github.com/vector-im/element-desktop/releases/t * Apply hidden char check to rawDisplayName too ([\#1816](https://github.com/matrix-org/matrix-js-sdk/pull/1816)). * Only clear bit 63 when we create the IV ([\#1819](https://github.com/matrix-org/matrix-js-sdk/pull/1819)). -Changes in [12.2.0](https://github.com/vector-im/element-desktop/releases/tag/v12.2.0) (2021-07-02) +Changes in [12.2.0](https://github.com/vector-im/element-desktop/releases/tag/v12.2.0) (2021-08-02) =================================================================================================== ## ✨ Features From 81e2ea6a61843683bc6046450784c215c534d391 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 16 Aug 2021 13:52:35 +0100 Subject: [PATCH 55/65] Prepare changelog for v12.3.0 --- CHANGELOG.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60e57b5cc..611bd4b72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,5 @@ -Changes in [12.3.0-rc.2](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0-rc.2) (2021-08-12) -============================================================================================================= - -## 🐛 Bug Fixes - * Fix the types in shipped package ([\#1842](https://github.com/matrix-org/matrix-js-sdk/pull/1842)). Fixes vector-im/element-web#18503 and vector-im/element-web#18503. - -Changes in [12.3.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0-rc.1) (2021-08-11) -============================================================================================================= +Changes in [12.3.0](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0) (2021-08-16) +=================================================================================================== ## ✨ Features * Support for MSC3291: Muting in VoIP calls ([\#1812](https://github.com/matrix-org/matrix-js-sdk/pull/1812)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). @@ -13,6 +7,8 @@ Changes in [12.3.0-rc.1](https://github.com/vector-im/element-desktop/releases/t * Handle DTMF support ([\#1813](https://github.com/matrix-org/matrix-js-sdk/pull/1813)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). ## 🐛 Bug Fixes + * [Release] Fix glare related regressions ([\#1854](https://github.com/matrix-org/matrix-js-sdk/pull/1854)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix the types in shipped package ([\#1842](https://github.com/matrix-org/matrix-js-sdk/pull/1842)). Fixes vector-im/element-web#18503 and vector-im/element-web#18503. * Fix error on turning off screensharing ([\#1833](https://github.com/matrix-org/matrix-js-sdk/pull/1833)). Fixes vector-im/element-web#18449. Contributed by [SimonBrandner](https://github.com/SimonBrandner). * Fix blank profile in join events ([\#1837](https://github.com/matrix-org/matrix-js-sdk/pull/1837)). Fixes vector-im/element-web#18321. * fix TURN by fixing regression preventing multiple ICE candidates from sending. ([\#1838](https://github.com/matrix-org/matrix-js-sdk/pull/1838)). From 10e8aff46a8af2d92910d6df24972b5af9495899 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 16 Aug 2021 13:52:36 +0100 Subject: [PATCH 56/65] v12.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a5cfb7744..61e977cb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "12.3.0-rc.2", + "version": "12.3.0", "description": "Matrix Client-Server SDK for Javascript", "scripts": { "prepublishOnly": "yarn build", From db898e7bcf33b8b6b2ec8b3720cc54a775c565c4 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 16 Aug 2021 13:55:37 +0100 Subject: [PATCH 57/65] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 61e977cb7..fa421be78 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "keywords": [ "matrix-org" ], - "main": "./lib/index.js", + "main": "./src/index.ts", "browser": "./lib/browser-index.js", "matrix_src_main": "./src/index.ts", "matrix_src_browser": "./src/browser-index.js", @@ -112,6 +112,5 @@ "coverageReporters": [ "text" ] - }, - "typings": "./lib/index.d.ts" + } } From bfb2c5aad0e48cb801d01fe9c7c34a834ba8b1be Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 Aug 2021 14:38:42 +0100 Subject: [PATCH 58/65] Fix release script to handle no signing ID We now use the file for stuff other than just the signing ID, so the file may be present but with no signing ID: handle this case. --- release.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/release.sh b/release.sh index af42fe886..6754f18bb 100755 --- a/release.sh +++ b/release.sh @@ -191,7 +191,10 @@ git commit package.json $pkglock -m "$tag" # figure out if we should be signing this release signing_id= if [ -f release_config.yaml ]; then - signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"` + result=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']" 2> /dev/null || true` + if [ "$?" -eq 0 ]; then + signing_id=$result + fi fi From 4c552cc3501658a844740958b31374625a9bd3c2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 Aug 2021 23:06:57 +0100 Subject: [PATCH 59/65] Revert "Fix glare related regressions" --- src/webrtc/call.ts | 13 +++++++++++-- src/webrtc/callEventHandler.ts | 18 ++++++++---------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index cfc19b2fe..72b927bd9 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -535,7 +535,6 @@ export class MatrixCall extends EventEmitter { this.emit(CallEvent.FeedsChanged, this.feeds); } - // TODO: Find out what is going on here // why do we enable audio (and only audio) tracks here? -- matthew setTracksEnabled(stream.getAudioTracks(), true); @@ -709,6 +708,8 @@ export class MatrixCall extends EventEmitter { this.getUserMediaFailed(e); return; } + } else if (this.localUsermediaStream) { + this.gotUserMediaForAnswer(this.localUsermediaStream); } else if (this.waitForLocalAVStream) { this.setState(CallState.WaitLocalMedia); } @@ -720,10 +721,14 @@ export class MatrixCall extends EventEmitter { * @param {MatrixCall} newCall The new call. */ replacedBy(newCall: MatrixCall) { + logger.debug(this.callId + " being replaced by " + newCall.callId); if (this.state === CallState.WaitLocalMedia) { logger.debug("Telling new call to wait for local media"); newCall.waitForLocalAVStream = true; - } else if ([CallState.CreateOffer, CallState.InviteSent].includes(this.state)) { + } else if (this.state === CallState.CreateOffer) { + logger.debug("Handing local stream to new call"); + newCall.gotUserMediaForAnswer(this.localUsermediaStream); + } else if (this.state === CallState.InviteSent) { logger.debug("Handing local stream to new call"); newCall.gotUserMediaForAnswer(this.localUsermediaStream); } @@ -1024,6 +1029,7 @@ export class MatrixCall extends EventEmitter { this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia); this.setState(CallState.CreateOffer); + logger.info("Got local AV stream with id " + this.localUsermediaStream.id); logger.debug("gotUserMediaForInvite -> " + this.type); // Now we wait for the negotiationneeded event }; @@ -1081,6 +1087,9 @@ export class MatrixCall extends EventEmitter { } this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia); + + logger.info("Got local AV stream with id " + this.localUsermediaStream.id); + this.setState(CallState.CreateAnswer); let myAnswer; diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 0ce247263..2ad4bcc71 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -80,7 +80,7 @@ export class CallEventHandler { continue; } try { - await this.handleCallEvent(e); + this.handleCallEvent(e); } catch (e) { logger.error("Caught exception handling call event", e); } @@ -100,7 +100,7 @@ export class CallEventHandler { if (event.isBeingDecrypted() || event.isDecryptionFailure()) { // add an event listener for once the event is decrypted. - event.once("Event.decrypted", async () => { + event.once("Event.decrypted", () => { if (!this.eventIsACall(event)) return; if (this.callEventBuffer.includes(event)) { @@ -110,7 +110,7 @@ export class CallEventHandler { // This one wasn't buffered so just run the event handler for it // straight away try { - await this.handleCallEvent(event); + this.handleCallEvent(event); } catch (e) { logger.error("Caught exception handling call event", e); } @@ -128,7 +128,7 @@ export class CallEventHandler { return type.startsWith("m.call.") || type.startsWith("org.matrix.call."); } - private async handleCallEvent(event: MatrixEvent) { + private handleCallEvent(event: MatrixEvent) { const content = event.getContent(); const type = event.getType() as EventType; const weSentTheEvent = event.getSender() === this.client.credentials.userId; @@ -169,7 +169,7 @@ export class CallEventHandler { } call.callId = content.call_id; - await call.initWithInvite(event); + call.initWithInvite(event); this.calls.set(call.callId, call); // if we stashed candidate events for that call ID, play them back now @@ -201,11 +201,9 @@ export class CallEventHandler { // we've got an invite, pick the incoming call because we know // we haven't sent our invite yet otherwise, pick whichever // call has the lowest call ID (by string comparison) - if ( - existingCall.state === CallState.WaitLocalMedia || - existingCall.state === CallState.CreateOffer || - existingCall.callId > call.callId - ) { + if (existingCall.state === CallState.WaitLocalMedia || + existingCall.state === CallState.CreateOffer || + existingCall.callId > call.callId) { logger.log( "Glare detected: answering incoming call " + call.callId + " and canceling outgoing call " + existingCall.callId, From 0d09f877778d8b99cd5d6dde7650ead55ad7589f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 17 Aug 2021 07:33:40 +0200 Subject: [PATCH 60/65] Revert "Revert "Fix glare related regressions"" This reverts commit 4c552cc3501658a844740958b31374625a9bd3c2. --- src/webrtc/call.ts | 13 ++----------- src/webrtc/callEventHandler.ts | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 72b927bd9..cfc19b2fe 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -535,6 +535,7 @@ export class MatrixCall extends EventEmitter { this.emit(CallEvent.FeedsChanged, this.feeds); } + // TODO: Find out what is going on here // why do we enable audio (and only audio) tracks here? -- matthew setTracksEnabled(stream.getAudioTracks(), true); @@ -708,8 +709,6 @@ export class MatrixCall extends EventEmitter { this.getUserMediaFailed(e); return; } - } else if (this.localUsermediaStream) { - this.gotUserMediaForAnswer(this.localUsermediaStream); } else if (this.waitForLocalAVStream) { this.setState(CallState.WaitLocalMedia); } @@ -721,14 +720,10 @@ export class MatrixCall extends EventEmitter { * @param {MatrixCall} newCall The new call. */ replacedBy(newCall: MatrixCall) { - logger.debug(this.callId + " being replaced by " + newCall.callId); if (this.state === CallState.WaitLocalMedia) { logger.debug("Telling new call to wait for local media"); newCall.waitForLocalAVStream = true; - } else if (this.state === CallState.CreateOffer) { - logger.debug("Handing local stream to new call"); - newCall.gotUserMediaForAnswer(this.localUsermediaStream); - } else if (this.state === CallState.InviteSent) { + } else if ([CallState.CreateOffer, CallState.InviteSent].includes(this.state)) { logger.debug("Handing local stream to new call"); newCall.gotUserMediaForAnswer(this.localUsermediaStream); } @@ -1029,7 +1024,6 @@ export class MatrixCall extends EventEmitter { this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia); this.setState(CallState.CreateOffer); - logger.info("Got local AV stream with id " + this.localUsermediaStream.id); logger.debug("gotUserMediaForInvite -> " + this.type); // Now we wait for the negotiationneeded event }; @@ -1087,9 +1081,6 @@ export class MatrixCall extends EventEmitter { } this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia); - - logger.info("Got local AV stream with id " + this.localUsermediaStream.id); - this.setState(CallState.CreateAnswer); let myAnswer; diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 2ad4bcc71..0ce247263 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -80,7 +80,7 @@ export class CallEventHandler { continue; } try { - this.handleCallEvent(e); + await this.handleCallEvent(e); } catch (e) { logger.error("Caught exception handling call event", e); } @@ -100,7 +100,7 @@ export class CallEventHandler { if (event.isBeingDecrypted() || event.isDecryptionFailure()) { // add an event listener for once the event is decrypted. - event.once("Event.decrypted", () => { + event.once("Event.decrypted", async () => { if (!this.eventIsACall(event)) return; if (this.callEventBuffer.includes(event)) { @@ -110,7 +110,7 @@ export class CallEventHandler { // This one wasn't buffered so just run the event handler for it // straight away try { - this.handleCallEvent(event); + await this.handleCallEvent(event); } catch (e) { logger.error("Caught exception handling call event", e); } @@ -128,7 +128,7 @@ export class CallEventHandler { return type.startsWith("m.call.") || type.startsWith("org.matrix.call."); } - private handleCallEvent(event: MatrixEvent) { + private async handleCallEvent(event: MatrixEvent) { const content = event.getContent(); const type = event.getType() as EventType; const weSentTheEvent = event.getSender() === this.client.credentials.userId; @@ -169,7 +169,7 @@ export class CallEventHandler { } call.callId = content.call_id; - call.initWithInvite(event); + await call.initWithInvite(event); this.calls.set(call.callId, call); // if we stashed candidate events for that call ID, play them back now @@ -201,9 +201,11 @@ export class CallEventHandler { // we've got an invite, pick the incoming call because we know // we haven't sent our invite yet otherwise, pick whichever // call has the lowest call ID (by string comparison) - if (existingCall.state === CallState.WaitLocalMedia || - existingCall.state === CallState.CreateOffer || - existingCall.callId > call.callId) { + if ( + existingCall.state === CallState.WaitLocalMedia || + existingCall.state === CallState.CreateOffer || + existingCall.callId > call.callId + ) { logger.log( "Glare detected: answering incoming call " + call.callId + " and canceling outgoing call " + existingCall.callId, From bd8690de57ceae28a2ef78cfe0fce14411923445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 17 Aug 2021 08:09:14 +0200 Subject: [PATCH 61/65] Fix regressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/webrtc/callEventHandler.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 0ce247263..68aaffd0a 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -80,7 +80,7 @@ export class CallEventHandler { continue; } try { - await this.handleCallEvent(e); + this.handleCallEvent(e); } catch (e) { logger.error("Caught exception handling call event", e); } @@ -100,7 +100,7 @@ export class CallEventHandler { if (event.isBeingDecrypted() || event.isDecryptionFailure()) { // add an event listener for once the event is decrypted. - event.once("Event.decrypted", async () => { + event.once("Event.decrypted", () => { if (!this.eventIsACall(event)) return; if (this.callEventBuffer.includes(event)) { @@ -110,7 +110,7 @@ export class CallEventHandler { // This one wasn't buffered so just run the event handler for it // straight away try { - await this.handleCallEvent(event); + this.handleCallEvent(event); } catch (e) { logger.error("Caught exception handling call event", e); } @@ -169,7 +169,7 @@ export class CallEventHandler { } call.callId = content.call_id; - await call.initWithInvite(event); + const initWithInvitePromise = call.initWithInvite(event); this.calls.set(call.callId, call); // if we stashed candidate events for that call ID, play them back now @@ -210,6 +210,8 @@ export class CallEventHandler { "Glare detected: answering incoming call " + call.callId + " and canceling outgoing call " + existingCall.callId, ); + // Await init with invite as we need a peerConn for the following methods + await initWithInvitePromise; existingCall.replacedBy(call); call.answer(); } else { From 1ee84fbeca1f47846f0517b154aec0fbd566aa78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 17 Aug 2021 08:09:14 +0200 Subject: [PATCH 62/65] Fix regressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/webrtc/callEventHandler.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 024412695..1c03fbfeb 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -80,7 +80,7 @@ export class CallEventHandler { continue; } try { - await this.handleCallEvent(e); + this.handleCallEvent(e); } catch (e) { logger.error("Caught exception handling call event", e); } @@ -100,7 +100,7 @@ export class CallEventHandler { if (event.isBeingDecrypted() || event.isDecryptionFailure()) { // add an event listener for once the event is decrypted. - event.once("Event.decrypted", async () => { + event.once("Event.decrypted", () => { if (!this.eventIsACall(event)) return; if (this.callEventBuffer.includes(event)) { @@ -110,7 +110,7 @@ export class CallEventHandler { // This one wasn't buffered so just run the event handler for it // straight away try { - await this.handleCallEvent(event); + this.handleCallEvent(event); } catch (e) { logger.error("Caught exception handling call event", e); } @@ -169,7 +169,7 @@ export class CallEventHandler { } call.callId = content.call_id; - await call.initWithInvite(event); + const initWithInvitePromise = call.initWithInvite(event); this.calls.set(call.callId, call); // if we stashed candidate events for that call ID, play them back now @@ -210,6 +210,8 @@ export class CallEventHandler { "Glare detected: answering incoming call " + call.callId + " and canceling outgoing call " + existingCall.callId, ); + // Await init with invite as we need a peerConn for the following methods + await initWithInvitePromise; existingCall.replacedBy(call); call.answer(); } else { From 66b4279dd801852a233f919837436c75d8de4344 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 Aug 2021 14:38:42 +0100 Subject: [PATCH 63/65] Fix release script to handle no signing ID We now use the file for stuff other than just the signing ID, so the file may be present but with no signing ID: handle this case. --- release.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/release.sh b/release.sh index af42fe886..6754f18bb 100755 --- a/release.sh +++ b/release.sh @@ -191,7 +191,10 @@ git commit package.json $pkglock -m "$tag" # figure out if we should be signing this release signing_id= if [ -f release_config.yaml ]; then - signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"` + result=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']" 2> /dev/null || true` + if [ "$?" -eq 0 ]; then + signing_id=$result + fi fi From 543f35d9a8b865304fdcfe5be42a576982cf4ac4 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 17 Aug 2021 09:24:42 +0100 Subject: [PATCH 64/65] Prepare changelog for v12.3.1 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 611bd4b72..541369b81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [12.3.1](https://github.com/vector-im/element-desktop/releases/tag/v12.3.1) (2021-08-17) +=================================================================================================== + +## 🐛 Bug Fixes + * Fix multiple VoIP regressions ([\#1860](https://github.com/matrix-org/matrix-js-sdk/pull/1860)). + Changes in [12.3.0](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0) (2021-08-16) =================================================================================================== From b68f649414cc3b9b9c14e5d80135071764174dc8 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 17 Aug 2021 09:24:43 +0100 Subject: [PATCH 65/65] v12.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 61e977cb7..56f65c567 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "12.3.0", + "version": "12.3.1", "description": "Matrix Client-Server SDK for Javascript", "scripts": { "prepublishOnly": "yarn build",