You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-28 05:03:59 +03:00
Merge branch 'develop' into dbkr/cross_signing
This commit is contained in:
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
patreon: matrixdotorg
|
||||
liberapay: matrixdotorg
|
||||
@@ -296,7 +296,7 @@ Then visit ``http://localhost:8005`` to see the API docs.
|
||||
End-to-end encryption support
|
||||
=============================
|
||||
|
||||
The SDK supports end-to-end encryption via the and Megolm protocols, using
|
||||
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
||||
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
||||
application to make libolm available, via the ``Olm`` global.
|
||||
|
||||
|
||||
@@ -275,7 +275,7 @@ describe("AutoDiscovery", function() {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: null,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
@@ -304,7 +304,7 @@ describe("AutoDiscovery", function() {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: null,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
@@ -335,7 +335,7 @@ describe("AutoDiscovery", function() {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: null,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
@@ -528,7 +528,7 @@ describe("AutoDiscovery", function() {
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
base_url: null,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -569,7 +569,7 @@ describe("AutoDiscovery", function() {
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
base_url: null,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("MatrixScheduler", function() {
|
||||
clock.uninstall();
|
||||
});
|
||||
|
||||
it("should process events in a queue in a FIFO manner", function(done) {
|
||||
it("should process events in a queue in a FIFO manner", async function() {
|
||||
retryFn = function() {
|
||||
return 0;
|
||||
};
|
||||
@@ -57,28 +57,30 @@ describe("MatrixScheduler", function() {
|
||||
};
|
||||
const deferA = Promise.defer();
|
||||
const deferB = Promise.defer();
|
||||
let resolvedA = false;
|
||||
let yieldedA = false;
|
||||
scheduler.setProcessFunction(function(event) {
|
||||
if (resolvedA) {
|
||||
if (yieldedA) {
|
||||
expect(event).toEqual(eventB);
|
||||
return deferB.promise;
|
||||
} else {
|
||||
yieldedA = true;
|
||||
expect(event).toEqual(eventA);
|
||||
return deferA.promise;
|
||||
}
|
||||
});
|
||||
scheduler.queueEvent(eventA);
|
||||
scheduler.queueEvent(eventB).done(function() {
|
||||
expect(resolvedA).toBe(true);
|
||||
done();
|
||||
});
|
||||
deferA.resolve({});
|
||||
resolvedA = true;
|
||||
deferB.resolve({});
|
||||
const abPromise = Promise.all([
|
||||
scheduler.queueEvent(eventA),
|
||||
scheduler.queueEvent(eventB),
|
||||
]);
|
||||
deferB.resolve({b: true});
|
||||
deferA.resolve({a: true});
|
||||
const [a, b] = await abPromise;
|
||||
expect(a.a).toEqual(true);
|
||||
expect(b.b).toEqual(true);
|
||||
});
|
||||
|
||||
it("should invoke the retryFn on failure and wait the amount of time specified",
|
||||
function(done) {
|
||||
async function() {
|
||||
const waitTimeMs = 1500;
|
||||
const retryDefer = Promise.defer();
|
||||
retryFn = function() {
|
||||
@@ -97,24 +99,26 @@ describe("MatrixScheduler", function() {
|
||||
return defer.promise;
|
||||
} else if (procCount === 2) {
|
||||
// don't care about this defer
|
||||
return Promise.defer().promise;
|
||||
return new Promise();
|
||||
}
|
||||
expect(procCount).toBeLessThan(3);
|
||||
});
|
||||
|
||||
scheduler.queueEvent(eventA);
|
||||
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
|
||||
// wait just long enough before it does
|
||||
await Promise.resolve();
|
||||
expect(procCount).toEqual(1);
|
||||
defer.reject({});
|
||||
retryDefer.promise.done(function() {
|
||||
expect(procCount).toEqual(1);
|
||||
clock.tick(waitTimeMs);
|
||||
expect(procCount).toEqual(2);
|
||||
done();
|
||||
});
|
||||
await retryDefer.promise;
|
||||
expect(procCount).toEqual(1);
|
||||
clock.tick(waitTimeMs);
|
||||
await Promise.resolve();
|
||||
expect(procCount).toEqual(2);
|
||||
});
|
||||
|
||||
it("should give up if the retryFn on failure returns -1 and try the next event",
|
||||
function(done) {
|
||||
async function() {
|
||||
// Queue A & B.
|
||||
// Reject A and return -1 on retry.
|
||||
// Expect B to be tried next and the promise for A to be rejected.
|
||||
@@ -122,8 +126,8 @@ describe("MatrixScheduler", function() {
|
||||
return -1;
|
||||
};
|
||||
queueFn = function() {
|
||||
return "yep";
|
||||
};
|
||||
return "yep";
|
||||
};
|
||||
|
||||
const deferA = Promise.defer();
|
||||
const deferB = Promise.defer();
|
||||
@@ -142,13 +146,17 @@ describe("MatrixScheduler", function() {
|
||||
|
||||
const globalA = scheduler.queueEvent(eventA);
|
||||
scheduler.queueEvent(eventB);
|
||||
|
||||
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
|
||||
// wait just long enough before it does
|
||||
await Promise.resolve();
|
||||
expect(procCount).toEqual(1);
|
||||
deferA.reject({});
|
||||
globalA.catch(function() {
|
||||
try {
|
||||
await globalA;
|
||||
} catch(err) {
|
||||
await Promise.resolve();
|
||||
expect(procCount).toEqual(2);
|
||||
done();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("should treat each queue separately", function(done) {
|
||||
@@ -300,7 +308,11 @@ describe("MatrixScheduler", function() {
|
||||
expect(ev).toEqual(eventA);
|
||||
return defer.promise;
|
||||
});
|
||||
expect(procCount).toEqual(1);
|
||||
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
|
||||
// wait just long enough before it does
|
||||
Promise.resolve().then(() => {
|
||||
expect(procCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call the processFn if there are no queued events", function() {
|
||||
|
||||
@@ -256,6 +256,11 @@ export class AutoDiscovery {
|
||||
if (!hsVersions || !hsVersions.raw["versions"]) {
|
||||
logger.error("Invalid /versions response");
|
||||
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER;
|
||||
|
||||
// Supply the base_url to the caller because they may be ignoring liveliness
|
||||
// errors, like this one.
|
||||
clientConfig["m.homeserver"].base_url = hsUrl;
|
||||
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
@@ -311,6 +316,11 @@ export class AutoDiscovery {
|
||||
logger.error("Invalid /api/v1 response");
|
||||
failingClientConfig["m.identity_server"].error =
|
||||
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
|
||||
|
||||
// Supply the base_url to the caller because they may be ignoring
|
||||
// liveliness errors, like this one.
|
||||
failingClientConfig["m.identity_server"].base_url = isUrl;
|
||||
|
||||
return Promise.resolve(failingClientConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,13 +151,14 @@ MatrixBaseApis.prototype.isUsernameAvailable = function(username) {
|
||||
* threepid uses during registration in the ID server. Set 'msisdn' to
|
||||
* true to bind msisdn.
|
||||
* @param {string} guestAccessToken
|
||||
* @param {string} inhibitLogin
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.register = function(
|
||||
username, password,
|
||||
sessionId, auth, bindThreepids, guestAccessToken,
|
||||
sessionId, auth, bindThreepids, guestAccessToken, inhibitLogin,
|
||||
callback,
|
||||
) {
|
||||
// backwards compat
|
||||
@@ -166,6 +167,10 @@ MatrixBaseApis.prototype.register = function(
|
||||
} else if (bindThreepids === null || bindThreepids === undefined) {
|
||||
bindThreepids = {};
|
||||
}
|
||||
if (typeof inhibitLogin === 'function') {
|
||||
callback = inhibitLogin;
|
||||
inhibitLogin = undefined;
|
||||
}
|
||||
|
||||
if (auth === undefined || auth === null) {
|
||||
auth = {};
|
||||
@@ -192,6 +197,9 @@ MatrixBaseApis.prototype.register = function(
|
||||
if (guestAccessToken !== undefined && guestAccessToken !== null) {
|
||||
params.guest_access_token = guestAccessToken;
|
||||
}
|
||||
if (inhibitLogin !== undefined && inhibitLogin !== null) {
|
||||
params.inhibit_login = inhibitLogin;
|
||||
}
|
||||
// Temporary parameter added to make the register endpoint advertise
|
||||
// msisdn flows. This exists because there are clients that break
|
||||
// when given stages they don't recognise. This parameter will cease
|
||||
|
||||
@@ -740,19 +740,19 @@ async function _setDeviceVerification(
|
||||
* Request a key verification from another user.
|
||||
*
|
||||
* @param {string} userId the user to request verification with
|
||||
* @param {Array} devices array of device IDs to send requests to. Defaults to
|
||||
* all devices owned by the user
|
||||
* @param {Array} methods array of verification methods to use. Defaults to
|
||||
* all known methods
|
||||
* @param {Array} devices array of device IDs to send requests to. Defaults to
|
||||
* all devices owned by the user
|
||||
*
|
||||
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
|
||||
* when the request is accepted by the other user
|
||||
*/
|
||||
MatrixClient.prototype.requestVerification = function(userId, devices, methods) {
|
||||
MatrixClient.prototype.requestVerification = function(userId, methods, devices) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this._crypto.requestVerification(userId, devices);
|
||||
return this._crypto.requestVerification(userId, methods, devices);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2047,6 +2047,9 @@ MatrixClient.prototype._sendCompleteEvent = function(roomId, eventObject, txnId,
|
||||
txnId = this.makeTxnId();
|
||||
}
|
||||
|
||||
// we always construct a MatrixEvent when sending because the store and
|
||||
// scheduler use them. We'll extract the params back out if it turns out
|
||||
// the client has no scheduler or store.
|
||||
const localEvent = new MatrixEvent(Object.assign(eventObject, {
|
||||
event_id: "~" + roomId + ":" + txnId,
|
||||
user_id: this.credentials.userId,
|
||||
@@ -2054,13 +2057,23 @@ MatrixClient.prototype._sendCompleteEvent = function(roomId, eventObject, txnId,
|
||||
origin_server_ts: new Date().getTime(),
|
||||
}));
|
||||
|
||||
const room = this.getRoom(roomId);
|
||||
|
||||
// if this is a relation or redaction of an event
|
||||
// that hasn't been sent yet (e.g. with a local id starting with a ~)
|
||||
// then listen for the remote echo of that event so that by the time
|
||||
// this event does get sent, we have the correct event_id
|
||||
const targetId = localEvent.getAssociatedId();
|
||||
if (targetId && targetId.startsWith("~")) {
|
||||
const target = room.getPendingEvents().find(e => e.getId() === targetId);
|
||||
target.once("Event.localEventIdReplaced", () => {
|
||||
localEvent.updateAssociatedId(target.getId());
|
||||
});
|
||||
}
|
||||
|
||||
const type = localEvent.getType();
|
||||
logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`);
|
||||
|
||||
// we always construct a MatrixEvent when sending because the store and
|
||||
// scheduler use them. We'll extract the params back out if it turns out
|
||||
// the client has no scheduler or store.
|
||||
const room = this.getRoom(roomId);
|
||||
localEvent._txnId = txnId;
|
||||
localEvent.setStatus(EventStatus.SENDING);
|
||||
|
||||
@@ -2214,9 +2227,11 @@ function _sendEventHttpRequest(client, event) {
|
||||
pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey";
|
||||
}
|
||||
path = utils.encodeUri(pathTemplate, pathParams);
|
||||
} else if (event.getType() === "m.room.redaction") {
|
||||
const pathTemplate = `/rooms/$roomId/redact/${event.event.redacts}/$txnId`;
|
||||
path = utils.encodeUri(pathTemplate, pathParams);
|
||||
} else if (event.isRedaction()) {
|
||||
const pathTemplate = `/rooms/$roomId/redact/$redactsEventId/$txnId`;
|
||||
path = utils.encodeUri(pathTemplate, Object.assign({
|
||||
$redactsEventId: event.event.redacts,
|
||||
}, pathParams));
|
||||
} else {
|
||||
path = utils.encodeUri(
|
||||
"/rooms/$roomId/send/$eventType/$txnId", pathParams,
|
||||
|
||||
@@ -693,7 +693,9 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
|
||||
try {
|
||||
await olmlib.verifySignature(
|
||||
this._olmDevice,
|
||||
backupInfo.auth_data,
|
||||
// verifySignature modifies the object so we need to copy
|
||||
// if we verify more than one sig
|
||||
Object.assign({}, backupInfo.auth_data),
|
||||
this._userId,
|
||||
device.deviceId,
|
||||
device.getFingerprint(),
|
||||
@@ -2095,6 +2097,11 @@ Crypto.prototype._onKeyVerificationRequest = function(event) {
|
||||
}
|
||||
|
||||
const sender = event.getSender();
|
||||
if (sender === this._userId && content.from_device === this._deviceId) {
|
||||
// ignore requests from ourselves, because it doesn't make sense for a
|
||||
// device to verify itself
|
||||
return;
|
||||
}
|
||||
if (this._verificationTransactions.has(sender)) {
|
||||
if (this._verificationTransactions.get(sender).has(content.transaction_id)) {
|
||||
// transaction already exists: cancel it and drop the existing
|
||||
@@ -2147,7 +2154,7 @@ Crypto.prototype._onKeyVerificationRequest = function(event) {
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// notify the application that of the verification request, so it can
|
||||
// notify the application of the verification request, so it can
|
||||
// decide what to do with it
|
||||
const request = {
|
||||
event: event,
|
||||
|
||||
@@ -165,9 +165,21 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
const contentType = opts.type || file.type || 'application/octet-stream';
|
||||
const fileName = opts.name || file.name;
|
||||
|
||||
// we used to recommend setting file.stream to the thing to upload on
|
||||
// nodejs.
|
||||
const body = file.stream ? file.stream : file;
|
||||
// We used to recommend setting file.stream to the thing to upload on
|
||||
// Node.js. As of 2019-06-11, this is still in widespread use in various
|
||||
// clients, so we should preserve this for simple objects used in
|
||||
// Node.js. File API objects (via either the File or Blob interfaces) in
|
||||
// the browser now define a `stream` method, which leads to trouble
|
||||
// here, so we also check the type of `stream`.
|
||||
let body = file;
|
||||
if (body.stream && typeof body.stream !== "function") {
|
||||
logger.warn(
|
||||
"Using `file.stream` as the content to upload. Future " +
|
||||
"versions of the js-sdk will change this to expect `file` to " +
|
||||
"be the content directly.",
|
||||
);
|
||||
body = body.stream;
|
||||
}
|
||||
|
||||
// backwards-compatibility hacks where we used to do different things
|
||||
// between browser and node.
|
||||
|
||||
@@ -49,11 +49,18 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
|
||||
* @param {object?} opts.authData error response from the last request. If
|
||||
* null, a request will be made with no auth before starting.
|
||||
*
|
||||
* @param {function(object?, bool?): module:client.Promise} opts.doRequest
|
||||
* called with the new auth dict to submit the request and a flag set
|
||||
* to true if this request is a background request. Should return a
|
||||
* promise which resolves to the successful response or rejects with a
|
||||
* MatrixError.
|
||||
* @param {function(object?): module:client.Promise} opts.doRequest
|
||||
* 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
|
||||
* to the successful response or rejects with a MatrixError.
|
||||
*
|
||||
* @param {function(bool): module:client.Promise} opts.busyChanged
|
||||
* called whenever the interactive auth logic becomes busy submitting
|
||||
* information provided by the user or finsihes. After this has been
|
||||
* called with true the UI should indicate that a request is in progress
|
||||
* until it is called again with false.
|
||||
*
|
||||
* @param {function(string, object?)} opts.stateUpdated
|
||||
* called when the status of the UI auth changes, ie. when the state of
|
||||
@@ -101,6 +108,7 @@ 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;
|
||||
@@ -112,9 +120,14 @@ function InteractiveAuth(opts) {
|
||||
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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
InteractiveAuth.prototype = {
|
||||
@@ -135,7 +148,10 @@ InteractiveAuth.prototype = {
|
||||
// if we have no flows, try a request (we'll have
|
||||
// just a session ID in _data if resuming)
|
||||
if (!this._data.flows) {
|
||||
this._doRequest(this._data);
|
||||
if (this._busyChangedCallback) this._busyChangedCallback(true);
|
||||
this._doRequest(this._data).finally(() => {
|
||||
if (this._busyChangedCallback) this._busyChangedCallback(false);
|
||||
});
|
||||
} else {
|
||||
this._startNextAuthStage();
|
||||
}
|
||||
@@ -147,8 +163,11 @@ InteractiveAuth.prototype = {
|
||||
* completed out-of-band. If so, the attemptAuth promise will
|
||||
* be resolved.
|
||||
*/
|
||||
poll: function() {
|
||||
poll: async function() {
|
||||
if (!this._data.session) 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;
|
||||
|
||||
let authDict = {};
|
||||
if (this._currentStage == EMAIL_STAGE_TYPE) {
|
||||
@@ -221,18 +240,44 @@ InteractiveAuth.prototype = {
|
||||
* 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: function(authData, background) {
|
||||
submitAuthDict: async function(authData, background) {
|
||||
if (!this._resolveFunc) {
|
||||
throw new Error("submitAuthDict() called before attemptAuth()");
|
||||
}
|
||||
|
||||
if (!background && this._busyChangedCallback) {
|
||||
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,
|
||||
// not whether it worked or not)
|
||||
while (this._submitPromise) {
|
||||
try {
|
||||
await this._submitPromise;
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
// use the sessionid from the last request.
|
||||
const auth = {
|
||||
session: this._data.session,
|
||||
};
|
||||
utils.extend(auth, authData);
|
||||
|
||||
this._doRequest(auth, background);
|
||||
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;
|
||||
} finally {
|
||||
this._submitPromise = null;
|
||||
if (!background && this._busyChangedCallback) {
|
||||
this._busyChangedCallback(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -305,12 +350,14 @@ InteractiveAuth.prototype = {
|
||||
|
||||
if (
|
||||
!this._emailSid &&
|
||||
!this._requestingEmailToken &&
|
||||
this._chosenFlow.stages.includes('m.login.email.identity')
|
||||
) {
|
||||
// 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;
|
||||
try {
|
||||
const requestTokenResult = await this._requestEmailTokenCallback(
|
||||
this._inputs.emailAddress,
|
||||
@@ -333,6 +380,8 @@ InteractiveAuth.prototype = {
|
||||
// the failure up as the user can't complete auth if we can't
|
||||
// send the email, foe whatever reason.
|
||||
this._rejectFunc(e);
|
||||
} finally {
|
||||
this._requestingEmailToken = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ limitations under the License.
|
||||
const EventEmitter = require("events").EventEmitter;
|
||||
const utils = require("../utils");
|
||||
const EventTimeline = require("./event-timeline");
|
||||
import {EventStatus} from "./event";
|
||||
import logger from '../../src/logger';
|
||||
import Relations from './relations';
|
||||
|
||||
@@ -749,6 +750,10 @@ EventTimelineSet.prototype.aggregateRelations = function(event) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.isRedacted() || event.status === EventStatus.CANCELLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the event is currently encrypted, wait until it has been decrypted.
|
||||
if (event.isBeingDecrypted()) {
|
||||
event.once("Event.decrypted", () => {
|
||||
|
||||
@@ -753,6 +753,15 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
return Boolean(this.getUnsigned().redacted_because);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if this event is a redaction of another event
|
||||
*
|
||||
* @return {boolean} True if this event is a redaction
|
||||
*/
|
||||
isRedaction: function() {
|
||||
return this.getType() === "m.room.redaction";
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the push actions, if known, for this event
|
||||
*
|
||||
@@ -776,9 +785,26 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
* @param {Object} event the object to assign to the `event` property
|
||||
*/
|
||||
handleRemoteEcho: function(event) {
|
||||
const oldUnsigned = this.getUnsigned();
|
||||
const oldId = this.getId();
|
||||
this.event = event;
|
||||
// if this event was redacted before it was sent, it's locally marked as redacted.
|
||||
// At this point, we've received the remote echo for the event, but not yet for
|
||||
// the redaction that we are sending ourselves. Preserve the locally redacted
|
||||
// state by copying over redacted_because so we don't get a flash of
|
||||
// redacted, not-redacted, redacted as remote echos come in
|
||||
if (oldUnsigned.redacted_because) {
|
||||
if (!this.event.unsigned) {
|
||||
this.event.unsigned = {};
|
||||
}
|
||||
this.event.unsigned.redacted_because = oldUnsigned.redacted_because;
|
||||
}
|
||||
// successfully sent.
|
||||
this.setStatus(null);
|
||||
if (this.getId() !== oldId) {
|
||||
// emit the event if it changed
|
||||
this.emit("Event.localEventIdReplaced", this);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -801,6 +827,11 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
this.emit("Event.status", this, status);
|
||||
},
|
||||
|
||||
replaceLocalEventId(eventId) {
|
||||
this.event.event_id = eventId;
|
||||
this.emit("Event.localEventIdReplaced", this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get whether the event is a relation event, and of a given type if
|
||||
* `relType` is passed in.
|
||||
@@ -876,6 +907,46 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
return this._replacingEvent;
|
||||
},
|
||||
|
||||
/**
|
||||
* For relations and redactions, returns the event_id this event is referring to.
|
||||
*
|
||||
* @return {string?}
|
||||
*/
|
||||
getAssociatedId() {
|
||||
const relation = this.getRelation();
|
||||
if (relation) {
|
||||
return relation.event_id;
|
||||
} else if (this.isRedaction()) {
|
||||
return this.event.redacts;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if this event is associated with another event. See `getAssociatedId`.
|
||||
*
|
||||
* @return {bool}
|
||||
*/
|
||||
hasAssocation() {
|
||||
return !!this.getAssociatedId();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the related id with a new one.
|
||||
*
|
||||
* Used to replace a local id with remote one before sending
|
||||
* an event with a related id.
|
||||
*
|
||||
* @param {string} eventId the new event id
|
||||
*/
|
||||
updateAssociatedId(eventId) {
|
||||
const relation = this.getRelation();
|
||||
if (relation) {
|
||||
relation.event_id = eventId;
|
||||
} else if (this.isRedaction()) {
|
||||
this.event.redacts = eventId;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -242,12 +242,7 @@ export default class Relations extends EventEmitter {
|
||||
|
||||
redactedEvent.removeListener("Event.beforeRedaction", this._onBeforeRedaction);
|
||||
|
||||
// Dispatch a redaction event on this collection. `setTimeout` is used
|
||||
// to wait until the next event loop iteration by which time the event
|
||||
// has actually been marked as redacted.
|
||||
setTimeout(() => {
|
||||
this.emit("Relations.redaction");
|
||||
}, 0);
|
||||
this.emit("Relations.redaction");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1032,7 +1032,7 @@ Room.prototype.removeFilteredTimelineSet = function(filter) {
|
||||
* @private
|
||||
*/
|
||||
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
||||
if (event.getType() === "m.room.redaction") {
|
||||
if (event.isRedaction()) {
|
||||
const redactId = event.event.redacts;
|
||||
|
||||
// if we know about this event, redact its contents now.
|
||||
@@ -1141,9 +1141,13 @@ Room.prototype.addPendingEvent = function(event, txnId) {
|
||||
this._aggregateNonLiveRelation(event);
|
||||
}
|
||||
|
||||
if (event.getType() === "m.room.redaction") {
|
||||
if (event.isRedaction()) {
|
||||
const redactId = event.event.redacts;
|
||||
const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
|
||||
let redactedEvent = this._pendingEventList &&
|
||||
this._pendingEventList.find(e => e.getId() === redactId);
|
||||
if (!redactedEvent) {
|
||||
redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
|
||||
}
|
||||
if (redactedEvent) {
|
||||
redactedEvent.markLocallyRedacted(event);
|
||||
this.emit("Room.redaction", event, this);
|
||||
@@ -1211,7 +1215,7 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
|
||||
const oldStatus = localEvent.status;
|
||||
|
||||
// no longer pending
|
||||
delete this._txnToEvent[remoteEvent.transaction_id];
|
||||
delete this._txnToEvent[remoteEvent.getUnsigned().transaction_id];
|
||||
|
||||
// if it's in the pending list, remove it
|
||||
if (this._pendingEventList) {
|
||||
@@ -1315,7 +1319,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
|
||||
|
||||
if (newStatus == EventStatus.SENT) {
|
||||
// update the event id
|
||||
event.event.event_id = newEventId;
|
||||
event.replaceLocalEventId(newEventId);
|
||||
|
||||
// if the event was already in the timeline (which will be the case if
|
||||
// opts.pendingEventOrdering==chronological), we need to update the
|
||||
@@ -1329,7 +1333,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
|
||||
const idx = this._pendingEventList.findIndex(ev => ev.getId() === oldEventId);
|
||||
if (idx !== -1) {
|
||||
const [removedEvent] = this._pendingEventList.splice(idx, 1);
|
||||
if (removedEvent.getType() === "m.room.redaction") {
|
||||
if (removedEvent.isRedaction()) {
|
||||
this._revertRedactionLocalEcho(removedEvent);
|
||||
}
|
||||
}
|
||||
@@ -1435,7 +1439,7 @@ Room.prototype.removeEvent = function(eventId) {
|
||||
for (let i = 0; i < this._timelineSets.length; i++) {
|
||||
const removed = this._timelineSets[i].removeEvent(eventId);
|
||||
if (removed) {
|
||||
if (removed.getType() === "m.room.redaction") {
|
||||
if (removed.isRedaction()) {
|
||||
this._revertRedactionLocalEcho(removed);
|
||||
}
|
||||
removedAny = true;
|
||||
|
||||
@@ -177,7 +177,8 @@ MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
|
||||
* @see module:scheduler~queueAlgorithm
|
||||
*/
|
||||
MatrixScheduler.QUEUE_MESSAGES = function(event) {
|
||||
if (event.getType() === "m.room.message") {
|
||||
// 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";
|
||||
}
|
||||
@@ -220,7 +221,14 @@ function _processQueue(scheduler, queueName) {
|
||||
);
|
||||
// fire the process function and if it resolves, resolve the deferred. Else
|
||||
// invoke the retry algorithm.
|
||||
scheduler._procFn(obj.event).done(function(res) {
|
||||
|
||||
// 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());
|
||||
|
||||
@@ -198,11 +198,13 @@ IndexedDBStore.prototype.wantsSave = function() {
|
||||
|
||||
/**
|
||||
* Possibly write data to the database.
|
||||
*
|
||||
* @param {bool} force True to force a save to happen
|
||||
* @return {Promise} Promise resolves after the write completes
|
||||
* (or immediately if no write is performed)
|
||||
*/
|
||||
IndexedDBStore.prototype.save = function() {
|
||||
if (this.wantsSave()) {
|
||||
IndexedDBStore.prototype.save = function(force) {
|
||||
if (force || this.wantsSave()) {
|
||||
return this._reallySave();
|
||||
}
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -335,8 +335,10 @@ module.exports.MemoryStore.prototype = {
|
||||
|
||||
/**
|
||||
* Save does nothing as there is no backing data store.
|
||||
* @param {bool} force True to force a save (but the memory
|
||||
* store still can't save anything)
|
||||
*/
|
||||
save: function() {},
|
||||
save: function(force) {},
|
||||
|
||||
/**
|
||||
* Startup does nothing as this store doesn't require starting up.
|
||||
|
||||
Reference in New Issue
Block a user