You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-12-05 17:02:07 +03:00
Merge pull request #1140 from matrix-org/bwindels/verification-right-panel
Support for .ready verification event (MSC2366) & other things
This commit is contained in:
@@ -22,6 +22,8 @@ import {makeTestClients} from './util';
|
|||||||
|
|
||||||
const Olm = global.Olm;
|
const Olm = global.Olm;
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
describe("verification request", function() {
|
describe("verification request", function() {
|
||||||
if (!global.Olm) {
|
if (!global.Olm) {
|
||||||
logger.warn('Not running device verification unit tests: libolm not present');
|
logger.warn('Not running device verification unit tests: libolm not present');
|
||||||
@@ -64,7 +66,9 @@ describe("verification request", function() {
|
|||||||
// XXX: Private function access (but it's a test, so we're okay)
|
// XXX: Private function access (but it's a test, so we're okay)
|
||||||
bobVerifier._endTimer();
|
bobVerifier._endTimer();
|
||||||
});
|
});
|
||||||
const aliceVerifier = await alice.client.requestVerification("@bob:example.com");
|
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
|
||||||
|
await aliceRequest.waitFor(r => r.started);
|
||||||
|
const aliceVerifier = aliceRequest.verifier;
|
||||||
expect(aliceVerifier).toBeInstanceOf(SAS);
|
expect(aliceVerifier).toBeInstanceOf(SAS);
|
||||||
|
|
||||||
// XXX: Private function access (but it's a test, so we're okay)
|
// XXX: Private function access (but it's a test, so we're okay)
|
||||||
|
|||||||
@@ -442,9 +442,11 @@ describe("SAS verification", function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
aliceVerifier = await alice.client.requestVerificationDM(
|
const aliceRequest = await alice.client.requestVerificationDM(
|
||||||
bob.client.getUserId(), "!room_id", [verificationMethods.SAS],
|
bob.client.getUserId(), "!room_id", [verificationMethods.SAS],
|
||||||
);
|
);
|
||||||
|
await aliceRequest.waitFor(r => r.started);
|
||||||
|
aliceVerifier = aliceRequest.verifier;
|
||||||
aliceVerifier.on("show_sas", (e) => {
|
aliceVerifier.on("show_sas", (e) => {
|
||||||
if (!e.sas.emoji || !e.sas.decimal) {
|
if (!e.sas.emoji || !e.sas.decimal) {
|
||||||
e.cancel();
|
e.cancel();
|
||||||
|
|||||||
@@ -33,38 +33,48 @@ export async function makeTestClients(userInfos, options) {
|
|||||||
content: msg,
|
content: msg,
|
||||||
});
|
});
|
||||||
const client = clientMap[userId][deviceId];
|
const client = clientMap[userId][deviceId];
|
||||||
if (event.isEncrypted()) {
|
const decryptionPromise = event.isEncrypted() ?
|
||||||
event.attemptDecryption(client._crypto)
|
event.attemptDecryption(client._crypto) :
|
||||||
.then(() => client.emit("toDeviceEvent", event));
|
Promise.resolve();
|
||||||
} else {
|
|
||||||
setTimeout(
|
decryptionPromise.then(
|
||||||
() => client.emit("toDeviceEvent", event),
|
() => client.emit("toDeviceEvent", event),
|
||||||
0,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const sendEvent = function(room, type, content) {
|
const sendEvent = function(room, type, content) {
|
||||||
// make up a unique ID as the event ID
|
// make up a unique ID as the event ID
|
||||||
const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this
|
const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this
|
||||||
const event = new MatrixEvent({
|
const rawEvent = {
|
||||||
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
|
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
|
||||||
type: type,
|
type: type,
|
||||||
content: content,
|
content: content,
|
||||||
room_id: room,
|
room_id: room,
|
||||||
event_id: eventId,
|
event_id: eventId,
|
||||||
});
|
origin_server_ts: Date.now(),
|
||||||
for (const tc of clients) {
|
};
|
||||||
setTimeout(
|
const event = new MatrixEvent(rawEvent);
|
||||||
() => tc.client.emit("Room.timeline", event),
|
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
|
||||||
0,
|
unsigned: {
|
||||||
);
|
transaction_id: this.makeTxnId(), // eslint-disable-line babel/no-invalid-this
|
||||||
}
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
return {event_id: eventId};
|
setImmediate(() => {
|
||||||
|
for (const tc of clients) {
|
||||||
|
if (tc.client === this) { // eslint-disable-line babel/no-invalid-this
|
||||||
|
console.log("sending remote echo!!");
|
||||||
|
tc.client.emit("Room.timeline", remoteEcho);
|
||||||
|
} else {
|
||||||
|
tc.client.emit("Room.timeline", event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve({event_id: eventId});
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const userInfo of userInfos) {
|
for (const userInfo of userInfos) {
|
||||||
|
|||||||
@@ -865,8 +865,8 @@ async function _setDeviceVerification(
|
|||||||
* @param {Array} methods array of verification methods to use. Defaults to
|
* @param {Array} methods array of verification methods to use. Defaults to
|
||||||
* all known methods
|
* all known methods
|
||||||
*
|
*
|
||||||
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
|
* @returns {Promise<module:crypto/verification/request/VerificationRequest>} resolves to a VerificationRequest
|
||||||
* when the request is accepted by the other user
|
* when the request has been sent to the other party.
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.requestVerificationDM = function(userId, roomId, methods) {
|
MatrixClient.prototype.requestVerificationDM = function(userId, roomId, methods) {
|
||||||
if (this._crypto === null) {
|
if (this._crypto === null) {
|
||||||
@@ -875,22 +875,6 @@ MatrixClient.prototype.requestVerificationDM = function(userId, roomId, methods)
|
|||||||
return this._crypto.requestVerificationDM(userId, roomId, methods);
|
return this._crypto.requestVerificationDM(userId, roomId, methods);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Accept a key verification request from a DM.
|
|
||||||
*
|
|
||||||
* @param {module:models/event~MatrixEvent} event the verification request
|
|
||||||
* that is accepted
|
|
||||||
* @param {string} method the verification mmethod to use
|
|
||||||
*
|
|
||||||
* @returns {module:crypto/verification/Base} a verifier
|
|
||||||
*/
|
|
||||||
MatrixClient.prototype.acceptVerificationDM = function(event, method) {
|
|
||||||
if (this._crypto === null) {
|
|
||||||
throw new Error("End-to-end encryption disabled");
|
|
||||||
}
|
|
||||||
return this._crypto.acceptVerificationDM(event, method);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request a key verification from another user.
|
* Request a key verification from another user.
|
||||||
*
|
*
|
||||||
@@ -900,8 +884,8 @@ MatrixClient.prototype.acceptVerificationDM = function(event, method) {
|
|||||||
* @param {Array} devices array of device IDs to send requests to. Defaults to
|
* @param {Array} devices array of device IDs to send requests to. Defaults to
|
||||||
* all devices owned by the user
|
* all devices owned by the user
|
||||||
*
|
*
|
||||||
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
|
* @returns {Promise<module:crypto/verification/request/VerificationRequest>} resolves to a VerificationRequest
|
||||||
* when the request is accepted by the other user
|
* when the request has been sent to the other party.
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.requestVerification = function(userId, methods, devices) {
|
MatrixClient.prototype.requestVerification = function(userId, methods, devices) {
|
||||||
if (this._crypto === null) {
|
if (this._crypto === null) {
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ import {SAS} from './verification/SAS';
|
|||||||
import {keyFromPassphrase} from './key_passphrase';
|
import {keyFromPassphrase} from './key_passphrase';
|
||||||
import {encodeRecoveryKey} from './recoverykey';
|
import {encodeRecoveryKey} from './recoverykey';
|
||||||
import {VerificationRequest} from "./verification/request/VerificationRequest";
|
import {VerificationRequest} from "./verification/request/VerificationRequest";
|
||||||
import {InRoomChannel} from "./verification/request/InRoomChannel";
|
import {InRoomChannel, InRoomRequests} from "./verification/request/InRoomChannel";
|
||||||
import {ToDeviceChannel} from "./verification/request/ToDeviceChannel";
|
import {ToDeviceChannel, ToDeviceRequests} from "./verification/request/ToDeviceChannel";
|
||||||
import * as httpApi from "../http-api";
|
import * as httpApi from "../http-api";
|
||||||
|
|
||||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||||
@@ -204,8 +204,8 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
|
|||||||
// }
|
// }
|
||||||
this._lastNewSessionForced = {};
|
this._lastNewSessionForced = {};
|
||||||
|
|
||||||
this._toDeviceVerificationRequests = new Map();
|
this._toDeviceVerificationRequests = new ToDeviceRequests();
|
||||||
this._inRoomVerificationRequests = new Map();
|
this._inRoomVerificationRequests = new InRoomRequests();
|
||||||
|
|
||||||
const cryptoCallbacks = this._baseApis._cryptoCallbacks || {};
|
const cryptoCallbacks = this._baseApis._cryptoCallbacks || {};
|
||||||
|
|
||||||
@@ -1147,17 +1147,13 @@ Crypto.prototype.registerEventHandlers = function(eventEmitter) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
eventEmitter.on("toDeviceEvent", function(event) {
|
eventEmitter.on("toDeviceEvent", crypto._onToDeviceEvent.bind(crypto));
|
||||||
crypto._onToDeviceEvent(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventEmitter.on("Room.timeline", function(event) {
|
const timelineHandler = crypto._onTimelineEvent.bind(crypto);
|
||||||
crypto._onTimelineEvent(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventEmitter.on("Event.decrypted", function(event) {
|
eventEmitter.on("Room.timeline", timelineHandler);
|
||||||
crypto._onTimelineEvent(event);
|
|
||||||
});
|
eventEmitter.on("Event.decrypted", timelineHandler);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -1557,98 +1553,79 @@ Crypto.prototype.setDeviceVerification = async function(
|
|||||||
return deviceObj;
|
return deviceObj;
|
||||||
};
|
};
|
||||||
|
|
||||||
Crypto.prototype.requestVerificationDM = async function(userId, roomId, methods) {
|
Crypto.prototype.requestVerificationDM = function(userId, roomId, methods) {
|
||||||
const channel = new InRoomChannel(this._baseApis, roomId, userId);
|
const channel = new InRoomChannel(this._baseApis, roomId, userId);
|
||||||
const request = await this._requestVerificationWithChannel(
|
return this._requestVerificationWithChannel(
|
||||||
userId,
|
userId,
|
||||||
methods,
|
methods,
|
||||||
channel,
|
channel,
|
||||||
this._inRoomVerificationRequests,
|
this._inRoomVerificationRequests,
|
||||||
);
|
);
|
||||||
return await request.waitForVerifier();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Crypto.prototype.acceptVerificationDM = function(event, method) {
|
Crypto.prototype.requestVerification = function(userId, methods, devices) {
|
||||||
if(!InRoomChannel.validateEvent(event, this._baseApis)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sender = event.getSender();
|
|
||||||
const requestsByTxnId = this._inRoomVerificationRequests.get(sender);
|
|
||||||
if (!requestsByTxnId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const transactionId = InRoomChannel.getTransactionId(event);
|
|
||||||
const request = requestsByTxnId.get(transactionId);
|
|
||||||
if (!request) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return request.beginKeyVerification(method);
|
|
||||||
};
|
|
||||||
|
|
||||||
Crypto.prototype.requestVerification = async function(userId, methods, devices) {
|
|
||||||
if (!devices) {
|
if (!devices) {
|
||||||
devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(userId));
|
devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(userId));
|
||||||
}
|
}
|
||||||
const channel = new ToDeviceChannel(this._baseApis, userId, devices);
|
const channel = new ToDeviceChannel(this._baseApis, userId, devices);
|
||||||
const request = await this._requestVerificationWithChannel(
|
return this._requestVerificationWithChannel(
|
||||||
userId,
|
userId,
|
||||||
methods,
|
methods,
|
||||||
channel,
|
channel,
|
||||||
this._toDeviceVerificationRequests,
|
this._toDeviceVerificationRequests,
|
||||||
);
|
);
|
||||||
return await request.waitForVerifier();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Crypto.prototype._requestVerificationWithChannel = async function(
|
Crypto.prototype._requestVerificationWithChannel = async function(
|
||||||
userId, methods, channel, requestsMap,
|
userId, methods, channel, requestsMap,
|
||||||
) {
|
) {
|
||||||
if (!methods) {
|
let verificationMethods = this._verificationMethods;
|
||||||
// .keys() returns an iterator, so we need to explicitly turn it into an array
|
if (methods) {
|
||||||
methods = [...this._verificationMethods.keys()];
|
verificationMethods = methods.reduce((map, name) => {
|
||||||
|
const method = this._verificationMethods.get(name);
|
||||||
|
if (!method) {
|
||||||
|
throw new Error(`Verification method ${name} is not supported.`);
|
||||||
|
} else {
|
||||||
|
map.set(name, method);
|
||||||
}
|
}
|
||||||
// TODO: filter by given methods
|
return map;
|
||||||
const request = new VerificationRequest(
|
}, new Map());
|
||||||
channel, this._verificationMethods, userId, this._baseApis);
|
}
|
||||||
|
let request = new VerificationRequest(
|
||||||
|
channel, verificationMethods, this._baseApis);
|
||||||
await request.sendRequest();
|
await request.sendRequest();
|
||||||
|
// don't replace the request created by a racing remote echo
|
||||||
let requestsByTxnId = requestsMap.get(userId);
|
const racingRequest = requestsMap.getRequestByChannel(channel);
|
||||||
if (!requestsByTxnId) {
|
if (racingRequest) {
|
||||||
requestsByTxnId = new Map();
|
request = racingRequest;
|
||||||
requestsMap.set(userId, requestsByTxnId);
|
} else {
|
||||||
|
logger.log(`Crypto: adding new request to ` +
|
||||||
|
`requestsByTxnId with id ${channel.transactionId} ${channel.roomId}`);
|
||||||
|
requestsMap.setRequestByChannel(channel, request);
|
||||||
}
|
}
|
||||||
// TODO: we're only adding the request to the map once it has been sent
|
|
||||||
// but if the other party is really fast they could potentially respond to the
|
|
||||||
// request before the server tells us the event got sent, and we would probably
|
|
||||||
// create a new request object
|
|
||||||
requestsByTxnId.set(channel.transactionId, request);
|
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
};
|
};
|
||||||
|
|
||||||
Crypto.prototype.beginKeyVerification = function(
|
Crypto.prototype.beginKeyVerification = function(
|
||||||
method, userId, deviceId, transactionId = null,
|
method, userId, deviceId, transactionId = null,
|
||||||
) {
|
) {
|
||||||
let requestsByTxnId = this._toDeviceVerificationRequests.get(userId);
|
|
||||||
if (!requestsByTxnId) {
|
|
||||||
requestsByTxnId = new Map();
|
|
||||||
this._toDeviceVerificationRequests.set(userId, requestsByTxnId);
|
|
||||||
}
|
|
||||||
let request;
|
let request;
|
||||||
if (transactionId) {
|
if (transactionId) {
|
||||||
request = requestsByTxnId.get(transactionId);
|
request = this._toDeviceVerificationRequests.getRequestBySenderAndTxnId(
|
||||||
|
userId, transactionId);
|
||||||
|
if (!request) {
|
||||||
|
throw new Error(
|
||||||
|
`No request found for user ${userId} with ` +
|
||||||
|
`transactionId ${transactionId}`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
transactionId = ToDeviceChannel.makeTransactionId();
|
transactionId = ToDeviceChannel.makeTransactionId();
|
||||||
const channel = new ToDeviceChannel(
|
const channel = new ToDeviceChannel(
|
||||||
this._baseApis, userId, [deviceId], transactionId, deviceId);
|
this._baseApis, userId, [deviceId], transactionId, deviceId);
|
||||||
request = new VerificationRequest(
|
request = new VerificationRequest(
|
||||||
channel, this._verificationMethods, userId, this._baseApis);
|
channel, this._verificationMethods, this._baseApis);
|
||||||
requestsByTxnId.set(transactionId, request);
|
this._toDeviceVerificationRequests.setRequestBySenderAndTxnId(
|
||||||
}
|
userId, transactionId, request);
|
||||||
if (!request) {
|
|
||||||
throw new Error(
|
|
||||||
`No request found for user ${userId} with transactionId ${transactionId}`);
|
|
||||||
}
|
}
|
||||||
return request.beginKeyVerification(method, {userId, deviceId});
|
return request.beginKeyVerification(method, {userId, deviceId});
|
||||||
};
|
};
|
||||||
@@ -2534,7 +2511,6 @@ Crypto.prototype._onKeyVerificationMessage = function(event) {
|
|||||||
if (!ToDeviceChannel.validateEvent(event, this._baseApis)) {
|
if (!ToDeviceChannel.validateEvent(event, this._baseApis)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const transactionId = ToDeviceChannel.getTransactionId(event);
|
|
||||||
const createRequest = event => {
|
const createRequest = event => {
|
||||||
if (!ToDeviceChannel.canCreateRequest(ToDeviceChannel.getEventType(event))) {
|
if (!ToDeviceChannel.canCreateRequest(ToDeviceChannel.getEventType(event))) {
|
||||||
return;
|
return;
|
||||||
@@ -2551,10 +2527,13 @@ Crypto.prototype._onKeyVerificationMessage = function(event) {
|
|||||||
[deviceId],
|
[deviceId],
|
||||||
);
|
);
|
||||||
return new VerificationRequest(
|
return new VerificationRequest(
|
||||||
channel, this._verificationMethods, userId, this._baseApis);
|
channel, this._verificationMethods, this._baseApis);
|
||||||
};
|
};
|
||||||
this._handleVerificationEvent(event, transactionId,
|
this._handleVerificationEvent(
|
||||||
this._toDeviceVerificationRequests, createRequest);
|
event,
|
||||||
|
this._toDeviceVerificationRequests,
|
||||||
|
createRequest,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2562,65 +2541,65 @@ Crypto.prototype._onKeyVerificationMessage = function(event) {
|
|||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @param {module:models/event.MatrixEvent} event the timeline event
|
* @param {module:models/event.MatrixEvent} event the timeline event
|
||||||
|
* @param {module:models/Room} room not used
|
||||||
|
* @param {bool} atStart not used
|
||||||
|
* @param {bool} removed not used
|
||||||
|
* @param {bool} data.liveEvent whether this is a live event
|
||||||
*/
|
*/
|
||||||
Crypto.prototype._onTimelineEvent = function(event) {
|
Crypto.prototype._onTimelineEvent = function(
|
||||||
|
event, room, atStart, removed, {liveEvent} = {},
|
||||||
|
) {
|
||||||
if (!InRoomChannel.validateEvent(event, this._baseApis)) {
|
if (!InRoomChannel.validateEvent(event, this._baseApis)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const transactionId = InRoomChannel.getTransactionId(event);
|
|
||||||
const createRequest = event => {
|
const createRequest = event => {
|
||||||
if (!InRoomChannel.canCreateRequest(InRoomChannel.getEventType(event))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const userId = event.getSender();
|
|
||||||
const channel = new InRoomChannel(
|
const channel = new InRoomChannel(
|
||||||
this._baseApis,
|
this._baseApis,
|
||||||
event.getRoomId(),
|
event.getRoomId(),
|
||||||
userId,
|
|
||||||
);
|
);
|
||||||
return new VerificationRequest(
|
return new VerificationRequest(
|
||||||
channel, this._verificationMethods, userId, this._baseApis);
|
channel, this._verificationMethods, this._baseApis);
|
||||||
};
|
};
|
||||||
this._handleVerificationEvent(event, transactionId,
|
this._handleVerificationEvent(
|
||||||
this._inRoomVerificationRequests, createRequest);
|
event,
|
||||||
|
this._inRoomVerificationRequests,
|
||||||
|
createRequest,
|
||||||
|
liveEvent,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Crypto.prototype._handleVerificationEvent = async function(
|
Crypto.prototype._handleVerificationEvent = async function(
|
||||||
event, transactionId, requestsMap, createRequest,
|
event, requestsMap, createRequest, isLiveEvent = true,
|
||||||
) {
|
) {
|
||||||
const sender = event.getSender();
|
let request = requestsMap.getRequest(event);
|
||||||
let requestsByTxnId = requestsMap.get(sender);
|
|
||||||
let isNewRequest = false;
|
let isNewRequest = false;
|
||||||
let request = requestsByTxnId && requestsByTxnId.get(transactionId);
|
|
||||||
if (!request) {
|
if (!request) {
|
||||||
request = createRequest(event);
|
request = createRequest(event);
|
||||||
// a request could not be made from this event, so ignore event
|
// a request could not be made from this event, so ignore event
|
||||||
if (!request) {
|
if (!request) {
|
||||||
|
logger.log(`Crypto: could not find VerificationRequest for ` +
|
||||||
|
`${event.getType()}, and could not create one, so ignoring.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isNewRequest = true;
|
isNewRequest = true;
|
||||||
if (!requestsByTxnId) {
|
requestsMap.setRequest(event, request);
|
||||||
requestsByTxnId = new Map();
|
|
||||||
requestsMap.set(sender, requestsByTxnId);
|
|
||||||
}
|
|
||||||
requestsByTxnId.set(transactionId, request);
|
|
||||||
}
|
}
|
||||||
|
event.setVerificationRequest(request);
|
||||||
try {
|
try {
|
||||||
const hadVerifier = !!request.verifier;
|
const hadVerifier = !!request.verifier;
|
||||||
await request.channel.handleEvent(event, request);
|
await request.channel.handleEvent(event, request, isLiveEvent);
|
||||||
// emit start event when verifier got set
|
// emit start event when verifier got set
|
||||||
if (!hadVerifier && request.verifier) {
|
if (!hadVerifier && request.verifier) {
|
||||||
this._baseApis.emit("crypto.verification.start", request.verifier);
|
this._baseApis.emit("crypto.verification.start", request.verifier);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("error while handling verification event", event, err);
|
logger.error("error while handling verification event: " + err.message);
|
||||||
}
|
}
|
||||||
if (!request.pending) {
|
const shouldEmit = isNewRequest &&
|
||||||
requestsByTxnId.delete(transactionId);
|
!request.initiatedByMe &&
|
||||||
if (requestsByTxnId.size === 0) {
|
!request.invalid && // check it has enough events to pass the UNSENT stage
|
||||||
requestsMap.delete(sender);
|
!request.observeOnly;
|
||||||
}
|
if (shouldEmit) {
|
||||||
} else if (isNewRequest && !request.initiatedByMe) {
|
|
||||||
this._baseApis.emit("crypto.verification.request", request);
|
this._baseApis.emit("crypto.verification.request", request);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,9 +67,6 @@ export class VerificationBase extends EventEmitter {
|
|||||||
this._done = false;
|
this._done = false;
|
||||||
this._promise = null;
|
this._promise = null;
|
||||||
this._transactionTimeoutTimer = null;
|
this._transactionTimeoutTimer = null;
|
||||||
|
|
||||||
// At this point, the verification request was received so start the timeout timer.
|
|
||||||
this._resetTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_resetTimer() {
|
_resetTimer() {
|
||||||
@@ -122,8 +119,15 @@ export class VerificationBase extends EventEmitter {
|
|||||||
} else if (e.getType() === "m.key.verification.cancel") {
|
} else if (e.getType() === "m.key.verification.cancel") {
|
||||||
const reject = this._reject;
|
const reject = this._reject;
|
||||||
this._reject = undefined;
|
this._reject = undefined;
|
||||||
reject(new Error("Other side cancelled verification"));
|
const content = e.getContent();
|
||||||
} else {
|
const {reason, code} = content;
|
||||||
|
reject(new Error(`Other side cancelled verification ` +
|
||||||
|
`because ${reason} (${code})`));
|
||||||
|
} else if (this._expectedEvent) {
|
||||||
|
// only cancel if there is an event expected.
|
||||||
|
// if there is no event expected, it means verify() wasn't called
|
||||||
|
// and we're just replaying the timeline events when syncing
|
||||||
|
// after a refresh when the events haven't been stored in the cache yet.
|
||||||
const exception = new Error(
|
const exception = new Error(
|
||||||
"Unexpected message: expecting " + this._expectedEvent
|
"Unexpected message: expecting " + this._expectedEvent
|
||||||
+ " but got " + e.getType(),
|
+ " but got " + e.getType(),
|
||||||
|
|||||||
@@ -23,12 +23,10 @@ limitations under the License.
|
|||||||
import {MatrixEvent} from "../../models/event";
|
import {MatrixEvent} from "../../models/event";
|
||||||
|
|
||||||
export function newVerificationError(code, reason, extradata) {
|
export function newVerificationError(code, reason, extradata) {
|
||||||
extradata = extradata || {};
|
const content = Object.assign({}, {code, reason}, extradata);
|
||||||
extradata.code = code;
|
|
||||||
extradata.reason = reason;
|
|
||||||
return new MatrixEvent({
|
return new MatrixEvent({
|
||||||
type: "m.key.verification.cancel",
|
type: "m.key.verification.cancel",
|
||||||
content: extradata,
|
content,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,13 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {REQUEST_TYPE, START_TYPE, VerificationRequest} from "./VerificationRequest";
|
import {
|
||||||
|
VerificationRequest,
|
||||||
|
REQUEST_TYPE,
|
||||||
|
READY_TYPE,
|
||||||
|
START_TYPE,
|
||||||
|
} from "./VerificationRequest";
|
||||||
|
import {logger} from '../../../logger';
|
||||||
|
|
||||||
const MESSAGE_TYPE = "m.room.message";
|
const MESSAGE_TYPE = "m.room.message";
|
||||||
const M_REFERENCE = "m.reference";
|
const M_REFERENCE = "m.reference";
|
||||||
@@ -31,10 +37,10 @@ export class InRoomChannel {
|
|||||||
* @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user.
|
* @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user.
|
||||||
* @param {string} userId id of user that the verification request is directed at, should be present in the room.
|
* @param {string} userId id of user that the verification request is directed at, should be present in the room.
|
||||||
*/
|
*/
|
||||||
constructor(client, roomId, userId) {
|
constructor(client, roomId, userId = null) {
|
||||||
this._client = client;
|
this._client = client;
|
||||||
this._roomId = roomId;
|
this._roomId = roomId;
|
||||||
this._userId = userId;
|
this.userId = userId;
|
||||||
this._requestEventId = null;
|
this._requestEventId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,16 +49,45 @@ export class InRoomChannel {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get receiveStartFromOtherDevices() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get roomId() {
|
||||||
|
return this._roomId;
|
||||||
|
}
|
||||||
|
|
||||||
/** The transaction id generated/used by this verification channel */
|
/** The transaction id generated/used by this verification channel */
|
||||||
get transactionId() {
|
get transactionId() {
|
||||||
return this._requestEventId;
|
return this._requestEventId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getOtherPartyUserId(event, client) {
|
||||||
|
const type = InRoomChannel.getEventType(event);
|
||||||
|
if (type !== REQUEST_TYPE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ownUserId = client.getUserId();
|
||||||
|
const sender = event.getSender();
|
||||||
|
const content = event.getContent();
|
||||||
|
const receiver = content.to;
|
||||||
|
|
||||||
|
// request is not sent by or directed at us
|
||||||
|
if (sender !== ownUserId && receiver !== ownUserId) {
|
||||||
|
return sender;
|
||||||
|
}
|
||||||
|
if (sender === ownUserId) {
|
||||||
|
return receiver;
|
||||||
|
} else {
|
||||||
|
return sender;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {MatrixEvent} event the event to get the timestamp of
|
* @param {MatrixEvent} event the event to get the timestamp of
|
||||||
* @return {number} the timestamp when the event was sent
|
* @return {number} the timestamp when the event was sent
|
||||||
*/
|
*/
|
||||||
static getTimestamp(event) {
|
getTimestamp(event) {
|
||||||
return event.getTs();
|
return event.getTs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,23 +128,28 @@ export class InRoomChannel {
|
|||||||
static validateEvent(event, client) {
|
static validateEvent(event, client) {
|
||||||
const txnId = InRoomChannel.getTransactionId(event);
|
const txnId = InRoomChannel.getTransactionId(event);
|
||||||
if (typeof txnId !== "string" || txnId.length === 0) {
|
if (typeof txnId !== "string" || txnId.length === 0) {
|
||||||
|
logger.log("InRoomChannel: validateEvent: no valid txnId " + txnId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const type = InRoomChannel.getEventType(event);
|
const type = InRoomChannel.getEventType(event);
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
if (type === REQUEST_TYPE) {
|
if (type === REQUEST_TYPE) {
|
||||||
if (typeof content.to !== "string" || !content.to.length) {
|
if (!content || typeof content.to !== "string" || !content.to.length) {
|
||||||
|
logger.log("InRoomChannel: validateEvent: " +
|
||||||
|
"no valid to " + (content && content.to));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const ownUserId = client.getUserId();
|
|
||||||
// ignore requests that are not direct to or sent by the syncing user
|
// ignore requests that are not direct to or sent by the syncing user
|
||||||
if (event.getSender() !== ownUserId && content.to !== ownUserId) {
|
if (!InRoomChannel.getOtherPartyUserId(event, client)) {
|
||||||
|
logger.log("InRoomChannel: validateEvent: " +
|
||||||
|
`not directed to or sent by me: ${event.getSender()}` +
|
||||||
|
`, ${content && content.to}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return VerificationRequest.validateEvent(
|
return VerificationRequest.validateEvent(type, event, client);
|
||||||
type, event, InRoomChannel.getTimestamp(event), client);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,21 +177,41 @@ export class InRoomChannel {
|
|||||||
* Changes the state of the channel, request, and verifier in response to a key verification event.
|
* Changes the state of the channel, request, and verifier in response to a key verification event.
|
||||||
* @param {MatrixEvent} event to handle
|
* @param {MatrixEvent} event to handle
|
||||||
* @param {VerificationRequest} request the request to forward handling to
|
* @param {VerificationRequest} request the request to forward handling to
|
||||||
|
* @param {bool} isLiveEvent whether this is an even received through sync or not
|
||||||
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
||||||
*/
|
*/
|
||||||
async handleEvent(event, request) {
|
async handleEvent(event, request, isLiveEvent) {
|
||||||
const type = InRoomChannel.getEventType(event);
|
const type = InRoomChannel.getEventType(event);
|
||||||
// do validations that need state (roomId, userId),
|
// do validations that need state (roomId, userId),
|
||||||
// ignore if invalid
|
// ignore if invalid
|
||||||
if (event.getRoomId() !== this._roomId || event.getSender() !== this._userId) {
|
|
||||||
|
if (event.getRoomId() !== this._roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// set transactionId when receiving a .request
|
// set userId if not set already
|
||||||
if (!this._requestEventId && type === REQUEST_TYPE) {
|
if (this.userId === null) {
|
||||||
this._requestEventId = event.getId();
|
const userId = InRoomChannel.getOtherPartyUserId(event, this._client);
|
||||||
|
if (userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ignore events not sent by us or the other party
|
||||||
|
const ownUserId = this._client.getUserId();
|
||||||
|
const sender = event.getSender();
|
||||||
|
if (this.userId !== null) {
|
||||||
|
if (sender !== ownUserId && sender !== this.userId) {
|
||||||
|
logger.log(`InRoomChannel: ignoring verification event from ` +
|
||||||
|
`non-participating sender ${sender}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this._requestEventId === null) {
|
||||||
|
this._requestEventId = InRoomChannel.getTransactionId(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await request.handleEvent(type, event, InRoomChannel.getTimestamp(event));
|
const isRemoteEcho = !!event.getUnsigned().transaction_id;
|
||||||
|
|
||||||
|
return await request.handleEvent(type, event, isLiveEvent, isRemoteEcho);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -180,7 +240,7 @@ export class InRoomChannel {
|
|||||||
*/
|
*/
|
||||||
completeContent(type, content) {
|
completeContent(type, content) {
|
||||||
content = Object.assign({}, content);
|
content = Object.assign({}, content);
|
||||||
if (type === REQUEST_TYPE || type === START_TYPE) {
|
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
|
||||||
content.from_device = this._client.getDeviceId();
|
content.from_device = this._client.getDeviceId();
|
||||||
}
|
}
|
||||||
if (type === REQUEST_TYPE) {
|
if (type === REQUEST_TYPE) {
|
||||||
@@ -191,7 +251,7 @@ export class InRoomChannel {
|
|||||||
"verification. You will need to use legacy key " +
|
"verification. You will need to use legacy key " +
|
||||||
"verification to verify keys.",
|
"verification to verify keys.",
|
||||||
msgtype: REQUEST_TYPE,
|
msgtype: REQUEST_TYPE,
|
||||||
to: this._userId,
|
to: this.userId,
|
||||||
from_device: content.from_device,
|
from_device: content.from_device,
|
||||||
methods: content.methods,
|
methods: content.methods,
|
||||||
};
|
};
|
||||||
@@ -232,3 +292,59 @@ export class InRoomChannel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class InRoomRequests {
|
||||||
|
constructor() {
|
||||||
|
this._requestsByRoomId = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequest(event) {
|
||||||
|
const roomId = event.getRoomId();
|
||||||
|
const txnId = InRoomChannel.getTransactionId(event);
|
||||||
|
// console.log(`looking for request in room ${roomId} with txnId ${txnId} for an ${event.getType()} from ${event.getSender()}...`);
|
||||||
|
return this._getRequestByTxnId(roomId, txnId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequestByChannel(channel) {
|
||||||
|
return this._getRequestByTxnId(channel.roomId, channel.transactionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getRequestByTxnId(roomId, txnId) {
|
||||||
|
const requestsByTxnId = this._requestsByRoomId.get(roomId);
|
||||||
|
if (requestsByTxnId) {
|
||||||
|
return requestsByTxnId.get(txnId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequest(event, request) {
|
||||||
|
this._setRequest(
|
||||||
|
event.getRoomId(),
|
||||||
|
InRoomChannel.getTransactionId(event),
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequestByChannel(channel, request) {
|
||||||
|
this._setRequest(channel.roomId, channel.transactionId, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
_setRequest(roomId, txnId, request) {
|
||||||
|
let requestsByTxnId = this._requestsByRoomId.get(roomId);
|
||||||
|
if (!requestsByTxnId) {
|
||||||
|
requestsByTxnId = new Map();
|
||||||
|
this._requestsByRoomId.set(roomId, requestsByTxnId);
|
||||||
|
}
|
||||||
|
requestsByTxnId.set(txnId, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRequest(event) {
|
||||||
|
const roomId = event.getRoomId();
|
||||||
|
const requestsByTxnId = this._requestsByRoomId.get(roomId);
|
||||||
|
if (requestsByTxnId) {
|
||||||
|
requestsByTxnId.delete(InRoomChannel.getTransactionId(event));
|
||||||
|
if (requestsByTxnId.size === 0) {
|
||||||
|
this._requestsByRoomId.delete(roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** a key verification channel that wraps over an actual channel to pass it to a verifier,
|
|
||||||
* to notify the VerificationRequest when the verifier tries to send anything over the channel.
|
|
||||||
* This way, the VerificationRequest can update its state based on events sent by the verifier.
|
|
||||||
* Anything that is not sending is just routing through to the wrapped channel.
|
|
||||||
*/
|
|
||||||
export class RequestCallbackChannel {
|
|
||||||
constructor(request, channel) {
|
|
||||||
this._request = request;
|
|
||||||
this._channel = channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
get transactionId() {
|
|
||||||
return this._channel.transactionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
get needsDoneMessage() {
|
|
||||||
return this._channel.needsDoneMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEvent(event, request) {
|
|
||||||
return this._channel.handleEvent(event, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
completedContentFromEvent(event) {
|
|
||||||
return this._channel.completedContentFromEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
completeContent(type, content) {
|
|
||||||
return this._channel.completeContent(type, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
async send(type, uncompletedContent) {
|
|
||||||
this._request.handleVerifierSend(type, uncompletedContent);
|
|
||||||
const result = await this._channel.send(type, uncompletedContent);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendCompleted(type, content) {
|
|
||||||
this._request.handleVerifierSend(type, content);
|
|
||||||
const result = await this._channel.sendCompleted(type, content);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,11 +20,14 @@ import {logger} from '../../../logger';
|
|||||||
import {
|
import {
|
||||||
CANCEL_TYPE,
|
CANCEL_TYPE,
|
||||||
PHASE_STARTED,
|
PHASE_STARTED,
|
||||||
|
PHASE_READY,
|
||||||
REQUEST_TYPE,
|
REQUEST_TYPE,
|
||||||
|
READY_TYPE,
|
||||||
START_TYPE,
|
START_TYPE,
|
||||||
VerificationRequest,
|
VerificationRequest,
|
||||||
} from "./VerificationRequest";
|
} from "./VerificationRequest";
|
||||||
import {errorFromEvent, newUnexpectedMessageError} from "../Error";
|
import {errorFromEvent, newUnexpectedMessageError} from "../Error";
|
||||||
|
import {MatrixEvent} from "../../../models/event";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A key verification channel that sends verification events over to_device messages.
|
* A key verification channel that sends verification events over to_device messages.
|
||||||
@@ -34,7 +37,7 @@ export class ToDeviceChannel {
|
|||||||
// userId and devices of user we're about to verify
|
// userId and devices of user we're about to verify
|
||||||
constructor(client, userId, devices, transactionId = null, deviceId = null) {
|
constructor(client, userId, devices, transactionId = null, deviceId = null) {
|
||||||
this._client = client;
|
this._client = client;
|
||||||
this._userId = userId;
|
this.userId = userId;
|
||||||
this._devices = devices;
|
this._devices = devices;
|
||||||
this.transactionId = transactionId;
|
this.transactionId = transactionId;
|
||||||
this._deviceId = deviceId;
|
this._deviceId = deviceId;
|
||||||
@@ -80,10 +83,12 @@ export class ToDeviceChannel {
|
|||||||
}
|
}
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
if (!content) {
|
if (!content) {
|
||||||
|
logger.warn("ToDeviceChannel.validateEvent: invalid: no content");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content.transaction_id) {
|
if (!content.transaction_id) {
|
||||||
|
logger.warn("ToDeviceChannel.validateEvent: invalid: no transaction_id");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +96,7 @@ export class ToDeviceChannel {
|
|||||||
|
|
||||||
if (type === REQUEST_TYPE) {
|
if (type === REQUEST_TYPE) {
|
||||||
if (!Number.isFinite(content.timestamp)) {
|
if (!Number.isFinite(content.timestamp)) {
|
||||||
|
logger.warn("ToDeviceChannel.validateEvent: invalid: no timestamp");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (event.getSender() === client.getUserId() &&
|
if (event.getSender() === client.getUserId() &&
|
||||||
@@ -98,19 +104,19 @@ export class ToDeviceChannel {
|
|||||||
) {
|
) {
|
||||||
// ignore requests from ourselves, because it doesn't make sense for a
|
// ignore requests from ourselves, because it doesn't make sense for a
|
||||||
// device to verify itself
|
// device to verify itself
|
||||||
|
logger.warn("ToDeviceChannel.validateEvent: invalid: from own device");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return VerificationRequest.validateEvent(
|
return VerificationRequest.validateEvent(type, event, client);
|
||||||
type, event, ToDeviceChannel.getTimestamp(event), client);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {MatrixEvent} event the event to get the timestamp of
|
* @param {MatrixEvent} event the event to get the timestamp of
|
||||||
* @return {number} the timestamp when the event was sent
|
* @return {number} the timestamp when the event was sent
|
||||||
*/
|
*/
|
||||||
static getTimestamp(event) {
|
getTimestamp(event) {
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
return content && content.timestamp;
|
return content && content.timestamp;
|
||||||
}
|
}
|
||||||
@@ -119,9 +125,10 @@ export class ToDeviceChannel {
|
|||||||
* Changes the state of the channel, request, and verifier in response to a key verification event.
|
* Changes the state of the channel, request, and verifier in response to a key verification event.
|
||||||
* @param {MatrixEvent} event to handle
|
* @param {MatrixEvent} event to handle
|
||||||
* @param {VerificationRequest} request the request to forward handling to
|
* @param {VerificationRequest} request the request to forward handling to
|
||||||
|
* @param {bool} isLiveEvent whether this is an even received through sync or not
|
||||||
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
||||||
*/
|
*/
|
||||||
async handleEvent(event, request) {
|
async handleEvent(event, request, isLiveEvent) {
|
||||||
const type = event.getType();
|
const type = event.getType();
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
if (type === REQUEST_TYPE || type === START_TYPE) {
|
if (type === REQUEST_TYPE || type === START_TYPE) {
|
||||||
@@ -143,14 +150,17 @@ export class ToDeviceChannel {
|
|||||||
return this._sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
|
return this._sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const wasStarted = request.phase === PHASE_STARTED ||
|
||||||
|
request.phase === PHASE_READY;
|
||||||
|
|
||||||
const wasStarted = request.phase === PHASE_STARTED;
|
await request.handleEvent(event.getType(), event, isLiveEvent, false);
|
||||||
await request.handleEvent(
|
|
||||||
event.getType(), event, ToDeviceChannel.getTimestamp(event));
|
|
||||||
const isStarted = request.phase === PHASE_STARTED;
|
|
||||||
|
|
||||||
// the request has picked a start event, tell the other devices about it
|
const isStarted = request.phase === PHASE_STARTED ||
|
||||||
if (type === START_TYPE && !wasStarted && isStarted && this._deviceId) {
|
request.phase === PHASE_READY;
|
||||||
|
|
||||||
|
const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
|
||||||
|
// the request has picked a ready or start event, tell the other devices about it
|
||||||
|
if (isAcceptingEvent && !wasStarted && isStarted && this._deviceId) {
|
||||||
const nonChosenDevices = this._devices.filter(d => d !== this._deviceId);
|
const nonChosenDevices = this._devices.filter(d => d !== this._deviceId);
|
||||||
if (nonChosenDevices.length) {
|
if (nonChosenDevices.length) {
|
||||||
const message = this.completeContent({
|
const message = this.completeContent({
|
||||||
@@ -186,7 +196,7 @@ export class ToDeviceChannel {
|
|||||||
if (this.transactionId) {
|
if (this.transactionId) {
|
||||||
content.transaction_id = this.transactionId;
|
content.transaction_id = this.transactionId;
|
||||||
}
|
}
|
||||||
if (type === REQUEST_TYPE || type === START_TYPE) {
|
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
|
||||||
content.from_device = this._client.getDeviceId();
|
content.from_device = this._client.getDeviceId();
|
||||||
}
|
}
|
||||||
if (type === REQUEST_TYPE) {
|
if (type === REQUEST_TYPE) {
|
||||||
@@ -216,12 +226,27 @@ export class ToDeviceChannel {
|
|||||||
* @param {object} content
|
* @param {object} content
|
||||||
* @returns {Promise} the promise of the request
|
* @returns {Promise} the promise of the request
|
||||||
*/
|
*/
|
||||||
sendCompleted(type, content) {
|
async sendCompleted(type, content) {
|
||||||
|
let result;
|
||||||
if (type === REQUEST_TYPE) {
|
if (type === REQUEST_TYPE) {
|
||||||
return this._sendToDevices(type, content, this._devices);
|
result = await this._sendToDevices(type, content, this._devices);
|
||||||
} else {
|
} else {
|
||||||
return this._sendToDevices(type, content, [this._deviceId]);
|
result = await this._sendToDevices(type, content, [this._deviceId]);
|
||||||
}
|
}
|
||||||
|
// the VerificationRequest state machine requires remote echos of the event
|
||||||
|
// the client sends itself, so we fake this for to_device messages
|
||||||
|
const remoteEchoEvent = new MatrixEvent({
|
||||||
|
sender: this._client.getUserId(),
|
||||||
|
content,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
await this._request.handleEvent(
|
||||||
|
type,
|
||||||
|
remoteEchoEvent,
|
||||||
|
/*isLiveEvent=*/true,
|
||||||
|
/*isRemoteEcho=*/true,
|
||||||
|
);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendToDevices(type, content, devices) {
|
_sendToDevices(type, content, devices) {
|
||||||
@@ -231,7 +256,7 @@ export class ToDeviceChannel {
|
|||||||
msgMap[deviceId] = content;
|
msgMap[deviceId] = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._client.sendToDevice(type, {[this._userId]: msgMap});
|
return this._client.sendToDevice(type, {[this.userId]: msgMap});
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@@ -245,3 +270,60 @@ export class ToDeviceChannel {
|
|||||||
return randomString(32);
|
return randomString(32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class ToDeviceRequests {
|
||||||
|
constructor() {
|
||||||
|
this._requestsByUserId = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequest(event) {
|
||||||
|
return this.getRequestBySenderAndTxnId(
|
||||||
|
event.getSender(),
|
||||||
|
ToDeviceChannel.getTransactionId(event),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequestByChannel(channel) {
|
||||||
|
return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequestBySenderAndTxnId(sender, txnId) {
|
||||||
|
const requestsByTxnId = this._requestsByUserId.get(sender);
|
||||||
|
if (requestsByTxnId) {
|
||||||
|
return requestsByTxnId.get(txnId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequest(event, request) {
|
||||||
|
this.setRequestBySenderAndTxnId(
|
||||||
|
event.getSender(),
|
||||||
|
ToDeviceChannel.getTransactionId(event),
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequestByChannel(channel, request) {
|
||||||
|
this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequestBySenderAndTxnId(sender, txnId, request) {
|
||||||
|
let requestsByTxnId = this._requestsByUserId.get(sender);
|
||||||
|
if (!requestsByTxnId) {
|
||||||
|
requestsByTxnId = new Map();
|
||||||
|
this._requestsByUserId.set(sender, requestsByTxnId);
|
||||||
|
}
|
||||||
|
requestsByTxnId.set(txnId, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRequest(event) {
|
||||||
|
const userId = event.getSender();
|
||||||
|
const requestsByTxnId = this._requestsByUserId.get(userId);
|
||||||
|
if (requestsByTxnId) {
|
||||||
|
requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event));
|
||||||
|
if (requestsByTxnId.size === 0) {
|
||||||
|
this._requestsByUserId.delete(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {logger} from '../../../logger';
|
import {logger} from '../../../logger';
|
||||||
import {RequestCallbackChannel} from "./RequestCallbackChannel";
|
|
||||||
import {EventEmitter} from 'events';
|
import {EventEmitter} from 'events';
|
||||||
import {
|
import {
|
||||||
errorFactory,
|
errorFactory,
|
||||||
@@ -41,11 +40,11 @@ export const REQUEST_TYPE = EVENT_PREFIX + "request";
|
|||||||
export const START_TYPE = EVENT_PREFIX + "start";
|
export const START_TYPE = EVENT_PREFIX + "start";
|
||||||
export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
|
export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
|
||||||
export const DONE_TYPE = EVENT_PREFIX + "done";
|
export const DONE_TYPE = EVENT_PREFIX + "done";
|
||||||
// export const READY_TYPE = EVENT_PREFIX + "ready";
|
export const READY_TYPE = EVENT_PREFIX + "ready";
|
||||||
|
|
||||||
export const PHASE_UNSENT = 1;
|
export const PHASE_UNSENT = 1;
|
||||||
export const PHASE_REQUESTED = 2;
|
export const PHASE_REQUESTED = 2;
|
||||||
// const PHASE_READY = 3;
|
export const PHASE_READY = 3;
|
||||||
export const PHASE_STARTED = 4;
|
export const PHASE_STARTED = 4;
|
||||||
export const PHASE_CANCELLED = 5;
|
export const PHASE_CANCELLED = 5;
|
||||||
export const PHASE_DONE = 6;
|
export const PHASE_DONE = 6;
|
||||||
@@ -58,17 +57,18 @@ export const PHASE_DONE = 6;
|
|||||||
* @event "change" whenever the state of the request object has changed.
|
* @event "change" whenever the state of the request object has changed.
|
||||||
*/
|
*/
|
||||||
export class VerificationRequest extends EventEmitter {
|
export class VerificationRequest extends EventEmitter {
|
||||||
constructor(channel, verificationMethods, userId, client) {
|
constructor(channel, verificationMethods, client) {
|
||||||
super();
|
super();
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
|
this.channel._request = this;
|
||||||
this._verificationMethods = verificationMethods;
|
this._verificationMethods = verificationMethods;
|
||||||
this._client = client;
|
this._client = client;
|
||||||
this._commonMethods = [];
|
this._commonMethods = [];
|
||||||
this._setPhase(PHASE_UNSENT, false);
|
this._setPhase(PHASE_UNSENT, false);
|
||||||
this._requestEvent = null;
|
this._eventsByUs = new Map();
|
||||||
this._otherUserId = userId;
|
this._eventsByThem = new Map();
|
||||||
this._initiatedByMe = null;
|
this._observeOnly = false;
|
||||||
this._startTimestamp = null;
|
this._timeoutTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,37 +76,36 @@ export class VerificationRequest extends EventEmitter {
|
|||||||
* Invoked by the same static method in either channel.
|
* Invoked by the same static method in either channel.
|
||||||
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
|
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
|
||||||
* @param {MatrixEvent} event the event to validate. Don't call getType() on it but use the `type` parameter instead.
|
* @param {MatrixEvent} event the event to validate. Don't call getType() on it but use the `type` parameter instead.
|
||||||
* @param {number} timestamp the timestamp in milliseconds when this event was sent.
|
|
||||||
* @param {MatrixClient} client the client to get the current user and device id from
|
* @param {MatrixClient} client the client to get the current user and device id from
|
||||||
* @returns {bool} whether the event is valid and should be passed to handleEvent
|
* @returns {bool} whether the event is valid and should be passed to handleEvent
|
||||||
*/
|
*/
|
||||||
static validateEvent(type, event, timestamp, client) {
|
static validateEvent(type, event, client) {
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
logger.log("VerificationRequest: validateEvent: no content");
|
||||||
|
}
|
||||||
|
|
||||||
if (!type.startsWith(EVENT_PREFIX)) {
|
if (!type.startsWith(EVENT_PREFIX)) {
|
||||||
|
logger.log("VerificationRequest: validateEvent: " +
|
||||||
|
"fail because type doesnt start with " + EVENT_PREFIX);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === REQUEST_TYPE) {
|
if (type === REQUEST_TYPE || type === READY_TYPE) {
|
||||||
if (!Array.isArray(content.methods)) {
|
if (!Array.isArray(content.methods)) {
|
||||||
|
logger.log("VerificationRequest: validateEvent: " +
|
||||||
|
"fail because methods");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (type === REQUEST_TYPE || type === START_TYPE) {
|
|
||||||
|
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
|
||||||
if (typeof content.from_device !== "string" ||
|
if (typeof content.from_device !== "string" ||
|
||||||
content.from_device.length === 0
|
content.from_device.length === 0
|
||||||
) {
|
) {
|
||||||
return false;
|
logger.log("VerificationRequest: validateEvent: "+
|
||||||
}
|
"fail because from_device");
|
||||||
}
|
|
||||||
|
|
||||||
// a timestamp is not provided on all to_device events
|
|
||||||
if (Number.isFinite(timestamp)) {
|
|
||||||
const elapsed = Date.now() - timestamp;
|
|
||||||
// ignore if event is too far in the past or too far in the future
|
|
||||||
if (elapsed > (VERIFICATION_REQUEST_TIMEOUT - VERIFICATION_REQUEST_MARGIN) ||
|
|
||||||
elapsed < -(VERIFICATION_REQUEST_TIMEOUT / 2)) {
|
|
||||||
logger.log("received verification that is too old or from the future");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,20 +113,53 @@ export class VerificationRequest extends EventEmitter {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** once the phase is PHASE_STARTED, common methods supported by both sides */
|
get invalid() {
|
||||||
|
return this.phase === PHASE_UNSENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns whether the phase is PHASE_REQUESTED */
|
||||||
|
get requested() {
|
||||||
|
return this.phase === PHASE_REQUESTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns whether the phase is PHASE_CANCELLED */
|
||||||
|
get cancelled() {
|
||||||
|
return this.phase === PHASE_CANCELLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns whether the phase is PHASE_READY */
|
||||||
|
get ready() {
|
||||||
|
return this.phase === PHASE_READY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns whether the phase is PHASE_STARTED */
|
||||||
|
get started() {
|
||||||
|
return this.phase === PHASE_STARTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns whether the phase is PHASE_DONE */
|
||||||
|
get done() {
|
||||||
|
return this.phase === PHASE_DONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */
|
||||||
get methods() {
|
get methods() {
|
||||||
return this._commonMethods;
|
return this._commonMethods;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** the timeout of the request, provided for compatibility with previous verification code */
|
/** the current remaining amount of ms before the request should be automatically cancelled */
|
||||||
get timeout() {
|
get timeout() {
|
||||||
const elapsed = Date.now() - this._startTimestamp;
|
const requestEvent = this._getEventByEither(REQUEST_TYPE);
|
||||||
|
if (requestEvent) {
|
||||||
|
const elapsed = Date.now() - this.channel.getTimestamp(requestEvent);
|
||||||
return Math.max(0, VERIFICATION_REQUEST_TIMEOUT - elapsed);
|
return Math.max(0, VERIFICATION_REQUEST_TIMEOUT - elapsed);
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/** the m.key.verification.request event that started this request, provided for compatibility with previous verification code */
|
/** the m.key.verification.request event that started this request, provided for compatibility with previous verification code */
|
||||||
get event() {
|
get event() {
|
||||||
return this._requestEvent;
|
return this._getEventByEither(REQUEST_TYPE) || this._getEventByEither(START_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** current phase of the request. Some properties might only be defined in a current phase. */
|
/** current phase of the request. Some properties might only be defined in a current phase. */
|
||||||
@@ -142,8 +174,7 @@ export class VerificationRequest extends EventEmitter {
|
|||||||
|
|
||||||
/** whether this request has sent it's initial event and needs more events to complete */
|
/** whether this request has sent it's initial event and needs more events to complete */
|
||||||
get pending() {
|
get pending() {
|
||||||
return this._phase !== PHASE_UNSENT
|
return this._phase !== PHASE_DONE
|
||||||
&& this._phase !== PHASE_DONE
|
|
||||||
&& this._phase !== PHASE_CANCELLED;
|
&& this._phase !== PHASE_CANCELLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +183,25 @@ export class VerificationRequest extends EventEmitter {
|
|||||||
* For ToDeviceChannel, this is who sent the .start event
|
* For ToDeviceChannel, this is who sent the .start event
|
||||||
*/
|
*/
|
||||||
get initiatedByMe() {
|
get initiatedByMe() {
|
||||||
return this._initiatedByMe;
|
// event created by us but no remote echo has been received yet
|
||||||
|
const noEventsYet = (this._eventsByUs.size + this._eventsByThem.size) === 0;
|
||||||
|
if (this._phase === PHASE_UNSENT && noEventsYet) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const hasMyRequest = this._eventsByUs.has(REQUEST_TYPE);
|
||||||
|
const hasTheirRequest = this._eventsByThem.has(REQUEST_TYPE);
|
||||||
|
if (hasMyRequest && !hasTheirRequest) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!hasMyRequest && hasTheirRequest) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasMyStart = this._eventsByUs.has(START_TYPE);
|
||||||
|
const hasTheirStart = this._eventsByThem.has(START_TYPE);
|
||||||
|
if (hasMyStart && !hasTheirStart) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** the id of the user that initiated the request */
|
/** the id of the user that initiated the request */
|
||||||
@@ -160,19 +209,45 @@ export class VerificationRequest extends EventEmitter {
|
|||||||
if (this.initiatedByMe) {
|
if (this.initiatedByMe) {
|
||||||
return this._client.getUserId();
|
return this._client.getUserId();
|
||||||
} else {
|
} else {
|
||||||
return this._otherUserId;
|
return this.otherUserId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** the id of the user that (will) receive(d) the request */
|
/** the id of the user that (will) receive(d) the request */
|
||||||
get receivingUserId() {
|
get receivingUserId() {
|
||||||
if (this.initiatedByMe) {
|
if (this.initiatedByMe) {
|
||||||
return this._otherUserId;
|
return this.otherUserId;
|
||||||
} else {
|
} else {
|
||||||
return this._client.getUserId();
|
return this._client.getUserId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** the user id of the other party in this request */
|
||||||
|
get otherUserId() {
|
||||||
|
return this.channel.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the id of the user that cancelled the request,
|
||||||
|
* only defined when phase is PHASE_CANCELLED
|
||||||
|
*/
|
||||||
|
get cancellingUserId() {
|
||||||
|
const myCancel = this._eventsByUs.get(CANCEL_TYPE);
|
||||||
|
const theirCancel = this._eventsByThem.get(CANCEL_TYPE);
|
||||||
|
|
||||||
|
if (myCancel && (!theirCancel || myCancel.getId() < theirCancel.getId())) {
|
||||||
|
return myCancel.getSender();
|
||||||
|
}
|
||||||
|
if (theirCancel) {
|
||||||
|
return theirCancel.getSender();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
get observeOnly() {
|
||||||
|
return this._observeOnly;
|
||||||
|
}
|
||||||
|
|
||||||
/* Start the key verification, creating a verifier and sending a .start event.
|
/* Start the key verification, creating a verifier and sending a .start event.
|
||||||
* If no previous events have been sent, pass in `targetDevice` to set who to direct this request to.
|
* If no previous events have been sent, pass in `targetDevice` to set who to direct this request to.
|
||||||
* @param {string} method the name of the verification method to use.
|
* @param {string} method the name of the verification method to use.
|
||||||
@@ -182,8 +257,13 @@ export class VerificationRequest extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
beginKeyVerification(method, targetDevice = null) {
|
beginKeyVerification(method, targetDevice = null) {
|
||||||
// need to allow also when unsent in case of to_device
|
// need to allow also when unsent in case of to_device
|
||||||
if (!this._verifier) {
|
if (!this.observeOnly && !this._verifier) {
|
||||||
if (this._hasValidPreStartPhase()) {
|
const validStartPhase =
|
||||||
|
this.phase === PHASE_REQUESTED ||
|
||||||
|
this.phase === PHASE_READY ||
|
||||||
|
(this.phase === PHASE_UNSENT &&
|
||||||
|
this.channel.constructor.canCreateRequest(START_TYPE));
|
||||||
|
if (validStartPhase) {
|
||||||
// when called on a request that was initiated with .request event
|
// when called on a request that was initiated with .request event
|
||||||
// check the method is supported by both sides
|
// check the method is supported by both sides
|
||||||
if (this._commonMethods.length && !this._commonMethods.includes(method)) {
|
if (this._commonMethods.length && !this._commonMethods.includes(method)) {
|
||||||
@@ -203,12 +283,9 @@ export class VerificationRequest extends EventEmitter {
|
|||||||
* @returns {Promise} resolves when the event has been sent.
|
* @returns {Promise} resolves when the event has been sent.
|
||||||
*/
|
*/
|
||||||
async sendRequest() {
|
async sendRequest() {
|
||||||
if (this._phase === PHASE_UNSENT) {
|
if (!this.observeOnly && this._phase === PHASE_UNSENT) {
|
||||||
this._initiatedByMe = true;
|
|
||||||
this._setPhase(PHASE_REQUESTED, false);
|
|
||||||
const methods = [...this._verificationMethods.keys()];
|
const methods = [...this._verificationMethods.keys()];
|
||||||
await this.channel.send(REQUEST_TYPE, {methods});
|
await this.channel.send(REQUEST_TYPE, {methods});
|
||||||
this.emit("change");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,32 +296,54 @@ export class VerificationRequest extends EventEmitter {
|
|||||||
* @returns {Promise} resolves when the event has been sent.
|
* @returns {Promise} resolves when the event has been sent.
|
||||||
*/
|
*/
|
||||||
async cancel({reason = "User declined", code = "m.user"} = {}) {
|
async cancel({reason = "User declined", code = "m.user"} = {}) {
|
||||||
if (this._phase !== PHASE_CANCELLED) {
|
if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
|
||||||
if (this._verifier) {
|
if (this._verifier) {
|
||||||
return this._verifier.cancel(errorFactory(code, reason));
|
return this._verifier.cancel(errorFactory(code, reason));
|
||||||
} else {
|
} else {
|
||||||
this._setPhase(PHASE_CANCELLED, false);
|
this._cancellingUserId = this._client.getUserId();
|
||||||
await this.channel.send(CANCEL_TYPE, {code, reason});
|
await this.channel.send(CANCEL_TYPE, {code, reason});
|
||||||
}
|
}
|
||||||
this.emit("change");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {Promise} with the verifier once it becomes available. Can be used after calling `sendRequest`. */
|
/**
|
||||||
waitForVerifier() {
|
* Accepts the request, sending a .ready event to the other party
|
||||||
if (this.verifier) {
|
* @returns {Promise} resolves when the event has been sent.
|
||||||
return Promise.resolve(this.verifier);
|
*/
|
||||||
} else {
|
async accept() {
|
||||||
return new Promise(resolve => {
|
if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
|
||||||
const checkVerifier = () => {
|
const methods = [...this._verificationMethods.keys()];
|
||||||
if (this.verifier) {
|
await this.channel.send(READY_TYPE, {methods});
|
||||||
this.off("change", checkVerifier);
|
|
||||||
resolve(this.verifier);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can be used to listen for state changes until the callback returns true.
|
||||||
|
* @param {Function} fn callback to evaluate whether the request is in the desired state.
|
||||||
|
* Takes the request as an argument.
|
||||||
|
* @returns {Promise} that resolves once the callback returns true
|
||||||
|
* @throws {Error} when the request is cancelled
|
||||||
|
*/
|
||||||
|
waitFor(fn) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const check = () => {
|
||||||
|
let handled = false;
|
||||||
|
if (fn(this)) {
|
||||||
|
resolve(this);
|
||||||
|
handled = true;
|
||||||
|
} else if (this.cancelled) {
|
||||||
|
reject(new Error("cancelled"));
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
if (handled) {
|
||||||
|
this.off("change", check);
|
||||||
|
}
|
||||||
|
return handled;
|
||||||
};
|
};
|
||||||
this.on("change", checkVerifier);
|
if (!check()) {
|
||||||
});
|
this.on("change", check);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_setPhase(phase, notify = true) {
|
_setPhase(phase, notify = true) {
|
||||||
@@ -254,155 +353,278 @@ export class VerificationRequest extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getEventByEither(type) {
|
||||||
|
return this._eventsByThem.get(type) || this._eventsByUs.get(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getEventByOther(type, notSender) {
|
||||||
|
if (notSender === this._client.getUserId()) {
|
||||||
|
return this._eventsByThem.get(type);
|
||||||
|
} else {
|
||||||
|
return this._eventsByUs.get(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getEventBy(type, sender) {
|
||||||
|
if (sender === this._client.getUserId()) {
|
||||||
|
return this._eventsByUs.get(type);
|
||||||
|
} else {
|
||||||
|
return this._eventsByThem.get(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_calculatePhaseTransitions() {
|
||||||
|
const transitions = [{phase: PHASE_UNSENT}];
|
||||||
|
const phase = () => transitions[transitions.length - 1].phase;
|
||||||
|
|
||||||
|
// always pass by .request first to be sure channel.userId has been set
|
||||||
|
const requestEvent = this._getEventByEither(REQUEST_TYPE);
|
||||||
|
if (requestEvent) {
|
||||||
|
transitions.push({phase: PHASE_REQUESTED, event: requestEvent});
|
||||||
|
}
|
||||||
|
|
||||||
|
const readyEvent =
|
||||||
|
requestEvent && this._getEventByOther(READY_TYPE, requestEvent.getSender());
|
||||||
|
if (readyEvent && phase() === PHASE_REQUESTED) {
|
||||||
|
transitions.push({phase: PHASE_READY, event: readyEvent});
|
||||||
|
}
|
||||||
|
|
||||||
|
const startEvent = readyEvent || !requestEvent ?
|
||||||
|
this._getEventByEither(START_TYPE) : // any party can send .start after a .ready or unsent
|
||||||
|
this._getEventByOther(START_TYPE, requestEvent.getSender());
|
||||||
|
if (startEvent) {
|
||||||
|
const fromRequestPhase = phase() === PHASE_REQUESTED &&
|
||||||
|
requestEvent.getSender() !== startEvent.getSender();
|
||||||
|
const fromUnsentPhase = phase() === PHASE_UNSENT &&
|
||||||
|
this.channel.constructor.canCreateRequest(START_TYPE);
|
||||||
|
if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) {
|
||||||
|
transitions.push({phase: PHASE_STARTED, event: startEvent});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ourDoneEvent = this._eventsByUs.get(DONE_TYPE);
|
||||||
|
const theirDoneEvent = this._eventsByThem.get(DONE_TYPE);
|
||||||
|
if (ourDoneEvent && theirDoneEvent && phase() === PHASE_STARTED) {
|
||||||
|
transitions.push({phase: PHASE_DONE});
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelEvent = this._getEventByEither(CANCEL_TYPE);
|
||||||
|
if (cancelEvent && phase() !== PHASE_DONE) {
|
||||||
|
transitions.push({phase: PHASE_CANCELLED, event: cancelEvent});
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
_transitionToPhase(transition) {
|
||||||
|
const {phase, event} = transition;
|
||||||
|
// get common methods
|
||||||
|
if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
|
||||||
|
if (!this._wasSentByOwnDevice(event)) {
|
||||||
|
const content = event.getContent();
|
||||||
|
this._commonMethods =
|
||||||
|
content.methods.filter(m => this._verificationMethods.has(m));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// detect if we're not a party in the request, and we should just observe
|
||||||
|
if (!this.observeOnly) {
|
||||||
|
// if requested or accepted by one of my other devices
|
||||||
|
if (phase === PHASE_REQUESTED ||
|
||||||
|
phase === PHASE_STARTED ||
|
||||||
|
phase === PHASE_READY
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
this.channel.receiveStartFromOtherDevices &&
|
||||||
|
this._wasSentByOwnUser(event) &&
|
||||||
|
!this._wasSentByOwnDevice(event)
|
||||||
|
) {
|
||||||
|
this._observeOnly = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// create verifier
|
||||||
|
if (phase === PHASE_STARTED) {
|
||||||
|
const {method} = event.getContent();
|
||||||
|
if (!this._verifier && !this.observeOnly) {
|
||||||
|
this._verifier = this._createVerifier(method, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the state of the request and verifier in response to a key verification event.
|
* Changes the state of the request and verifier in response to a key verification event.
|
||||||
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
|
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
|
||||||
* @param {MatrixEvent} event the event to handle. Don't call getType() on it but use the `type` parameter instead.
|
* @param {MatrixEvent} event the event to handle. Don't call getType() on it but use the `type` parameter instead.
|
||||||
* @param {number} timestamp the timestamp in milliseconds when this event was sent.
|
* @param {bool} isLiveEvent whether this is an even received through sync or not
|
||||||
|
* @param {bool} isRemoteEcho whether this is the remote echo of an event sent by the same device
|
||||||
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
||||||
*/
|
*/
|
||||||
async handleEvent(type, event, timestamp) {
|
async handleEvent(type, event, isLiveEvent, isRemoteEcho) {
|
||||||
const content = event.getContent();
|
// if reached phase cancelled or done, ignore anything else that comes
|
||||||
if (type === REQUEST_TYPE || type === START_TYPE) {
|
if (!this.pending) {
|
||||||
if (this._startTimestamp === null) {
|
return;
|
||||||
this._startTimestamp = timestamp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (type === REQUEST_TYPE) {
|
|
||||||
await this._handleRequest(content, event);
|
|
||||||
} else if (type === START_TYPE) {
|
|
||||||
await this._handleStart(content, event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._verifier) {
|
this._adjustObserveOnly(event, isLiveEvent);
|
||||||
|
|
||||||
|
if (!this.observeOnly && !isRemoteEcho) {
|
||||||
|
if (await this._cancelOnError(type, event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._addEvent(type, event, isRemoteEcho);
|
||||||
|
|
||||||
|
const transitions = this._calculatePhaseTransitions();
|
||||||
|
const existingIdx = transitions.findIndex(t => t.phase === this.phase);
|
||||||
|
// trim off phases we already went through, if any
|
||||||
|
const newTransitions = transitions.slice(existingIdx + 1);
|
||||||
|
// transition to all new phases
|
||||||
|
for (const transition of newTransitions) {
|
||||||
|
this._transitionToPhase(transition);
|
||||||
|
}
|
||||||
|
// only pass events from the other side to the verifier,
|
||||||
|
// no remote echos of our own events
|
||||||
|
if (this._verifier && !isRemoteEcho) {
|
||||||
if (type === CANCEL_TYPE || (this._verifier.events
|
if (type === CANCEL_TYPE || (this._verifier.events
|
||||||
&& this._verifier.events.includes(type))) {
|
&& this._verifier.events.includes(type))) {
|
||||||
this._verifier.handleEvent(event);
|
this._verifier.handleEvent(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === CANCEL_TYPE) {
|
if (newTransitions.length) {
|
||||||
this._handleCancel();
|
const lastTransition = newTransitions[newTransitions.length - 1];
|
||||||
} else if (type === DONE_TYPE) {
|
const {phase} = lastTransition;
|
||||||
this._handleDone();
|
|
||||||
|
this._setupTimeout(phase);
|
||||||
|
// set phase as last thing as this emits the "change" event
|
||||||
|
this._setPhase(phase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _handleRequest(content, event) {
|
_setupTimeout(phase) {
|
||||||
if (this._phase === PHASE_UNSENT) {
|
const shouldTimeout = !this._timeoutTimer && !this.observeOnly &&
|
||||||
const otherMethods = content.methods;
|
phase === PHASE_REQUESTED && this.initiatedByMe;
|
||||||
this._commonMethods = otherMethods.
|
|
||||||
filter(m => this._verificationMethods.has(m));
|
if (shouldTimeout) {
|
||||||
this._requestEvent = event;
|
this._timeoutTimer = setTimeout(this._cancelOnTimeout, this.timeout);
|
||||||
this._initiatedByMe = this._wasSentByMe(event);
|
}
|
||||||
this._setPhase(PHASE_REQUESTED);
|
if (this._timeoutTimer) {
|
||||||
} else if (this._phase !== PHASE_REQUESTED) {
|
const shouldClear = phase === PHASE_STARTED ||
|
||||||
logger.warn("Ignoring flagged verification request from " +
|
phase === PHASE_READY ||
|
||||||
event.getSender());
|
phase === PHASE_DONE ||
|
||||||
await this.cancel(errorFromEvent(newUnexpectedMessageError()));
|
phase === PHASE_CANCELLED;
|
||||||
|
if (shouldClear) {
|
||||||
|
clearTimeout(this._timeoutTimer);
|
||||||
|
this._timeoutTimer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_hasValidPreStartPhase() {
|
_cancelOnTimeout = () => {
|
||||||
return this._phase === PHASE_REQUESTED ||
|
try {
|
||||||
(
|
this.cancel({reason: "Other party didn't accept in time", code: "m.timeout"});
|
||||||
this.channel.constructor.canCreateRequest(START_TYPE) &&
|
} catch (err) {
|
||||||
this._phase === PHASE_UNSENT
|
console.error("Error while cancelling verification request", err);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async _handleStart(content, event) {
|
async _cancelOnError(type, event) {
|
||||||
if (this._hasValidPreStartPhase()) {
|
if (type === START_TYPE) {
|
||||||
const {method} = content;
|
const method = event.getContent().method;
|
||||||
if (!this._verificationMethods.has(method)) {
|
if (!this._verificationMethods.has(method)) {
|
||||||
await this.cancel(errorFromEvent(newUnknownMethodError()));
|
await this.cancel(errorFromEvent(newUnknownMethodError()));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FIXME: https://github.com/vector-im/riot-web/issues/11765 */
|
||||||
|
const isUnexpectedRequest = type === REQUEST_TYPE && this.phase !== PHASE_UNSENT;
|
||||||
|
const isUnexpectedReady = type === READY_TYPE && this.phase !== PHASE_REQUESTED;
|
||||||
|
if (isUnexpectedRequest || isUnexpectedReady) {
|
||||||
|
logger.warn(`Cancelling, unexpected ${type} verification ` +
|
||||||
|
`event from ${event.getSender()}`);
|
||||||
|
const reason = `Unexpected ${type} event in phase ${this.phase}`;
|
||||||
|
await this.cancel(errorFromEvent(newUnexpectedMessageError({reason})));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_adjustObserveOnly(event, isLiveEvent) {
|
||||||
|
// don't send out events for historical requests
|
||||||
|
if (!isLiveEvent) {
|
||||||
|
this._observeOnly = true;
|
||||||
|
}
|
||||||
|
// a timestamp is not provided on all to_device events
|
||||||
|
const timestamp = this.channel.getTimestamp(event);
|
||||||
|
if (Number.isFinite(timestamp)) {
|
||||||
|
const elapsed = Date.now() - timestamp;
|
||||||
|
// don't allow interaction on old requests
|
||||||
|
if (elapsed > (VERIFICATION_REQUEST_TIMEOUT - VERIFICATION_REQUEST_MARGIN) ||
|
||||||
|
elapsed < -(VERIFICATION_REQUEST_TIMEOUT / 2)
|
||||||
|
) {
|
||||||
|
this._observeOnly = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_addEvent(type, event, isRemoteEcho) {
|
||||||
|
if (isRemoteEcho || this._wasSentByOwnDevice(event)) {
|
||||||
|
this._eventsByUs.set(type, event);
|
||||||
} else {
|
} else {
|
||||||
// if not in requested phase
|
this._eventsByThem.set(type, event);
|
||||||
if (this.phase === PHASE_UNSENT) {
|
|
||||||
this._initiatedByMe = this._wasSentByMe(event);
|
|
||||||
}
|
|
||||||
this._verifier = this._createVerifier(method, event);
|
|
||||||
this._setPhase(PHASE_STARTED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// once we know the userId of the other party (from the .request event)
|
||||||
* Called by RequestCallbackChannel when the verifier sends an event
|
// see if any event by anyone else crept into this._eventsByThem
|
||||||
* @param {string} type the "symbolic" event type
|
if (type === REQUEST_TYPE) {
|
||||||
* @param {object} content the completed or uncompleted content for the event to be sent
|
for (const [type, event] of this._eventsByThem.entries()) {
|
||||||
*/
|
if (event.getSender() !== this.otherUserId) {
|
||||||
handleVerifierSend(type, content) {
|
this._eventsByThem.delete(type);
|
||||||
if (type === CANCEL_TYPE) {
|
|
||||||
this._handleCancel();
|
|
||||||
} else if (type === START_TYPE) {
|
|
||||||
if (this._phase === PHASE_UNSENT || this._phase === PHASE_REQUESTED) {
|
|
||||||
// if unsent, we're sending a (first) .start event and hence requesting the verification.
|
|
||||||
// in any other situation, the request was initiated by the other party.
|
|
||||||
this._initiatedByMe = this.phase === PHASE_UNSENT;
|
|
||||||
this._setPhase(PHASE_STARTED);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleCancel() {
|
|
||||||
if (this._phase !== PHASE_CANCELLED) {
|
|
||||||
this._setPhase(PHASE_CANCELLED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleDone() {
|
|
||||||
if (this._phase === PHASE_STARTED) {
|
|
||||||
this._setPhase(PHASE_DONE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_createVerifier(method, startEvent = null, targetDevice = null) {
|
_createVerifier(method, startEvent = null, targetDevice = null) {
|
||||||
const startSentByMe = startEvent && this._wasSentByMe(startEvent);
|
const startedByMe = !startEvent || this._wasSentByOwnDevice(startEvent);
|
||||||
const {userId, deviceId} = this._getVerifierTarget(startEvent, targetDevice);
|
if (!targetDevice) {
|
||||||
|
const theirFirstEvent =
|
||||||
|
this._eventsByThem.get(REQUEST_TYPE) ||
|
||||||
|
this._eventsByThem.get(READY_TYPE) ||
|
||||||
|
this._eventsByThem.get(START_TYPE);
|
||||||
|
const theirFirstContent = theirFirstEvent.getContent();
|
||||||
|
const fromDevice = theirFirstContent.from_device;
|
||||||
|
targetDevice = {
|
||||||
|
userId: this.otherUserId,
|
||||||
|
deviceId: fromDevice,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const {userId, deviceId} = targetDevice;
|
||||||
|
|
||||||
const VerifierCtor = this._verificationMethods.get(method);
|
const VerifierCtor = this._verificationMethods.get(method);
|
||||||
if (!VerifierCtor) {
|
if (!VerifierCtor) {
|
||||||
console.warn("could not find verifier constructor for method", method);
|
console.warn("could not find verifier constructor for method", method);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// invokes handleVerifierSend when verifier sends something
|
|
||||||
const callbackMedium = new RequestCallbackChannel(this, this.channel);
|
|
||||||
return new VerifierCtor(
|
return new VerifierCtor(
|
||||||
callbackMedium,
|
this.channel,
|
||||||
this._client,
|
this._client,
|
||||||
userId,
|
userId,
|
||||||
deviceId,
|
deviceId,
|
||||||
startSentByMe ? null : startEvent,
|
startedByMe ? null : startEvent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getVerifierTarget(startEvent, targetDevice) {
|
_wasSentByOwnUser(event) {
|
||||||
// targetDevice should be set when creating a verifier for to_device before the .start event has been sent,
|
return event.getSender() === this._client.getUserId();
|
||||||
// so the userId and deviceId are provided
|
|
||||||
if (targetDevice) {
|
|
||||||
return targetDevice;
|
|
||||||
} else {
|
|
||||||
let targetEvent;
|
|
||||||
if (startEvent && !this._wasSentByMe(startEvent)) {
|
|
||||||
targetEvent = startEvent;
|
|
||||||
} else if (this._requestEvent && !this._wasSentByMe(this._requestEvent)) {
|
|
||||||
targetEvent = this._requestEvent;
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
"can't determine who the verifier should be targeted at. " +
|
|
||||||
"No .request or .start event and no targetDevice");
|
|
||||||
}
|
|
||||||
const userId = targetEvent.getSender();
|
|
||||||
const content = targetEvent.getContent();
|
|
||||||
const deviceId = content && content.from_device;
|
|
||||||
return {userId, deviceId};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// only for .request and .start
|
// only for .request, .ready or .start
|
||||||
_wasSentByMe(event) {
|
_wasSentByOwnDevice(event) {
|
||||||
if (event.getSender() !== this._client.getUserId()) {
|
if (!this._wasSentByOwnUser(event)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
|
|||||||
@@ -490,8 +490,9 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
|
|||||||
*
|
*
|
||||||
* @param {MatrixEvent} event Event to be added
|
* @param {MatrixEvent} event Event to be added
|
||||||
* @param {string?} duplicateStrategy 'ignore' or 'replace'
|
* @param {string?} duplicateStrategy 'ignore' or 'replace'
|
||||||
|
* @param {boolean} fromCache whether the sync response came from cache
|
||||||
*/
|
*/
|
||||||
EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
|
EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy, fromCache) {
|
||||||
if (this._filter) {
|
if (this._filter) {
|
||||||
const events = this._filter.filterRoomTimeline([event]);
|
const events = this._filter.filterRoomTimeline([event]);
|
||||||
if (!events.length) {
|
if (!events.length) {
|
||||||
@@ -529,7 +530,7 @@ EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addEventToTimeline(event, this._liveTimeline, false);
|
this.addEventToTimeline(event, this._liveTimeline, false, fromCache);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -541,11 +542,12 @@ EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
|
|||||||
* @param {MatrixEvent} event
|
* @param {MatrixEvent} event
|
||||||
* @param {EventTimeline} timeline
|
* @param {EventTimeline} timeline
|
||||||
* @param {boolean} toStartOfTimeline
|
* @param {boolean} toStartOfTimeline
|
||||||
|
* @param {boolean} fromCache whether the sync response came from cache
|
||||||
*
|
*
|
||||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||||
*/
|
*/
|
||||||
EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
|
EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
|
||||||
toStartOfTimeline) {
|
toStartOfTimeline, fromCache) {
|
||||||
const eventId = event.getId();
|
const eventId = event.getId();
|
||||||
timeline.addEvent(event, toStartOfTimeline);
|
timeline.addEvent(event, toStartOfTimeline);
|
||||||
this._eventIdToTimeline[eventId] = timeline;
|
this._eventIdToTimeline[eventId] = timeline;
|
||||||
@@ -555,7 +557,7 @@ EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
|
|||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
timeline: timeline,
|
timeline: timeline,
|
||||||
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
|
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline && !fromCache,
|
||||||
};
|
};
|
||||||
this.emit("Room.timeline", event, this.room,
|
this.emit("Room.timeline", event, this.room,
|
||||||
Boolean(toStartOfTimeline), false, data);
|
Boolean(toStartOfTimeline), false, data);
|
||||||
|
|||||||
@@ -154,6 +154,12 @@ export const MatrixEvent = function(
|
|||||||
* attempt may succeed)
|
* attempt may succeed)
|
||||||
*/
|
*/
|
||||||
this._retryDecryption = false;
|
this._retryDecryption = false;
|
||||||
|
|
||||||
|
/* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
|
||||||
|
* `Crypto` will set this the `VerificationRequest` for the event
|
||||||
|
* so it can be easily accessed from the timeline.
|
||||||
|
*/
|
||||||
|
this.verificationRequest = null;
|
||||||
};
|
};
|
||||||
utils.inherits(MatrixEvent, EventEmitter);
|
utils.inherits(MatrixEvent, EventEmitter);
|
||||||
|
|
||||||
@@ -1054,6 +1060,10 @@ utils.extend(MatrixEvent.prototype, {
|
|||||||
encrypted: this.event,
|
encrypted: this.event,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setVerificationRequest: function(request) {
|
||||||
|
this.verificationRequest = request;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1067,10 +1067,11 @@ Room.prototype.removeFilteredTimelineSet = function(filter) {
|
|||||||
*
|
*
|
||||||
* @param {MatrixEvent} event Event to be added
|
* @param {MatrixEvent} event Event to be added
|
||||||
* @param {string?} duplicateStrategy 'ignore' or 'replace'
|
* @param {string?} duplicateStrategy 'ignore' or 'replace'
|
||||||
|
* @param {boolean} fromCache whether the sync response came from cache
|
||||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
Room.prototype._addLiveEvent = function(event, duplicateStrategy, fromCache) {
|
||||||
if (event.isRedaction()) {
|
if (event.isRedaction()) {
|
||||||
const redactId = event.event.redacts;
|
const redactId = event.event.redacts;
|
||||||
|
|
||||||
@@ -1117,7 +1118,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
|||||||
|
|
||||||
// add to our timeline sets
|
// add to our timeline sets
|
||||||
for (let i = 0; i < this._timelineSets.length; i++) {
|
for (let i = 0; i < this._timelineSets.length; i++) {
|
||||||
this._timelineSets[i].addLiveEvent(event, duplicateStrategy);
|
this._timelineSets[i].addLiveEvent(event, duplicateStrategy, fromCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
// synthesize and inject implicit read receipts
|
// synthesize and inject implicit read receipts
|
||||||
@@ -1427,9 +1428,10 @@ Room.prototype._revertRedactionLocalEcho = function(redactionEvent) {
|
|||||||
* this function will be ignored entirely, preserving the existing event in the
|
* this function will be ignored entirely, preserving the existing event in the
|
||||||
* timeline. Events are identical based on their event ID <b>only</b>.
|
* timeline. Events are identical based on their event ID <b>only</b>.
|
||||||
*
|
*
|
||||||
|
* @param {boolean} fromCache whether the sync response came from cache
|
||||||
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
|
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
|
||||||
*/
|
*/
|
||||||
Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
|
Room.prototype.addLiveEvents = function(events, duplicateStrategy, fromCache) {
|
||||||
let i;
|
let i;
|
||||||
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
|
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
|
||||||
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
|
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
|
||||||
@@ -1455,7 +1457,7 @@ Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
|
|||||||
for (i = 0; i < events.length; i++) {
|
for (i = 0; i < events.length; i++) {
|
||||||
// TODO: We should have a filter to say "only add state event
|
// TODO: We should have a filter to say "only add state event
|
||||||
// types X Y Z to the timeline".
|
// types X Y Z to the timeline".
|
||||||
this._addLiveEvent(events[i], duplicateStrategy);
|
this._addLiveEvent(events[i], duplicateStrategy, fromCache);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -688,6 +688,7 @@ SyncApi.prototype._syncFromCache = async function(savedSync) {
|
|||||||
oldSyncToken: null,
|
oldSyncToken: null,
|
||||||
nextSyncToken,
|
nextSyncToken,
|
||||||
catchingUp: false,
|
catchingUp: false,
|
||||||
|
fromCache: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
@@ -1237,7 +1238,8 @@ SyncApi.prototype._processSyncResponse = async function(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self._processRoomEvents(room, stateEvents, timelineEvents);
|
self._processRoomEvents(room, stateEvents,
|
||||||
|
timelineEvents, syncEventData.fromCache);
|
||||||
|
|
||||||
// set summary after processing events,
|
// set summary after processing events,
|
||||||
// because it will trigger a name calculation
|
// because it will trigger a name calculation
|
||||||
@@ -1564,10 +1566,11 @@ SyncApi.prototype._resolveInvites = function(room) {
|
|||||||
* @param {MatrixEvent[]} stateEventList A list of state events. This is the state
|
* @param {MatrixEvent[]} stateEventList A list of state events. This is the state
|
||||||
* at the *START* of the timeline list if it is supplied.
|
* at the *START* of the timeline list if it is supplied.
|
||||||
* @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
|
* @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
|
||||||
|
* @param {boolean} fromCache whether the sync response came from cache
|
||||||
* is earlier in time. Higher index is later.
|
* is earlier in time. Higher index is later.
|
||||||
*/
|
*/
|
||||||
SyncApi.prototype._processRoomEvents = function(room, stateEventList,
|
SyncApi.prototype._processRoomEvents = function(room, stateEventList,
|
||||||
timelineEventList) {
|
timelineEventList, fromCache) {
|
||||||
// If there are no events in the timeline yet, initialise it with
|
// If there are no events in the timeline yet, initialise it with
|
||||||
// the given state events
|
// the given state events
|
||||||
const liveTimeline = room.getLiveTimeline();
|
const liveTimeline = room.getLiveTimeline();
|
||||||
@@ -1621,7 +1624,7 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
|
|||||||
// if the timeline has any state events in it.
|
// if the timeline has any state events in it.
|
||||||
// This also needs to be done before running push rules on the events as they need
|
// This also needs to be done before running push rules on the events as they need
|
||||||
// to be decorated with sender etc.
|
// to be decorated with sender etc.
|
||||||
room.addLiveEvents(timelineEventList || []);
|
room.addLiveEvents(timelineEventList || [], null, fromCache);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user