Compare commits
218 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ee99a0616 | |||
| 3ace1d04cd | |||
| 6684574bdf | |||
| 36a945f8e2 | |||
| 6a3d322033 | |||
| 00c003ec65 | |||
| f4d335c161 | |||
| 659f42139b | |||
| 0e791ed022 | |||
| 48655aa1a3 | |||
| 83fa80cfda | |||
| cf5b5ee085 | |||
| 429a4e3526 | |||
| d66d4c1cd9 | |||
| 7a1bbdf2dd | |||
| 29c1459568 | |||
| efad46a8a4 | |||
| a69c621305 | |||
| ad6dde6f26 | |||
| 2627e46723 | |||
| 408d70b55e | |||
| 3f369e528b | |||
| 312976294b | |||
| 77f42c479b | |||
| d60bd22674 | |||
| 2e67f77d3e | |||
| 6d8e8e6bd7 | |||
| 9c01945a05 | |||
| 7ce5ddd380 | |||
| 2b5de914f5 | |||
| 18a2426707 | |||
| 367fac6d54 | |||
| 157cc9e5eb | |||
| 81daf12598 | |||
| 9249b0652f | |||
| ee4c6b6265 | |||
| 68deab4a68 | |||
| c9c765b5b8 | |||
| 616f73d8c6 | |||
| 208c371afb | |||
| 3a59cfa9c0 | |||
| cf94527bd5 | |||
| fa93479863 | |||
| 8bc0ef8c27 | |||
| bd403b6d87 | |||
| 57a7328065 | |||
| 4945463beb | |||
| dfafa791f2 | |||
| 5f2cb6b3a4 | |||
| 5398fac348 | |||
| b217f6aa81 | |||
| ec597bea93 | |||
| 7a5c54fef7 | |||
| 4064f18de2 | |||
| 6d13457172 | |||
| f39518ef93 | |||
| 4b1cecd246 | |||
| 352509fd3a | |||
| d0f08f8839 | |||
| efd38a3471 | |||
| a4e74fea94 | |||
| fdb33b6189 | |||
| dcbb67838b | |||
| 1727d636a3 | |||
| 9eadc7f868 | |||
| 620118af5f | |||
| 3645764f9a | |||
| 769bfeb10f | |||
| 5fbaa9cfa7 | |||
| 007508ba12 | |||
| 0f1f18b232 | |||
| d6b754b133 | |||
| 1b80c83676 | |||
| ec4dc582b6 | |||
| 65646ff9e2 | |||
| 92f6ec918b | |||
| 62bd41d2e6 | |||
| 9d864ffd60 | |||
| c45b38cece | |||
| 0d7aee2c36 | |||
| be345a523f | |||
| 470bdf8741 | |||
| 59319fb55b | |||
| fb7695fdbc | |||
| 25b7552683 | |||
| 21d520378f | |||
| 9cd6607520 | |||
| efd3550f53 | |||
| 76402ec8d7 | |||
| f689142806 | |||
| fd563bda6a | |||
| 09a8f7122c | |||
| 608fb00844 | |||
| 5c45e9c306 | |||
| 950221dc13 | |||
| f816679596 | |||
| 80ccf18b16 | |||
| c7abd9062a | |||
| 4287f2229b | |||
| 8408055137 | |||
| cc0965d703 | |||
| 94b3d9d3e1 | |||
| 772bf7d6ff | |||
| 15c2e4bb07 | |||
| 419693023f | |||
| 2d081f2c19 | |||
| c76ce1fd85 | |||
| f38b4d37e6 | |||
| 73c92dfc57 | |||
| 61c5430deb | |||
| 21e4c597d9 | |||
| 4dbeee8cb3 | |||
| adc76c636e | |||
| 0dbf89b2b4 | |||
| 83241ac17d | |||
| 6aa5d39357 | |||
| 1304ecbe03 | |||
| aafc027812 | |||
| 3a4b6f0ea0 | |||
| b3d10ace21 | |||
| c17df7a6f7 | |||
| 1c13f5026e | |||
| b9cfede888 | |||
| 49fd9e90a0 | |||
| e09038232e | |||
| 2cfe310e89 | |||
| 973c7467e8 | |||
| 583df7ed7d | |||
| 6d05376f04 | |||
| e1f832bfa7 | |||
| b8092cd00b | |||
| 3c1dca6cef | |||
| c0f7dd6fe9 | |||
| 6af6e99480 | |||
| c5cbe48668 | |||
| 15707956ef | |||
| 4668fc87a1 | |||
| 468fb2cc41 | |||
| 7c79e7e836 | |||
| 0bf1f48623 | |||
| f286eb4d11 | |||
| 9346c83dc1 | |||
| a76267f5b0 | |||
| 1d3a7b3d52 | |||
| f78f04d553 | |||
| 7b6dabbe9c | |||
| ed01b3b8cf | |||
| 7880a30e57 | |||
| 3a3ff93450 | |||
| 3a1cdd37a3 | |||
| 8db38f8e75 | |||
| ff24ef4ee5 | |||
| 3faeec4add | |||
| 7d56ee5084 | |||
| b2afaabb8c | |||
| 3efaf90bc8 | |||
| 0c52887688 | |||
| d5e9155a33 | |||
| 5def5ab074 | |||
| 1b242e636b | |||
| 05f05c889a | |||
| 1367e285c8 | |||
| 45ec3e0bb9 | |||
| dc38f78da2 | |||
| 1b6a74fd93 | |||
| 9d8a1494aa | |||
| 08465cf236 | |||
| 7016848401 | |||
| bdd2a9e7e8 | |||
| 80256e6782 | |||
| 7907ef44f8 | |||
| 3a97a24686 | |||
| 7f208ed44e | |||
| 22e6cfaebb | |||
| 9d6f873048 | |||
| d526229a0f | |||
| aac68290ac | |||
| bd9a2c13eb | |||
| e5c65d53f8 | |||
| 121e9d0225 | |||
| c12a3b6610 | |||
| 77d0a76186 | |||
| e89528315d | |||
| c34ccc9d53 | |||
| e51ba795f3 | |||
| cbe2965849 | |||
| 59bfc45856 | |||
| 07cc93cca2 | |||
| 1205178e26 | |||
| 72fd1e4e7c | |||
| f44e0a8e12 | |||
| 9338d9c2a6 | |||
| 75fc25feb5 | |||
| 5919874f6f | |||
| 213bb9dba2 | |||
| 3a9dc37d02 | |||
| 423c8a886d | |||
| 3ec8233a2d | |||
| 8ed51c806e | |||
| 57135a898f | |||
| 0d3d27a519 | |||
| cf42ad83da | |||
| e7bcb61a3b | |||
| 883b83f1da | |||
| 48977e6eaa | |||
| efe2488155 | |||
| 29c04b6f9c | |||
| 984b6234d2 | |||
| dac4a5452d | |||
| 5f9e82204a | |||
| c4142d93c3 | |||
| b34a2c7ee2 | |||
| cd7cc1b71f | |||
| 4c6dd564a4 | |||
| 28e46a82ea | |||
| 10e294784e | |||
| 2da725340c | |||
| 882d3a765d |
@@ -1,3 +1,101 @@
|
||||
Changes in [5.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.0.0-rc.1) (2020-02-13)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v4.0.0...v5.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* The verification methods API has removed an argument ([\#1206](https://github.com/matrix-org/matrix-js-sdk/pull/1206))
|
||||
|
||||
All Changes
|
||||
---
|
||||
|
||||
* Remove methods argument to verification
|
||||
[\#1206](https://github.com/matrix-org/matrix-js-sdk/pull/1206)
|
||||
* don't do a dynamic import of request
|
||||
[\#1207](https://github.com/matrix-org/matrix-js-sdk/pull/1207)
|
||||
* QR self-verification fixes
|
||||
[\#1201](https://github.com/matrix-org/matrix-js-sdk/pull/1201)
|
||||
* Log every verification event
|
||||
[\#1204](https://github.com/matrix-org/matrix-js-sdk/pull/1204)
|
||||
* dont require .done event from other party
|
||||
[\#1203](https://github.com/matrix-org/matrix-js-sdk/pull/1203)
|
||||
* New option to fully reset Secret Storage keys in boostrapSecretStorage
|
||||
[\#1202](https://github.com/matrix-org/matrix-js-sdk/pull/1202)
|
||||
* Add function to estimate target device for a VerificationRequest
|
||||
[\#1190](https://github.com/matrix-org/matrix-js-sdk/pull/1190)
|
||||
* pass ssss item name to callback so we can differentiate UI on it
|
||||
[\#1200](https://github.com/matrix-org/matrix-js-sdk/pull/1200)
|
||||
* add export/import of Olm devices
|
||||
[\#1167](https://github.com/matrix-org/matrix-js-sdk/pull/1167)
|
||||
* Convert utils.js -> utils.ts
|
||||
[\#1199](https://github.com/matrix-org/matrix-js-sdk/pull/1199)
|
||||
* Don't sign ourselves as a user
|
||||
[\#1197](https://github.com/matrix-org/matrix-js-sdk/pull/1197)
|
||||
* Add a bunch of logging to verification
|
||||
[\#1196](https://github.com/matrix-org/matrix-js-sdk/pull/1196)
|
||||
* Fix: always return a valid string from InRoomChannel.getEventType
|
||||
[\#1198](https://github.com/matrix-org/matrix-js-sdk/pull/1198)
|
||||
* add logging when a request is being cancelled
|
||||
[\#1195](https://github.com/matrix-org/matrix-js-sdk/pull/1195)
|
||||
* Don't explode verification validation if we don't have an event type
|
||||
[\#1194](https://github.com/matrix-org/matrix-js-sdk/pull/1194)
|
||||
* Fix: verification request appearing for users that are not the receiver or
|
||||
sender if they are in room
|
||||
[\#1193](https://github.com/matrix-org/matrix-js-sdk/pull/1193)
|
||||
* Fix getting secrets encoded with passthrough keys
|
||||
[\#1192](https://github.com/matrix-org/matrix-js-sdk/pull/1192)
|
||||
* Update QR code handling for new spec
|
||||
[\#1175](https://github.com/matrix-org/matrix-js-sdk/pull/1175)
|
||||
* Don't add ephemeral events to timeline when peeking
|
||||
[\#1188](https://github.com/matrix-org/matrix-js-sdk/pull/1188)
|
||||
* Fix typo
|
||||
[\#1189](https://github.com/matrix-org/matrix-js-sdk/pull/1189)
|
||||
* Verification: resolve race between .start events from both parties
|
||||
[\#1187](https://github.com/matrix-org/matrix-js-sdk/pull/1187)
|
||||
* Add option to bootstrap to start new key backup
|
||||
[\#1184](https://github.com/matrix-org/matrix-js-sdk/pull/1184)
|
||||
* Add a bunch of null guards to feature checks
|
||||
[\#1182](https://github.com/matrix-org/matrix-js-sdk/pull/1182)
|
||||
* docs: fix MatrixClient reference
|
||||
[\#1183](https://github.com/matrix-org/matrix-js-sdk/pull/1183)
|
||||
* Add helper to obtain the cancellation code for a verification request
|
||||
[\#1180](https://github.com/matrix-org/matrix-js-sdk/pull/1180)
|
||||
* Publish pre-releases as a separate tag on npm
|
||||
[\#1178](https://github.com/matrix-org/matrix-js-sdk/pull/1178)
|
||||
* Fix support for passthrough keys
|
||||
[\#1177](https://github.com/matrix-org/matrix-js-sdk/pull/1177)
|
||||
* Trust our own cross-signing keys if we verify them with another device
|
||||
[\#1174](https://github.com/matrix-org/matrix-js-sdk/pull/1174)
|
||||
* Ensure cross-signing keys are downloaded when checking trust
|
||||
[\#1176](https://github.com/matrix-org/matrix-js-sdk/pull/1176)
|
||||
* Don't log verification validation errors for normal messages
|
||||
[\#1172](https://github.com/matrix-org/matrix-js-sdk/pull/1172)
|
||||
* Fix bootstrap cleanup
|
||||
[\#1173](https://github.com/matrix-org/matrix-js-sdk/pull/1173)
|
||||
* QR code verification
|
||||
[\#1155](https://github.com/matrix-org/matrix-js-sdk/pull/1155)
|
||||
* expose deviceId prop on device channel
|
||||
[\#1171](https://github.com/matrix-org/matrix-js-sdk/pull/1171)
|
||||
* Move & upgrade babel runtime into dependencies (like it wants)
|
||||
[\#1169](https://github.com/matrix-org/matrix-js-sdk/pull/1169)
|
||||
* Add unit tests for verifying your own device, remove .event property on
|
||||
verification request
|
||||
[\#1166](https://github.com/matrix-org/matrix-js-sdk/pull/1166)
|
||||
* For dm-verification, also consider events sent by other devices of same user
|
||||
as "our" events
|
||||
[\#1163](https://github.com/matrix-org/matrix-js-sdk/pull/1163)
|
||||
* Add a prepare script
|
||||
[\#1161](https://github.com/matrix-org/matrix-js-sdk/pull/1161)
|
||||
* Remove :deviceId from /keys/upload/:deviceId as not spec-compliant
|
||||
[\#1162](https://github.com/matrix-org/matrix-js-sdk/pull/1162)
|
||||
* Refactor and expose some logic publicly for the TimelineWindow class.
|
||||
[\#1159](https://github.com/matrix-org/matrix-js-sdk/pull/1159)
|
||||
* Allow a device key upload request without auth
|
||||
[\#1158](https://github.com/matrix-org/matrix-js-sdk/pull/1158)
|
||||
* Support for .ready verification event (MSC2366) & other things
|
||||
[\#1140](https://github.com/matrix-org/matrix-js-sdk/pull/1140)
|
||||
|
||||
Changes in [4.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v4.0.0) (2020-01-27)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v4.0.0-rc.1...v4.0.0)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
olm.js
|
||||
olm.wasm
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../../dist/browser-matrix.js
|
||||
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Test Crypto in Browser</title>
|
||||
<script src="lib/olm.js"></script>
|
||||
<script src="lib/matrix.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Testing export/import of Olm devices in the browser</h1>
|
||||
<ul>
|
||||
<li>
|
||||
Make sure you built the current version of the Matrix JS SDK
|
||||
(<code>yarn build</code>)
|
||||
</li>
|
||||
<li>
|
||||
copy <code>olm.js</code> and <code>olm.wasm</code>
|
||||
from a recent release of Olm (was tested with version 3.1.4)
|
||||
in directory <code>lib/</code>
|
||||
</li>
|
||||
<li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li>
|
||||
<li>Serve this HTML file (e.g. <code>python3 -m http.server</code>) and go to it through your browser</li>
|
||||
<li>
|
||||
in the JS console, do:
|
||||
<pre>
|
||||
aliceMatrixClient = await newMatrixClient("alice-"+randomHex());
|
||||
await aliceMatrixClient.exportDevice();
|
||||
await aliceMatrixClient.getAccessToken();
|
||||
</pre>
|
||||
</li>
|
||||
<li>
|
||||
copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere
|
||||
(<strong>not</strong> in a JS variable as it will be destroyed when you refresh the page)
|
||||
</li>
|
||||
<li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li>
|
||||
<li>
|
||||
Do the following, replacing <code>ALICE_ID</code>
|
||||
with the user ID of Alice (you can find it in the exported data)
|
||||
<pre>
|
||||
bobMatrixClient = await newMatrixClient("bob-"+randomHex());
|
||||
roomId = await bobMatrixClient.createEncryptedRoom([ALICE_ID]);
|
||||
await bobMatrixClient.sendTextMessage('Hi Alice!', roomId);
|
||||
</pre>
|
||||
</li>
|
||||
<li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li>
|
||||
<li>
|
||||
Now do the following, using the exported data and the access token you saved previously:
|
||||
<pre>
|
||||
aliceMatrixClient = await importMatrixClient(EXPORTED_DATA, ACCESS_TOKEN);
|
||||
</pre>
|
||||
</li>
|
||||
<li>You should see the message sent by Bob printed in the console.</li>
|
||||
</ul>
|
||||
|
||||
<script src="olm-device-export-import.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,122 @@
|
||||
if (!Olm) {
|
||||
console.error(
|
||||
"global.Olm does not seem to be present."
|
||||
+ " Did you forget to add olm in the lib/ directory?"
|
||||
);
|
||||
}
|
||||
|
||||
const BASE_URL = 'http://localhost:8008';
|
||||
const ROOM_CRYPTO_CONFIG = { algorithm: 'm.megolm.v1.aes-sha2' };
|
||||
const PASSWORD = 'password';
|
||||
|
||||
// useful to create new usernames
|
||||
window.randomHex = () => Math.floor(Math.random() * (10**6)).toString(16);
|
||||
|
||||
window.newMatrixClient = async function (username) {
|
||||
const registrationClient = matrixcs.createClient(BASE_URL);
|
||||
|
||||
const userRegisterResult = await registrationClient.register(
|
||||
username,
|
||||
PASSWORD,
|
||||
null,
|
||||
{ type: 'm.login.dummy' }
|
||||
);
|
||||
|
||||
const matrixClient = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
userId: userRegisterResult.user_id,
|
||||
accessToken: userRegisterResult.access_token,
|
||||
deviceId: userRegisterResult.device_id,
|
||||
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
|
||||
cryptoStore: new matrixcs.MemoryCryptoStore(),
|
||||
});
|
||||
|
||||
extendMatrixClient(matrixClient);
|
||||
|
||||
await matrixClient.initCrypto();
|
||||
await matrixClient.startClient();
|
||||
return matrixClient;
|
||||
}
|
||||
|
||||
window.importMatrixClient = async function (exportedDevice, accessToken) {
|
||||
const matrixClient = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
deviceToImport: exportedDevice,
|
||||
accessToken,
|
||||
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
|
||||
cryptoStore: new matrixcs.MemoryCryptoStore(),
|
||||
});
|
||||
|
||||
extendMatrixClient(matrixClient);
|
||||
|
||||
await matrixClient.initCrypto();
|
||||
await matrixClient.startClient();
|
||||
return matrixClient;
|
||||
}
|
||||
|
||||
function extendMatrixClient(matrixClient) {
|
||||
// automatic join
|
||||
matrixClient.on('RoomMember.membership', async (event, member) => {
|
||||
if (member.membership === 'invite' && member.userId === matrixClient.getUserId()) {
|
||||
await matrixClient.joinRoom(member.roomId);
|
||||
// setting up of room encryption seems to be triggered automatically
|
||||
// but if we don't wait for it the first messages we send are unencrypted
|
||||
await matrixClient.setRoomEncryption(member.roomId, { algorithm: 'm.megolm.v1.aes-sha2' })
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.onDecryptedMessage = message => {
|
||||
console.log('Got encrypted message: ', message);
|
||||
}
|
||||
|
||||
matrixClient.on('Event.decrypted', (event) => {
|
||||
if (event.getType() === 'm.room.message'){
|
||||
matrixClient.onDecryptedMessage(event.getContent().body);
|
||||
} else {
|
||||
console.log('decrypted an event of type', event.getType());
|
||||
console.log(event);
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.createEncryptedRoom = async function(usersToInvite) {
|
||||
const {
|
||||
room_id: roomId,
|
||||
} = await this.createRoom({
|
||||
visibility: 'private',
|
||||
invite: usersToInvite,
|
||||
});
|
||||
|
||||
// matrixClient.setRoomEncryption() only updates local state
|
||||
// but does not send anything to the server
|
||||
// (see https://github.com/matrix-org/matrix-js-sdk/issues/905)
|
||||
// so we do it ourselves with 'sendStateEvent'
|
||||
await this.sendStateEvent(
|
||||
roomId, 'm.room.encryption', ROOM_CRYPTO_CONFIG,
|
||||
);
|
||||
await this.setRoomEncryption(
|
||||
roomId, ROOM_CRYPTO_CONFIG,
|
||||
);
|
||||
|
||||
// Marking all devices as verified
|
||||
let room = this.getRoom(roomId);
|
||||
let members = (await room.getEncryptionTargetMembers()).map(x => x["userId"])
|
||||
let memberkeys = await this.downloadKeys(members);
|
||||
for (const userId in memberkeys) {
|
||||
for (const deviceId in memberkeys[userId]) {
|
||||
await this.setDeviceVerified(userId, deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
return roomId;
|
||||
}
|
||||
|
||||
matrixClient.sendTextMessage = async function(message, roomId) {
|
||||
return matrixClient.sendMessage(
|
||||
roomId,
|
||||
{
|
||||
body: message,
|
||||
msgtype: 'm.text',
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "4.0.0",
|
||||
"version": "5.0.0-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
|
||||
+6
-1
@@ -294,9 +294,14 @@ fi
|
||||
rm "${release_text}"
|
||||
rm "${latest_changes}"
|
||||
|
||||
npm_publish_flags=''
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
# Tag prereleases as `next` so the last stable release remains the default
|
||||
npm_publish_flags='--tag next'
|
||||
fi
|
||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
||||
# defined options and semantics than `yarn` for writing to the registry.
|
||||
npm publish
|
||||
npm publish $npm_publish_flags
|
||||
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
echo "generating jsdocs"
|
||||
|
||||
@@ -41,7 +41,7 @@ async function setupSession(initiator, opponent) {
|
||||
return sid;
|
||||
}
|
||||
|
||||
describe("OlmDecryption", function() {
|
||||
describe("OlmDevice", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running megolm unit tests: libolm not present');
|
||||
return;
|
||||
@@ -81,6 +81,60 @@ describe("OlmDecryption", function() {
|
||||
);
|
||||
});
|
||||
|
||||
it('exports picked account and olm sessions', async function() {
|
||||
const sessionId = await setupSession(aliceOlmDevice, bobOlmDevice);
|
||||
|
||||
const exported = await bobOlmDevice.export();
|
||||
// At this moment only Alice (the “initiator” in setupSession) has a session
|
||||
expect(exported.sessions).toEqual([]);
|
||||
|
||||
const MESSAGE = (
|
||||
"The olm or proteus is an aquatic salamander"
|
||||
+ " in the family Proteidae"
|
||||
);
|
||||
const ciphertext = await aliceOlmDevice.encryptMessage(
|
||||
bobOlmDevice.deviceCurve25519Key,
|
||||
sessionId,
|
||||
MESSAGE,
|
||||
);
|
||||
|
||||
const bobRecreatedOlmDevice = makeOlmDevice();
|
||||
bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
|
||||
|
||||
const decrypted = await bobRecreatedOlmDevice.createInboundSession(
|
||||
aliceOlmDevice.deviceCurve25519Key,
|
||||
ciphertext.type,
|
||||
ciphertext.body,
|
||||
);
|
||||
expect(decrypted.payload).toEqual(MESSAGE);
|
||||
|
||||
const exportedAgain = await bobRecreatedOlmDevice.export();
|
||||
// this time we expect Bob to have a session to export
|
||||
expect(exportedAgain.sessions).toHaveLength(1);
|
||||
|
||||
const MESSAGE_2 = (
|
||||
"In contrast to most amphibians,"
|
||||
+ " the olm is entirely aquatic"
|
||||
);
|
||||
const ciphertext2 = await aliceOlmDevice.encryptMessage(
|
||||
bobOlmDevice.deviceCurve25519Key,
|
||||
sessionId,
|
||||
MESSAGE_2,
|
||||
);
|
||||
|
||||
const bobRecreatedAgainOlmDevice = makeOlmDevice();
|
||||
bobRecreatedAgainOlmDevice.init({ fromExportedDevice: exportedAgain });
|
||||
|
||||
// Note: "decrypted_2" does not have the same structure as "decrypted"
|
||||
const decrypted2 = await bobRecreatedAgainOlmDevice.decryptMessage(
|
||||
aliceOlmDevice.deviceCurve25519Key,
|
||||
decrypted.session_id,
|
||||
ciphertext2.type,
|
||||
ciphertext2.body,
|
||||
);
|
||||
expect(decrypted2).toEqual(MESSAGE_2);
|
||||
});
|
||||
|
||||
it("creates only one session at a time", async function() {
|
||||
// if we call ensureOlmSessionsForDevices multiple times, it should
|
||||
// only try to create one session at a time, even if the server is
|
||||
|
||||
@@ -174,7 +174,7 @@ describe("Cross Signing", function() {
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
path: "/keys/upload",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
@@ -237,7 +237,7 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
path: "/keys/upload",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
@@ -429,7 +429,7 @@ describe("Cross Signing", function() {
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
path: "/keys/upload",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
@@ -487,7 +487,7 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
path: "/keys/upload",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
|
||||
@@ -34,6 +34,9 @@ async function makeTestClient(userInfo, options) {
|
||||
|
||||
await client.initCrypto();
|
||||
|
||||
// No need to download keys for these tests
|
||||
client._crypto.downloadKeys = async function() {};
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import {InRoomChannel} from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
|
||||
describe("InRoomChannel tests", function() {
|
||||
const ALICE = "@alice:hs.tld";
|
||||
const BOB = "@bob:hs.tld";
|
||||
const MALORY = "@malory:hs.tld";
|
||||
const client = {
|
||||
getUserId() { return ALICE; },
|
||||
};
|
||||
|
||||
it("getEventType only returns .request for a message with a msgtype", function() {
|
||||
const invalidEvent = new MatrixEvent({
|
||||
type: "m.key.verification.request",
|
||||
});
|
||||
expect(InRoomChannel.getEventType(invalidEvent)).toStrictEqual("");
|
||||
const validEvent = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: { msgtype: "m.key.verification.request" },
|
||||
});
|
||||
expect(InRoomChannel.getEventType(validEvent)).
|
||||
toStrictEqual("m.key.verification.request");
|
||||
const validFooEvent = new MatrixEvent({ type: "m.foo" });
|
||||
expect(InRoomChannel.getEventType(validFooEvent)).
|
||||
toStrictEqual("m.foo");
|
||||
});
|
||||
|
||||
it("getEventType should return m.room.message for messages", function() {
|
||||
const messageEvent = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: { msgtype: "m.text" },
|
||||
});
|
||||
// XXX: The event type doesn't matter too much, just as long as it's not a verification event
|
||||
expect(InRoomChannel.getEventType(messageEvent)).
|
||||
toStrictEqual("m.room.message");
|
||||
});
|
||||
|
||||
it("getEventType should return actual type for non-message events", function() {
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.member",
|
||||
content: { },
|
||||
});
|
||||
expect(InRoomChannel.getEventType(event)).
|
||||
toStrictEqual("m.room.member");
|
||||
});
|
||||
|
||||
it("getOtherPartyUserId should not return anything for a request not " +
|
||||
"directed at me", function() {
|
||||
const event = new MatrixEvent({
|
||||
sender: BOB,
|
||||
type: "m.room.message",
|
||||
content: { msgtype: "m.key.verification.request", to: MALORY },
|
||||
});
|
||||
expect(InRoomChannel.getOtherPartyUserId(event, client)).toStrictEqual(undefined);
|
||||
});
|
||||
|
||||
it("getOtherPartyUserId should not return anything an event that is not of a valid " +
|
||||
"request type", function() {
|
||||
// invalid because this should be a room message with msgtype
|
||||
const invalidRequest = new MatrixEvent({
|
||||
sender: BOB,
|
||||
type: "m.key.verification.request",
|
||||
content: { to: ALICE },
|
||||
});
|
||||
expect(InRoomChannel.getOtherPartyUserId(invalidRequest, client))
|
||||
.toStrictEqual(undefined);
|
||||
const startEvent = new MatrixEvent({
|
||||
sender: BOB,
|
||||
type: "m.key.verification.start",
|
||||
content: { to: ALICE },
|
||||
});
|
||||
expect(InRoomChannel.getOtherPartyUserId(startEvent, client))
|
||||
.toStrictEqual(undefined);
|
||||
const fooEvent = new MatrixEvent({
|
||||
sender: BOB,
|
||||
type: "m.foo",
|
||||
content: { to: ALICE },
|
||||
});
|
||||
expect(InRoomChannel.getOtherPartyUserId(fooEvent, client))
|
||||
.toStrictEqual(undefined);
|
||||
});
|
||||
});
|
||||
@@ -16,8 +16,6 @@ limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
|
||||
import {ScanQRCode, ShowQRCode} from "../../../../src/crypto/verification/QRCode";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -31,124 +29,13 @@ describe("QR code verification", function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
describe("showing", function() {
|
||||
it("should emit an event to show a QR code", async function() {
|
||||
const channel = {
|
||||
send: jest.fn(),
|
||||
};
|
||||
const qrCode = new ShowQRCode(channel, {
|
||||
getUserId: () => "@alice:example.com",
|
||||
deviceId: "ABCDEFG",
|
||||
getDeviceEd25519Key: function() {
|
||||
return "device+ed25519+key";
|
||||
},
|
||||
});
|
||||
const spy = jest.fn((e) => {
|
||||
qrCode.done();
|
||||
});
|
||||
qrCode.on("show_qr_code", spy);
|
||||
await qrCode.verify();
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
url: "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
|
||||
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("scanning", function() {
|
||||
const QR_CODE_URL = "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
|
||||
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey";
|
||||
it("should verify when a QR code is sent", async function() {
|
||||
const device = DeviceInfo.fromStorage(
|
||||
{
|
||||
algorithms: [],
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "device+curve25519+key",
|
||||
"ed25519:ABCDEFG": "device+ed25519+key",
|
||||
},
|
||||
verified: false,
|
||||
known: false,
|
||||
unsigned: {},
|
||||
},
|
||||
"ABCDEFG",
|
||||
);
|
||||
const client = {
|
||||
getStoredDevice: jest.fn().mockReturnValue(device),
|
||||
setDeviceVerified: jest.fn(),
|
||||
};
|
||||
const channel = {
|
||||
send: jest.fn(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(channel, client);
|
||||
qrCode.on("confirm_user_id", ({userId, confirm}) => {
|
||||
if (userId === "@alice:example.com") {
|
||||
confirm();
|
||||
} else {
|
||||
qrCode.cancel(new Error("Incorrect user"));
|
||||
}
|
||||
});
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
await qrCode.verify();
|
||||
expect(client.getStoredDevice)
|
||||
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
|
||||
expect(client.setDeviceVerified)
|
||||
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
|
||||
});
|
||||
|
||||
it("should error when the user ID doesn't match", async function() {
|
||||
const client = {
|
||||
getStoredDevice: jest.fn(),
|
||||
setDeviceVerified: jest.fn(),
|
||||
};
|
||||
const channel = {
|
||||
send: jest.fn(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(channel, client, "@bob:example.com", "ABCDEFG");
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
const spy = jest.fn();
|
||||
await qrCode.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(channel.send).toHaveBeenCalled();
|
||||
expect(client.getStoredDevice).not.toHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should error if the key doesn't match", async function() {
|
||||
const device = DeviceInfo.fromStorage(
|
||||
{
|
||||
algorithms: [],
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "device+curve25519+key",
|
||||
"ed25519:ABCDEFG": "a+different+device+ed25519+key",
|
||||
},
|
||||
verified: false,
|
||||
known: false,
|
||||
unsigned: {},
|
||||
},
|
||||
"ABCDEFG",
|
||||
);
|
||||
const client = {
|
||||
getStoredDevice: jest.fn().mockReturnValue(device),
|
||||
setDeviceVerified: jest.fn(),
|
||||
};
|
||||
const channel = {
|
||||
send: jest.fn(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(
|
||||
channel, client, "@alice:example.com", "ABCDEFG");
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
const spy = jest.fn();
|
||||
await qrCode.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(channel.send).toHaveBeenCalled();
|
||||
expect(client.getStoredDevice).toHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).not.toHaveBeenCalled();
|
||||
describe("reciprocate", () => {
|
||||
it("should verify the secret", () => {
|
||||
// TODO: Actually write a test for this.
|
||||
// Tests are hard because we are running before the verification
|
||||
// process actually begins, and are largely UI-driven rather than
|
||||
// logic-driven (compared to something like SAS). In the interest
|
||||
// of time, tests are currently excluded.
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,20 +18,27 @@ import "../../../olm-loader";
|
||||
import {verificationMethods} from "../../../../src/crypto";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {SAS} from "../../../../src/crypto/verification/SAS";
|
||||
import {makeTestClients} from './util';
|
||||
import {makeTestClients, setupWebcrypto, teardownWebcrypto} from './util';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("verification request", function() {
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("verification request integration tests with crypto layer", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running device verification unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("should request and accept a verification", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
@@ -64,7 +71,9 @@ describe("verification request", function() {
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
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);
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
|
||||
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import {makeTestClients} from './util';
|
||||
import {makeTestClients, setupWebcrypto, teardownWebcrypto} from './util';
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import {SAS} from "../../../../src/crypto/verification/SAS";
|
||||
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
|
||||
@@ -35,9 +35,14 @@ describe("SAS verification", function() {
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("should error on an unexpected event", async function() {
|
||||
const sas = new SAS({}, "@alice:example.com", "ABCDEFG");
|
||||
sas.handleEvent(new MatrixEvent({
|
||||
@@ -442,9 +447,11 @@ describe("SAS verification", function() {
|
||||
});
|
||||
});
|
||||
|
||||
aliceVerifier = await alice.client.requestVerificationDM(
|
||||
bob.client.getUserId(), "!room_id", [verificationMethods.SAS],
|
||||
const aliceRequest = await alice.client.requestVerificationDM(
|
||||
bob.client.getUserId(), "!room_id",
|
||||
);
|
||||
await aliceRequest.waitFor(r => r.started);
|
||||
aliceVerifier = aliceRequest.verifier;
|
||||
aliceVerifier.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
|
||||
import {TestClient} from '../../../TestClient';
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import nodeCrypto from "crypto";
|
||||
|
||||
export async function makeTestClients(userInfos, options) {
|
||||
const clients = [];
|
||||
@@ -33,15 +34,13 @@ export async function makeTestClients(userInfos, options) {
|
||||
content: msg,
|
||||
});
|
||||
const client = clientMap[userId][deviceId];
|
||||
if (event.isEncrypted()) {
|
||||
event.attemptDecryption(client._crypto)
|
||||
.then(() => client.emit("toDeviceEvent", event));
|
||||
} else {
|
||||
setTimeout(
|
||||
() => client.emit("toDeviceEvent", event),
|
||||
0,
|
||||
);
|
||||
}
|
||||
const decryptionPromise = event.isEncrypted() ?
|
||||
event.attemptDecryption(client._crypto) :
|
||||
Promise.resolve();
|
||||
|
||||
decryptionPromise.then(
|
||||
() => client.emit("toDeviceEvent", event),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,21 +49,33 @@ export async function makeTestClients(userInfos, options) {
|
||||
const sendEvent = function(room, type, content) {
|
||||
// make up a unique ID as the event ID
|
||||
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
|
||||
type: type,
|
||||
content: content,
|
||||
room_id: room,
|
||||
event_id: eventId,
|
||||
});
|
||||
for (const tc of clients) {
|
||||
setTimeout(
|
||||
() => tc.client.emit("Room.timeline", event),
|
||||
0,
|
||||
);
|
||||
}
|
||||
origin_server_ts: Date.now(),
|
||||
};
|
||||
const event = new MatrixEvent(rawEvent);
|
||||
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
|
||||
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) {
|
||||
@@ -92,3 +103,15 @@ export async function makeTestClients(userInfos, options) {
|
||||
|
||||
return clients;
|
||||
}
|
||||
|
||||
export function setupWebcrypto() {
|
||||
global.crypto = {
|
||||
getRandomValues: (buf) => {
|
||||
return nodeCrypto.randomFillSync(buf);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function teardownWebcrypto() {
|
||||
global.crypto = undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import {VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE} from
|
||||
"../../../../src/crypto/verification/request/VerificationRequest";
|
||||
import {InRoomChannel} from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
import {ToDeviceChannel} from
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import {setupWebcrypto, teardownWebcrypto} from "./util";
|
||||
|
||||
function makeMockClient(userId, deviceId) {
|
||||
let counter = 1;
|
||||
let events = [];
|
||||
const deviceEvents = {};
|
||||
return {
|
||||
getUserId() { return userId; },
|
||||
getDeviceId() { return deviceId; },
|
||||
|
||||
sendEvent(roomId, type, content) {
|
||||
counter = counter + 1;
|
||||
const eventId = `$${userId}-${deviceId}-${counter}`;
|
||||
events.push(new MatrixEvent({
|
||||
sender: userId,
|
||||
event_id: eventId,
|
||||
room_id: roomId,
|
||||
type,
|
||||
content,
|
||||
origin_server_ts: Date.now(),
|
||||
}));
|
||||
return Promise.resolve({event_id: eventId});
|
||||
},
|
||||
|
||||
sendToDevice(type, msgMap) {
|
||||
for (const userId of Object.keys(msgMap)) {
|
||||
const deviceMap = msgMap[userId];
|
||||
for (const deviceId of Object.keys(deviceMap)) {
|
||||
const content = deviceMap[deviceId];
|
||||
const event = new MatrixEvent({content, type});
|
||||
deviceEvents[userId] = deviceEvents[userId] || {};
|
||||
deviceEvents[userId][deviceId] = deviceEvents[userId][deviceId] || [];
|
||||
deviceEvents[userId][deviceId].push(event);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
popEvents() {
|
||||
const e = events;
|
||||
events = [];
|
||||
return e;
|
||||
},
|
||||
|
||||
popDeviceEvents(userId, deviceId) {
|
||||
const forDevice = deviceEvents[userId];
|
||||
const events = forDevice && forDevice[deviceId];
|
||||
const result = events || [];
|
||||
if (events) {
|
||||
delete forDevice[deviceId];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const MOCK_METHOD = "mock-verify";
|
||||
class MockVerifier {
|
||||
constructor(channel, client, userId, deviceId, startEvent) {
|
||||
this._channel = channel;
|
||||
this._startEvent = startEvent;
|
||||
}
|
||||
|
||||
get events() {
|
||||
return [DONE_TYPE];
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this._startEvent) {
|
||||
await this._channel.send(DONE_TYPE, {});
|
||||
} else {
|
||||
await this._channel.send(START_TYPE, {method: MOCK_METHOD});
|
||||
}
|
||||
}
|
||||
|
||||
async handleEvent(event) {
|
||||
if (event.getType() === DONE_TYPE && !this._startEvent) {
|
||||
await this._channel.send(DONE_TYPE, {});
|
||||
}
|
||||
}
|
||||
|
||||
canSwitchStartEvent() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function makeRemoteEcho(event) {
|
||||
return new MatrixEvent(Object.assign({}, event.event, {
|
||||
unsigned: {
|
||||
transaction_id: "abc",
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async function distributeEvent(ownRequest, theirRequest, event) {
|
||||
await ownRequest.channel.handleEvent(
|
||||
makeRemoteEcho(event), ownRequest, true);
|
||||
await theirRequest.channel.handleEvent(event, theirRequest, true);
|
||||
}
|
||||
|
||||
describe("verification request unit tests", function() {
|
||||
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("transition from UNSENT to DONE through happy path", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), alice);
|
||||
const bobRequest = new VerificationRequest(
|
||||
new InRoomChannel(bob, "!room"),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), bob);
|
||||
expect(aliceRequest.invalid).toBe(true);
|
||||
expect(bobRequest.invalid).toBe(true);
|
||||
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
expect(requestEvent.getType()).toBe("m.room.message");
|
||||
await distributeEvent(aliceRequest, bobRequest, requestEvent);
|
||||
expect(aliceRequest.requested).toBe(true);
|
||||
expect(bobRequest.requested).toBe(true);
|
||||
|
||||
await bobRequest.accept();
|
||||
const [readyEvent] = bob.popEvents();
|
||||
expect(readyEvent.getType()).toBe(READY_TYPE);
|
||||
await distributeEvent(bobRequest, aliceRequest, readyEvent);
|
||||
expect(bobRequest.ready).toBe(true);
|
||||
expect(aliceRequest.ready).toBe(true);
|
||||
|
||||
const verifier = aliceRequest.beginKeyVerification(MOCK_METHOD);
|
||||
await verifier.start();
|
||||
const [startEvent] = alice.popEvents();
|
||||
expect(startEvent.getType()).toBe(START_TYPE);
|
||||
await distributeEvent(aliceRequest, bobRequest, startEvent);
|
||||
expect(aliceRequest.started).toBe(true);
|
||||
expect(aliceRequest.verifier).toBeInstanceOf(MockVerifier);
|
||||
expect(bobRequest.started).toBe(true);
|
||||
expect(bobRequest.verifier).toBeInstanceOf(MockVerifier);
|
||||
|
||||
await bobRequest.verifier.start();
|
||||
const [bobDoneEvent] = bob.popEvents();
|
||||
expect(bobDoneEvent.getType()).toBe(DONE_TYPE);
|
||||
await distributeEvent(bobRequest, aliceRequest, bobDoneEvent);
|
||||
const [aliceDoneEvent] = alice.popEvents();
|
||||
expect(aliceDoneEvent.getType()).toBe(DONE_TYPE);
|
||||
await distributeEvent(aliceRequest, bobRequest, aliceDoneEvent);
|
||||
expect(aliceRequest.done).toBe(true);
|
||||
expect(bobRequest.done).toBe(true);
|
||||
});
|
||||
|
||||
it("methods only contains common methods", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()),
|
||||
new Map([["c", function() {}], ["a", function() {}]]), alice);
|
||||
const bobRequest = new VerificationRequest(
|
||||
new InRoomChannel(bob, "!room"),
|
||||
new Map([["c", function() {}], ["b", function() {}]]), bob);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
await distributeEvent(aliceRequest, bobRequest, requestEvent);
|
||||
await bobRequest.accept();
|
||||
const [readyEvent] = bob.popEvents();
|
||||
await distributeEvent(bobRequest, aliceRequest, readyEvent);
|
||||
expect(aliceRequest.methods).toStrictEqual(["c"]);
|
||||
expect(bobRequest.methods).toStrictEqual(["c"]);
|
||||
});
|
||||
|
||||
it("other client accepting request puts it in observeOnly mode", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob1.getUserId()), new Map(), alice);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
const bob1Request = new VerificationRequest(
|
||||
new InRoomChannel(bob1, "!room"), new Map(), bob1);
|
||||
const bob2Request = new VerificationRequest(
|
||||
new InRoomChannel(bob2, "!room"), new Map(), bob2);
|
||||
|
||||
await bob1Request.channel.handleEvent(requestEvent, bob1Request, true);
|
||||
await bob2Request.channel.handleEvent(requestEvent, bob2Request, true);
|
||||
|
||||
await bob1Request.accept();
|
||||
const [readyEvent] = bob1.popEvents();
|
||||
expect(bob2Request.observeOnly).toBe(false);
|
||||
await bob2Request.channel.handleEvent(readyEvent, bob2Request, true);
|
||||
expect(bob2Request.observeOnly).toBe(true);
|
||||
});
|
||||
|
||||
it("verify own device with to_device messages", async function() {
|
||||
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
|
||||
const bob1Request = new VerificationRequest(
|
||||
new ToDeviceChannel(bob1, bob1.getUserId(), ["device1", "device2"],
|
||||
ToDeviceChannel.makeTransactionId(), "device2"),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), bob1);
|
||||
const to = {userId: "@bob:matrix.tld", deviceId: "device2"};
|
||||
const verifier = bob1Request.beginKeyVerification(MOCK_METHOD, to);
|
||||
expect(verifier).toBeInstanceOf(MockVerifier);
|
||||
await verifier.start();
|
||||
const [startEvent] = bob1.popDeviceEvents(to.userId, to.deviceId);
|
||||
expect(startEvent.getType()).toBe(START_TYPE);
|
||||
const bob2Request = new VerificationRequest(
|
||||
new ToDeviceChannel(bob2, bob2.getUserId(), ["device1"]),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), bob2);
|
||||
|
||||
await bob2Request.channel.handleEvent(startEvent, bob2Request, true);
|
||||
await bob2Request.verifier.start();
|
||||
const [doneEvent1] = bob2.popDeviceEvents("@bob:matrix.tld", "device1");
|
||||
expect(doneEvent1.getType()).toBe(DONE_TYPE);
|
||||
await bob1Request.channel.handleEvent(doneEvent1, bob1Request, true);
|
||||
const [doneEvent2] = bob1.popDeviceEvents("@bob:matrix.tld", "device2");
|
||||
expect(doneEvent2.getType()).toBe(DONE_TYPE);
|
||||
await bob2Request.channel.handleEvent(doneEvent2, bob2Request, true);
|
||||
|
||||
expect(bob1Request.done).toBe(true);
|
||||
expect(bob2Request.done).toBe(true);
|
||||
});
|
||||
});
|
||||
+97
-108
@@ -172,7 +172,7 @@ MatrixBaseApis.prototype.makeTxnId = function() {
|
||||
* Check whether a username is available prior to registration. An error response
|
||||
* indicates an invalid/unavailable username.
|
||||
* @param {string} username The username to check the availability of.
|
||||
* @return {module:client.Promise} Resolves: to `true`.
|
||||
* @return {Promise} Resolves: to `true`.
|
||||
*/
|
||||
MatrixBaseApis.prototype.isUsernameAvailable = function(username) {
|
||||
return this._http.authedRequest(
|
||||
@@ -193,7 +193,7 @@ MatrixBaseApis.prototype.isUsernameAvailable = function(username) {
|
||||
* @param {string} guestAccessToken
|
||||
* @param {string} inhibitLogin
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.register = function(
|
||||
@@ -258,7 +258,7 @@ MatrixBaseApis.prototype.register = function(
|
||||
* @param {Object=} opts Registration options
|
||||
* @param {Object} opts.body JSON HTTP body to provide.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.registerGuest = function(opts, callback) {
|
||||
@@ -271,7 +271,7 @@ MatrixBaseApis.prototype.registerGuest = function(opts, callback) {
|
||||
* @param {Object} data parameters for registration request
|
||||
* @param {string=} kind type of user to register. may be "guest"
|
||||
* @param {module:client.callback=} callback
|
||||
* @return {module:client.Promise} Resolves: to the /register response
|
||||
* @return {Promise} Resolves: to the /register response
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.registerRequest = function(data, kind, callback) {
|
||||
@@ -287,7 +287,7 @@ MatrixBaseApis.prototype.registerRequest = function(data, kind, callback) {
|
||||
|
||||
/**
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.loginFlows = function(callback) {
|
||||
@@ -298,7 +298,7 @@ MatrixBaseApis.prototype.loginFlows = function(callback) {
|
||||
* @param {string} loginType
|
||||
* @param {Object} data
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.login = function(loginType, data, callback) {
|
||||
@@ -329,7 +329,7 @@ MatrixBaseApis.prototype.login = function(loginType, data, callback) {
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) {
|
||||
@@ -342,7 +342,7 @@ MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback)
|
||||
/**
|
||||
* @param {string} relayState URL Callback after SAML2 Authentication
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.loginWithSAML2 = function(relayState, callback) {
|
||||
@@ -379,7 +379,7 @@ MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType) {
|
||||
/**
|
||||
* @param {string} token Login token previously received from homeserver
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.loginWithToken = function(token, callback) {
|
||||
@@ -396,7 +396,7 @@ MatrixBaseApis.prototype.loginWithToken = function(token, callback) {
|
||||
* it is up to the caller to either reset or destroy the MatrixClient after
|
||||
* this method succeeds.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: On success, the empty object
|
||||
* @return {Promise} Resolves: On success, the empty object
|
||||
*/
|
||||
MatrixBaseApis.prototype.logout = function(callback) {
|
||||
return this._http.authedRequest(
|
||||
@@ -414,7 +414,7 @@ MatrixBaseApis.prototype.logout = function(callback) {
|
||||
* @param {boolean} erase Optional. If set, send as `erase` attribute in the
|
||||
* JSON request body, indicating whether the account should be erased. Defaults
|
||||
* to false.
|
||||
* @return {module:client.Promise} Resolves: On success, the empty object
|
||||
* @return {Promise} Resolves: On success, the empty object
|
||||
*/
|
||||
MatrixBaseApis.prototype.deactivateAccount = function(auth, erase) {
|
||||
if (typeof(erase) === 'function') {
|
||||
@@ -467,7 +467,7 @@ MatrixBaseApis.prototype.getFallbackAuthUrl = function(loginType, authSessionId)
|
||||
* @param {string} options.name The name to give this room.
|
||||
* @param {string} options.topic The topic to give this room.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: <code>{room_id: {string},
|
||||
* @return {Promise} Resolves: <code>{room_id: {string},
|
||||
* room_alias: {string(opt)}}</code>
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
@@ -512,7 +512,7 @@ MatrixBaseApis.prototype.fetchRelations =
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.roomState = function(roomId, callback) {
|
||||
@@ -545,7 +545,7 @@ MatrixBaseApis.prototype.fetchRoomEvent = function(roomId, eventId, callback) {
|
||||
* @param {string} excludeMembership the membership type to exclude from the response
|
||||
* @param {string} atEventId the id of the event for which moment in the timeline the members should be returned for
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: dictionary of userid to profile information
|
||||
* @return {Promise} Resolves: dictionary of userid to profile information
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.members =
|
||||
@@ -572,7 +572,7 @@ function(roomId, includeMembership, excludeMembership, atEventId, callback) {
|
||||
* Upgrades a room to a new protocol version
|
||||
* @param {string} roomId
|
||||
* @param {string} newVersion The target version to upgrade to
|
||||
* @return {module:client.Promise} Resolves: Object with key 'replacement_room'
|
||||
* @return {Promise} Resolves: Object with key 'replacement_room'
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.upgradeRoom = function(roomId, newVersion) {
|
||||
@@ -585,7 +585,7 @@ MatrixBaseApis.prototype.upgradeRoom = function(roomId, newVersion) {
|
||||
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @return {module:client.Promise} Resolves: Group summary object
|
||||
* @return {Promise} Resolves: Group summary object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getGroupSummary = function(groupId) {
|
||||
@@ -595,7 +595,7 @@ MatrixBaseApis.prototype.getGroupSummary = function(groupId) {
|
||||
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @return {module:client.Promise} Resolves: Group profile object
|
||||
* @return {Promise} Resolves: Group profile object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getGroupProfile = function(groupId) {
|
||||
@@ -610,7 +610,7 @@ MatrixBaseApis.prototype.getGroupProfile = function(groupId) {
|
||||
* @param {string=} profile.avatar_url MXC avatar URL
|
||||
* @param {string=} profile.short_description A short description of the room
|
||||
* @param {string=} profile.long_description A longer HTML description of the room
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setGroupProfile = function(groupId, profile) {
|
||||
@@ -626,7 +626,7 @@ MatrixBaseApis.prototype.setGroupProfile = function(groupId, profile) {
|
||||
* least a 'type' field which is 'open' if anyone can join the group
|
||||
* the group without prior approval, or 'invite' if an invite is
|
||||
* required to join.
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setGroupJoinPolicy = function(groupId, policy) {
|
||||
@@ -643,7 +643,7 @@ MatrixBaseApis.prototype.setGroupJoinPolicy = function(groupId, policy) {
|
||||
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @return {module:client.Promise} Resolves: Group users list object
|
||||
* @return {Promise} Resolves: Group users list object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getGroupUsers = function(groupId) {
|
||||
@@ -653,7 +653,7 @@ MatrixBaseApis.prototype.getGroupUsers = function(groupId) {
|
||||
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @return {module:client.Promise} Resolves: Group users list object
|
||||
* @return {Promise} Resolves: Group users list object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getGroupInvitedUsers = function(groupId) {
|
||||
@@ -663,7 +663,7 @@ MatrixBaseApis.prototype.getGroupInvitedUsers = function(groupId) {
|
||||
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @return {module:client.Promise} Resolves: Group rooms list object
|
||||
* @return {Promise} Resolves: Group rooms list object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getGroupRooms = function(groupId) {
|
||||
@@ -674,7 +674,7 @@ MatrixBaseApis.prototype.getGroupRooms = function(groupId) {
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @param {string} userId
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.inviteUserToGroup = function(groupId, userId) {
|
||||
@@ -688,7 +688,7 @@ MatrixBaseApis.prototype.inviteUserToGroup = function(groupId, userId) {
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @param {string} userId
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.removeUserFromGroup = function(groupId, userId) {
|
||||
@@ -703,7 +703,7 @@ MatrixBaseApis.prototype.removeUserFromGroup = function(groupId, userId) {
|
||||
* @param {string} groupId
|
||||
* @param {string} userId
|
||||
* @param {string} roleId Optional.
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.addUserToGroupSummary = function(groupId, userId, roleId) {
|
||||
@@ -719,7 +719,7 @@ MatrixBaseApis.prototype.addUserToGroupSummary = function(groupId, userId, roleI
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @param {string} userId
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.removeUserFromGroupSummary = function(groupId, userId) {
|
||||
@@ -734,7 +734,7 @@ MatrixBaseApis.prototype.removeUserFromGroupSummary = function(groupId, userId)
|
||||
* @param {string} groupId
|
||||
* @param {string} roomId
|
||||
* @param {string} categoryId Optional.
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.addRoomToGroupSummary = function(groupId, roomId, categoryId) {
|
||||
@@ -750,7 +750,7 @@ MatrixBaseApis.prototype.addRoomToGroupSummary = function(groupId, roomId, categ
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @param {string} roomId
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.removeRoomFromGroupSummary = function(groupId, roomId) {
|
||||
@@ -765,7 +765,7 @@ MatrixBaseApis.prototype.removeRoomFromGroupSummary = function(groupId, roomId)
|
||||
* @param {string} groupId
|
||||
* @param {string} roomId
|
||||
* @param {bool} isPublic Whether the room-group association is visible to non-members
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.addRoomToGroup = function(groupId, roomId, isPublic) {
|
||||
@@ -786,7 +786,7 @@ MatrixBaseApis.prototype.addRoomToGroup = function(groupId, roomId, isPublic) {
|
||||
* @param {string} groupId
|
||||
* @param {string} roomId
|
||||
* @param {bool} isPublic Whether the room-group association is visible to non-members
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.updateGroupRoomVisibility = function(groupId, roomId, isPublic) {
|
||||
@@ -806,7 +806,7 @@ MatrixBaseApis.prototype.updateGroupRoomVisibility = function(groupId, roomId, i
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @param {string} roomId
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.removeRoomFromGroup = function(groupId, roomId) {
|
||||
@@ -820,7 +820,7 @@ MatrixBaseApis.prototype.removeRoomFromGroup = function(groupId, roomId) {
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @param {Object} opts Additional options to send alongside the acceptance.
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.acceptGroupInvite = function(groupId, opts = null) {
|
||||
@@ -833,7 +833,7 @@ MatrixBaseApis.prototype.acceptGroupInvite = function(groupId, opts = null) {
|
||||
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.joinGroup = function(groupId) {
|
||||
@@ -846,7 +846,7 @@ MatrixBaseApis.prototype.joinGroup = function(groupId) {
|
||||
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.leaveGroup = function(groupId) {
|
||||
@@ -858,7 +858,7 @@ MatrixBaseApis.prototype.leaveGroup = function(groupId) {
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {module:client.Promise} Resolves: The groups to which the user is joined
|
||||
* @return {Promise} Resolves: The groups to which the user is joined
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getJoinedGroups = function() {
|
||||
@@ -870,7 +870,7 @@ MatrixBaseApis.prototype.getJoinedGroups = function() {
|
||||
* @param {Object} content Request content
|
||||
* @param {string} content.localpart The local part of the desired group ID
|
||||
* @param {Object} content.profile Group profile object
|
||||
* @return {module:client.Promise} Resolves: Object with key group_id: id of the created group
|
||||
* @return {Promise} Resolves: Object with key group_id: id of the created group
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.createGroup = function(content) {
|
||||
@@ -882,7 +882,7 @@ MatrixBaseApis.prototype.createGroup = function(content) {
|
||||
|
||||
/**
|
||||
* @param {string[]} userIds List of user IDs
|
||||
* @return {module:client.Promise} Resolves: Object as exmaple below
|
||||
* @return {Promise} Resolves: Object as exmaple below
|
||||
*
|
||||
* {
|
||||
* "users": {
|
||||
@@ -903,7 +903,7 @@ MatrixBaseApis.prototype.getPublicisedGroups = function(userIds) {
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @param {bool} isPublic Whether the user's membership of this group is made public
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setGroupPublicity = function(groupId, isPublic) {
|
||||
@@ -922,7 +922,7 @@ MatrixBaseApis.prototype.setGroupPublicity = function(groupId, isPublic) {
|
||||
* @param {string} eventType
|
||||
* @param {string} stateKey
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getStateEvent = function(roomId, eventType, stateKey, callback) {
|
||||
@@ -946,7 +946,7 @@ MatrixBaseApis.prototype.getStateEvent = function(roomId, eventType, stateKey, c
|
||||
* @param {Object} content
|
||||
* @param {string} stateKey
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.sendStateEvent = function(roomId, eventType, content, stateKey,
|
||||
@@ -969,7 +969,7 @@ MatrixBaseApis.prototype.sendStateEvent = function(roomId, eventType, content, s
|
||||
* @param {string} roomId
|
||||
* @param {Number} limit
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
|
||||
@@ -1000,7 +1000,7 @@ MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
|
||||
* @param {object} opts Options for the read markers.
|
||||
* @param {object} opts.hidden True to hide the read receipt from other users. <b>This
|
||||
* property is currently unstable and may change in the future.</b>
|
||||
* @return {module:client.Promise} Resolves: the empty object, {}.
|
||||
* @return {Promise} Resolves: the empty object, {}.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
|
||||
function(roomId, rmEventId, rrEventId, opts) {
|
||||
@@ -1020,7 +1020,7 @@ MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {module:client.Promise} Resolves: A list of the user's current rooms
|
||||
* @return {Promise} Resolves: A list of the user's current rooms
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getJoinedRooms = function() {
|
||||
@@ -1031,7 +1031,7 @@ MatrixBaseApis.prototype.getJoinedRooms = function() {
|
||||
/**
|
||||
* Retrieve membership info. for a room.
|
||||
* @param {string} roomId ID of the room to get membership for
|
||||
* @return {module:client.Promise} Resolves: A list of currently joined users
|
||||
* @return {Promise} Resolves: A list of currently joined users
|
||||
* and their profile data.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
@@ -1055,7 +1055,7 @@ MatrixBaseApis.prototype.getJoinedRoomMembers = function(roomId) {
|
||||
* @param {object} options.filter Filter parameters
|
||||
* @param {string} options.filter.generic_search_term String to search for
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.publicRooms = function(options, callback) {
|
||||
@@ -1087,7 +1087,7 @@ MatrixBaseApis.prototype.publicRooms = function(options, callback) {
|
||||
* @param {string} alias The room alias to create.
|
||||
* @param {string} roomId The room ID to link the alias to.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO.
|
||||
* @return {Promise} Resolves: TODO.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.createAlias = function(alias, roomId, callback) {
|
||||
@@ -1107,7 +1107,7 @@ MatrixBaseApis.prototype.createAlias = function(alias, roomId, callback) {
|
||||
* and you must have sufficient access to do this operation.
|
||||
* @param {string} alias The room alias to delete.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO.
|
||||
* @return {Promise} Resolves: TODO.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.deleteAlias = function(alias, callback) {
|
||||
@@ -1123,7 +1123,7 @@ MatrixBaseApis.prototype.deleteAlias = function(alias, callback) {
|
||||
* Get room info for the given alias.
|
||||
* @param {string} alias The room alias to resolve.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: Object with room_id and servers.
|
||||
* @return {Promise} Resolves: Object with room_id and servers.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getRoomIdForAlias = function(alias, callback) {
|
||||
@@ -1139,7 +1139,7 @@ MatrixBaseApis.prototype.getRoomIdForAlias = function(alias, callback) {
|
||||
/**
|
||||
* @param {string} roomAlias
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.resolveRoomAlias = function(roomAlias, callback) {
|
||||
@@ -1152,7 +1152,7 @@ MatrixBaseApis.prototype.resolveRoomAlias = function(roomAlias, callback) {
|
||||
* Get the visibility of a room in the current HS's room directory
|
||||
* @param {string} roomId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getRoomDirectoryVisibility =
|
||||
@@ -1170,7 +1170,7 @@ MatrixBaseApis.prototype.getRoomDirectoryVisibility =
|
||||
* in the public directory, or "private" to make
|
||||
* it invisible.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setRoomDirectoryVisibility =
|
||||
@@ -1193,7 +1193,7 @@ MatrixBaseApis.prototype.setRoomDirectoryVisibility =
|
||||
* in the public directory, or "private" to make
|
||||
* it invisible.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService =
|
||||
@@ -1216,7 +1216,7 @@ MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService =
|
||||
* @param {string} opts.term the term with which to search.
|
||||
* @param {number} opts.limit the maximum number of results to return. The server will
|
||||
* apply a limit if unspecified.
|
||||
* @return {module:client.Promise} Resolves: an array of results.
|
||||
* @return {Promise} Resolves: an array of results.
|
||||
*/
|
||||
MatrixBaseApis.prototype.searchUserDirectory = function(opts) {
|
||||
const body = {
|
||||
@@ -1272,7 +1272,7 @@ MatrixBaseApis.prototype.searchUserDirectory = function(opts) {
|
||||
* data has been uploaded, with an object containing the fields `loaded`
|
||||
* (number of bytes transferred) and `total` (total size, if known).
|
||||
*
|
||||
* @return {module:client.Promise} Resolves to response object, as
|
||||
* @return {Promise} Resolves to response object, as
|
||||
* determined by this.opts.onlyData, opts.rawResponse, and
|
||||
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
|
||||
*/
|
||||
@@ -1282,7 +1282,7 @@ MatrixBaseApis.prototype.uploadContent = function(file, opts) {
|
||||
|
||||
/**
|
||||
* Cancel a file upload in progress
|
||||
* @param {module:client.Promise} promise The promise returned from uploadContent
|
||||
* @param {Promise} promise The promise returned from uploadContent
|
||||
* @return {boolean} true if canceled, otherwise false
|
||||
*/
|
||||
MatrixBaseApis.prototype.cancelUpload = function(promise) {
|
||||
@@ -1310,7 +1310,7 @@ MatrixBaseApis.prototype.getCurrentUploads = function() {
|
||||
* @param {string} info The kind of info to retrieve (e.g. 'displayname',
|
||||
* 'avatar_url').
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getProfileInfo = function(userId, info, callback) {
|
||||
@@ -1332,7 +1332,7 @@ MatrixBaseApis.prototype.getProfileInfo = function(userId, info, callback) {
|
||||
|
||||
/**
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getThreePids = function(callback) {
|
||||
@@ -1352,7 +1352,7 @@ MatrixBaseApis.prototype.getThreePids = function(callback) {
|
||||
* @param {Object} creds
|
||||
* @param {boolean} bind
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: on success
|
||||
* @return {Promise} Resolves: on success
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
|
||||
@@ -1375,7 +1375,7 @@ MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
|
||||
*
|
||||
* @param {Object} data A object with 3PID validation data from having called
|
||||
* `account/3pid/<medium>/requestToken` on the homeserver.
|
||||
* @return {module:client.Promise} Resolves: on success
|
||||
* @return {Promise} Resolves: on success
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.addThreePidOnly = async function(data) {
|
||||
@@ -1398,7 +1398,7 @@ MatrixBaseApis.prototype.addThreePidOnly = async function(data) {
|
||||
* @param {Object} data A object with 3PID validation data from having called
|
||||
* `validate/<medium>/requestToken` on the identity server. It should also
|
||||
* contain `id_server` and `id_access_token` fields as well.
|
||||
* @return {module:client.Promise} Resolves: on success
|
||||
* @return {Promise} Resolves: on success
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.bindThreePid = async function(data) {
|
||||
@@ -1418,7 +1418,7 @@ MatrixBaseApis.prototype.bindThreePid = async function(data) {
|
||||
* @param {string} medium The threepid medium (eg. 'email')
|
||||
* @param {string} address The threepid address (eg. 'bob@example.com')
|
||||
* this must be as returned by getThreePids.
|
||||
* @return {module:client.Promise} Resolves: on success
|
||||
* @return {Promise} Resolves: on success
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.unbindThreePid = async function(medium, address) {
|
||||
@@ -1439,7 +1439,7 @@ MatrixBaseApis.prototype.unbindThreePid = async function(medium, address) {
|
||||
* @param {string} medium The threepid medium (eg. 'email')
|
||||
* @param {string} address The threepid address (eg. 'bob@example.com')
|
||||
* this must be as returned by getThreePids.
|
||||
* @return {module:client.Promise} Resolves: The server response on success
|
||||
* @return {Promise} Resolves: The server response on success
|
||||
* (generally the empty JSON object)
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
@@ -1457,7 +1457,7 @@ MatrixBaseApis.prototype.deleteThreePid = function(medium, address) {
|
||||
* @param {Object} authDict
|
||||
* @param {string} newPassword The new desired password.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setPassword = function(authDict, newPassword, callback) {
|
||||
@@ -1478,7 +1478,7 @@ MatrixBaseApis.prototype.setPassword = function(authDict, newPassword, callback)
|
||||
|
||||
/**
|
||||
* Gets all devices recorded for the logged-in user
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getDevices = function() {
|
||||
@@ -1492,7 +1492,7 @@ MatrixBaseApis.prototype.getDevices = function() {
|
||||
*
|
||||
* @param {string} device_id device to update
|
||||
* @param {Object} body body of request
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) {
|
||||
@@ -1508,7 +1508,7 @@ MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) {
|
||||
*
|
||||
* @param {string} device_id device to delete
|
||||
* @param {object} auth Optional. Auth data to supply for User-Interactive auth.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) {
|
||||
@@ -1530,7 +1530,7 @@ MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) {
|
||||
*
|
||||
* @param {string[]} devices IDs of the devices to delete
|
||||
* @param {object} auth Optional. Auth data to supply for User-Interactive auth.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.deleteMultipleDevices = function(devices, auth) {
|
||||
@@ -1552,7 +1552,7 @@ MatrixBaseApis.prototype.deleteMultipleDevices = function(devices, auth) {
|
||||
* Gets all pushers registered for the logged-in user
|
||||
*
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: Array of objects representing pushers
|
||||
* @return {Promise} Resolves: Array of objects representing pushers
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getPushers = function(callback) {
|
||||
@@ -1567,7 +1567,7 @@ MatrixBaseApis.prototype.getPushers = function(callback) {
|
||||
*
|
||||
* @param {Object} pusher Object representing a pusher
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: Empty json object on success
|
||||
* @return {Promise} Resolves: Empty json object on success
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setPusher = function(pusher, callback) {
|
||||
@@ -1579,7 +1579,7 @@ MatrixBaseApis.prototype.setPusher = function(pusher, callback) {
|
||||
|
||||
/**
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getPushRules = function(callback) {
|
||||
@@ -1594,7 +1594,7 @@ MatrixBaseApis.prototype.getPushRules = function(callback) {
|
||||
* @param {string} ruleId
|
||||
* @param {Object} body
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.addPushRule = function(scope, kind, ruleId, body, callback) {
|
||||
@@ -1613,7 +1613,7 @@ MatrixBaseApis.prototype.addPushRule = function(scope, kind, ruleId, body, callb
|
||||
* @param {string} kind
|
||||
* @param {string} ruleId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.deletePushRule = function(scope, kind, ruleId, callback) {
|
||||
@@ -1632,7 +1632,7 @@ MatrixBaseApis.prototype.deletePushRule = function(scope, kind, ruleId, callback
|
||||
* @param {string} ruleId
|
||||
* @param {boolean} enabled
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setPushRuleEnabled = function(scope, kind,
|
||||
@@ -1653,7 +1653,7 @@ MatrixBaseApis.prototype.setPushRuleEnabled = function(scope, kind,
|
||||
* @param {string} ruleId
|
||||
* @param {array} actions
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setPushRuleActions = function(scope, kind,
|
||||
@@ -1677,7 +1677,7 @@ MatrixBaseApis.prototype.setPushRuleActions = function(scope, kind,
|
||||
* @param {string} opts.next_batch the batch token to pass in the query string
|
||||
* @param {Object} opts.body the JSON object to pass to the request body.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.search = function(opts, callback) {
|
||||
@@ -1698,28 +1698,16 @@ MatrixBaseApis.prototype.search = function(opts, callback) {
|
||||
*
|
||||
* @param {Object} content body of upload request
|
||||
*
|
||||
* @param {Object=} opts
|
||||
*
|
||||
* @param {string=} opts.device_id explicit device_id to use for upload
|
||||
* (default is to use the same as that used during auth).
|
||||
* @param {Object=} opts this method no longer takes any opts,
|
||||
* used to take opts.device_id but this was not removed from the spec as a redundant parameter
|
||||
*
|
||||
* @param {module:client.callback=} callback
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: result object. Rejects: with
|
||||
* @return {Promise} Resolves: result object. Rejects: with
|
||||
* an error response ({@link module:http-api.MatrixError}).
|
||||
*/
|
||||
MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
|
||||
opts = opts || {};
|
||||
const deviceId = opts.device_id;
|
||||
let path;
|
||||
if (deviceId) {
|
||||
path = utils.encodeUri("/keys/upload/$deviceId", {
|
||||
$deviceId: deviceId,
|
||||
});
|
||||
} else {
|
||||
path = "/keys/upload";
|
||||
}
|
||||
return this._http.authedRequest(callback, "POST", path, undefined, content);
|
||||
return this._http.authedRequest(callback, "POST", "/keys/upload", undefined, content);
|
||||
};
|
||||
|
||||
MatrixBaseApis.prototype.uploadKeySignatures = function(content) {
|
||||
@@ -1741,7 +1729,7 @@ MatrixBaseApis.prototype.uploadKeySignatures = function(content) {
|
||||
* @param {string=} opts.token sync token to pass in the query request, to help
|
||||
* the HS give the most recent results
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: result object. Rejects: with
|
||||
* @return {Promise} Resolves: result object. Rejects: with
|
||||
* an error response ({@link module:http-api.MatrixError}).
|
||||
*/
|
||||
MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
|
||||
@@ -1773,7 +1761,7 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
|
||||
*
|
||||
* @param {string} [key_algorithm = signed_curve25519] desired key type
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: result object. Rejects: with
|
||||
* @return {Promise} Resolves: result object. Rejects: with
|
||||
* an error response ({@link module:http-api.MatrixError}).
|
||||
*/
|
||||
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
|
||||
@@ -1802,7 +1790,7 @@ MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
|
||||
* @param {string} oldToken
|
||||
* @param {string} newToken
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: result object. Rejects: with
|
||||
* @return {Promise} Resolves: result object. Rejects: with
|
||||
* an error response ({@link module:http-api.MatrixError}).
|
||||
*/
|
||||
MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
|
||||
@@ -1816,7 +1804,8 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
|
||||
};
|
||||
|
||||
MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(auth, keys) {
|
||||
const data = Object.assign({}, keys, {auth});
|
||||
const data = Object.assign({}, keys);
|
||||
if (auth) Object.assign(data, {auth});
|
||||
return this._http.authedRequest(
|
||||
undefined, "POST", "/keys/device_signing/upload", undefined, data, {
|
||||
prefix: PREFIX_UNSTABLE,
|
||||
@@ -1836,7 +1825,7 @@ MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(auth, keys) {
|
||||
* general) was added as part of the v2 API version.
|
||||
*
|
||||
* @param {object} hsOpenIdToken
|
||||
* @return {module:client.Promise} Resolves: with object containing an Identity
|
||||
* @return {Promise} Resolves: with object containing an Identity
|
||||
* Server access token.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
@@ -1872,7 +1861,7 @@ MatrixBaseApis.prototype.registerWithIdentityServer = function(hsOpenIdToken) {
|
||||
* @param {string} identityAccessToken The `access_token` field of the identity
|
||||
* server `/account/register` response (see {@link registerWithIdentityServer}).
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @throws Error if no identity server is set
|
||||
*/
|
||||
@@ -1938,7 +1927,7 @@ MatrixBaseApis.prototype.requestEmailToken = async function(
|
||||
* @param {string} identityAccessToken The `access_token` field of the Identity
|
||||
* Server `/account/register` response (see {@link registerWithIdentityServer}).
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @throws Error if no identity server is set
|
||||
*/
|
||||
@@ -1998,7 +1987,7 @@ MatrixBaseApis.prototype.requestMsisdnToken = async function(
|
||||
* @param {string} identityAccessToken The `access_token` field of the Identity
|
||||
* Server `/account/register` response (see {@link registerWithIdentityServer}).
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: Object, currently with no parameters.
|
||||
* @return {Promise} Resolves: Object, currently with no parameters.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @throws Error if No ID server is set
|
||||
*/
|
||||
@@ -2049,7 +2038,7 @@ MatrixBaseApis.prototype.submitMsisdnToken = async function(
|
||||
* This must be the same value submitted in the requestToken call.
|
||||
* @param {string} msisdnToken The MSISDN token, as enetered by the user.
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: Object, currently with no parameters.
|
||||
* @return {Promise} Resolves: Object, currently with no parameters.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.submitMsisdnTokenOtherUrl = function(
|
||||
@@ -2175,7 +2164,7 @@ MatrixBaseApis.prototype.identityHashedLookup = async function(
|
||||
* @param {string} identityAccessToken The `access_token` field of the Identity
|
||||
* Server `/account/register` response (see {@link registerWithIdentityServer}).
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: A threepid mapping
|
||||
* @return {Promise} Resolves: A threepid mapping
|
||||
* object or the empty object if no mapping
|
||||
* exists
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
@@ -2243,7 +2232,7 @@ MatrixBaseApis.prototype.lookupThreePid = async function(
|
||||
* @param {string} identityAccessToken The `access_token` field of the Identity
|
||||
* Server `/account/register` response (see {@link registerWithIdentityServer}).
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: Lookup results from IS.
|
||||
* @return {Promise} Resolves: Lookup results from IS.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.bulkLookupThreePids = async function(
|
||||
@@ -2300,7 +2289,7 @@ MatrixBaseApis.prototype.bulkLookupThreePids = async function(
|
||||
* @param {string} identityAccessToken The `access_token` field of the Identity
|
||||
* Server `/account/register` response (see {@link registerWithIdentityServer}).
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: an object with account info.
|
||||
* @return {Promise} Resolves: an object with account info.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getIdentityAccount = function(
|
||||
@@ -2323,7 +2312,7 @@ MatrixBaseApis.prototype.getIdentityAccount = function(
|
||||
* content to send. Map from user_id to device_id to content object.
|
||||
* @param {string=} txnId transaction id. One will be made up if not
|
||||
* supplied.
|
||||
* @return {module:client.Promise} Resolves to the result object
|
||||
* @return {Promise} Resolves to the result object
|
||||
*/
|
||||
MatrixBaseApis.prototype.sendToDevice = function(
|
||||
eventType, contentMap, txnId,
|
||||
@@ -2352,7 +2341,7 @@ MatrixBaseApis.prototype.sendToDevice = function(
|
||||
/**
|
||||
* Get the third party protocols that can be reached using
|
||||
* this HS
|
||||
* @return {module:client.Promise} Resolves to the result object
|
||||
* @return {Promise} Resolves to the result object
|
||||
*/
|
||||
MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
|
||||
return this._http.authedRequest(
|
||||
@@ -2374,7 +2363,7 @@ MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
|
||||
* @param {string} protocol The protocol given in getThirdpartyProtocols()
|
||||
* @param {object} params Protocol-specific parameters, as given in the
|
||||
* response to getThirdpartyProtocols()
|
||||
* @return {module:client.Promise} Resolves to the result object
|
||||
* @return {Promise} Resolves to the result object
|
||||
*/
|
||||
MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) {
|
||||
const path = utils.encodeUri("/thirdparty/location/$protocol", {
|
||||
@@ -2390,7 +2379,7 @@ MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) {
|
||||
* @param {string} protocol The protocol given in getThirdpartyProtocols()
|
||||
* @param {object} params Protocol-specific parameters, as given in the
|
||||
* response to getThirdpartyProtocols()
|
||||
* @return {module:client.Promise} Resolves to the result object
|
||||
* @return {Promise} Resolves to the result object
|
||||
*/
|
||||
MatrixBaseApis.prototype.getThirdpartyUser = function(protocol, params) {
|
||||
const path = utils.encodeUri("/thirdparty/user/$protocol", {
|
||||
@@ -2425,7 +2414,7 @@ MatrixBaseApis.prototype.agreeToTerms = function(
|
||||
* @param {string} eventId The event to report.
|
||||
* @param {number} score The score to rate this content as where -100 is most offensive and 0 is inoffensive.
|
||||
* @param {string} reason The reason the content is being reported. May be blank.
|
||||
* @returns {module:client.Promise} Resolves to an empty object if successful
|
||||
* @returns {Promise} Resolves to an empty object if successful
|
||||
*/
|
||||
MatrixBaseApis.prototype.reportEvent = function(roomId, eventId, score, reason) {
|
||||
const path = utils.encodeUri("/rooms/$roomId/report/$eventId", {
|
||||
|
||||
+136
-118
@@ -98,6 +98,12 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
*
|
||||
* @param {string} opts.userId The user ID for this user.
|
||||
*
|
||||
* @param {Object} opts.deviceToImport Device data exported with
|
||||
* "exportDevice" method that must be imported to recreate this device.
|
||||
* Should only be useful for devices with end-to-end crypto enabled.
|
||||
* If provided, opts.deviceId and opts.userId should **NOT** be provided
|
||||
* (they are present in the exported data).
|
||||
*
|
||||
* @param {IdentityServerProvider} [opts.identityServer]
|
||||
* Optional. A provider object with one function `getAccessToken`, which is a
|
||||
* callback that returns a Promise<String> of an identity access token to supply
|
||||
@@ -215,6 +221,7 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
* pubkey: {UInt8Array}
|
||||
* }
|
||||
* }
|
||||
* {string} name the name of the value we want to read out of SSSS, for UI purposes.
|
||||
*
|
||||
* @param {function} [opts.cryptoCallbacks.onSecretRequested]
|
||||
* Optional. Function called when a request for a secret is received from another
|
||||
@@ -248,6 +255,29 @@ export function MatrixClient(opts) {
|
||||
userId: userId,
|
||||
};
|
||||
|
||||
if (opts.deviceToImport) {
|
||||
if (this.deviceId) {
|
||||
logger.warn(
|
||||
'not importing device because'
|
||||
+ ' device ID is provided to constructor'
|
||||
+ ' independently of exported data',
|
||||
);
|
||||
} else if (this.credentials.userId) {
|
||||
logger.warn(
|
||||
'not importing device because'
|
||||
+ ' user ID is provided to constructor'
|
||||
+ ' independently of exported data',
|
||||
);
|
||||
} else if (!(opts.deviceToImport.deviceId)) {
|
||||
logger.warn('not importing device because no device ID in exported data');
|
||||
} else {
|
||||
this.deviceId = opts.deviceToImport.deviceId;
|
||||
this.credentials.userId = opts.deviceToImport.userId;
|
||||
// will be used during async initialization of the crypto
|
||||
this._exportedOlmDeviceToImport = opts.deviceToImport.olmDevice;
|
||||
}
|
||||
}
|
||||
|
||||
this.scheduler = opts.scheduler;
|
||||
if (this.scheduler) {
|
||||
const self = this;
|
||||
@@ -391,6 +421,18 @@ export function MatrixClient(opts) {
|
||||
utils.inherits(MatrixClient, EventEmitter);
|
||||
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
|
||||
|
||||
MatrixClient.prototype.exportDevice = async function() {
|
||||
if (!(this._crypto)) {
|
||||
logger.warn('not exporting device if crypto is not enabled');
|
||||
return;
|
||||
}
|
||||
return {
|
||||
userId: this.credentials.userId,
|
||||
deviceId: this.deviceId,
|
||||
olmDevice: await this._crypto._olmDevice.export(),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear any data out of the persistent stores used by the client.
|
||||
*
|
||||
@@ -507,7 +549,7 @@ MatrixClient.prototype.isInitialSyncComplete = function() {
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
return state === "PREPAED" || state === "SYNCING";
|
||||
return state === "PREPARED" || state === "SYNCING";
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -570,7 +612,7 @@ MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
|
||||
* Gets the capabilities of the homeserver. Always returns an object of
|
||||
* capability keys and their options, which may be empty.
|
||||
* @param {boolean} fresh True to ignore any cached values.
|
||||
* @return {module:client.Promise} Resolves to the capabilities of the homeserver
|
||||
* @return {Promise} Resolves to the capabilities of the homeserver
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.getCapabilities = function(fresh=false) {
|
||||
@@ -684,7 +726,10 @@ MatrixClient.prototype.initCrypto = async function() {
|
||||
]);
|
||||
|
||||
logger.log("Crypto: initialising crypto object...");
|
||||
await crypto.init();
|
||||
await crypto.init({
|
||||
exportedOlmDevice: this._exportedOlmDeviceToImport,
|
||||
});
|
||||
delete this._exportedOlmDeviceToImport;
|
||||
|
||||
this.olmVersion = Crypto.getOlmVersion();
|
||||
|
||||
@@ -751,7 +796,7 @@ MatrixClient.prototype.downloadKeys = function(userIds, forceDownload) {
|
||||
*
|
||||
* @param {string} userId the user to list keys for.
|
||||
*
|
||||
* @return {Promise<module:crypto-deviceinfo[]>} list of devices
|
||||
* @return {Promise<module:crypto/deviceinfo[]>} list of devices
|
||||
*/
|
||||
MatrixClient.prototype.getStoredDevicesForUser = async function(userId) {
|
||||
if (this._crypto === null) {
|
||||
@@ -766,7 +811,7 @@ MatrixClient.prototype.getStoredDevicesForUser = async function(userId) {
|
||||
* @param {string} userId the user to list keys for.
|
||||
* @param {string} deviceId unique identifier for the device
|
||||
*
|
||||
* @return {Promise<?module:crypto-deviceinfo>} device or null
|
||||
* @return {Promise<?module:crypto/deviceinfo>} device or null
|
||||
*/
|
||||
MatrixClient.prototype.getStoredDevice = async function(userId, deviceId) {
|
||||
if (this._crypto === null) {
|
||||
@@ -862,52 +907,32 @@ async function _setDeviceVerification(
|
||||
*
|
||||
* @param {string} userId the user to request verification with
|
||||
* @param {string} roomId the room to use for verification
|
||||
* @param {Array} methods array of verification methods to use. Defaults to
|
||||
* all known methods
|
||||
*
|
||||
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
|
||||
* when the request is accepted by the other user
|
||||
* @returns {Promise<module:crypto/verification/request/VerificationRequest>} resolves to a VerificationRequest
|
||||
* when the request has been sent to the other party.
|
||||
*/
|
||||
MatrixClient.prototype.requestVerificationDM = function(userId, roomId, methods) {
|
||||
MatrixClient.prototype.requestVerificationDM = function(userId, roomId) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
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);
|
||||
return this._crypto.requestVerificationDM(userId, roomId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Request a key verification from another user.
|
||||
*
|
||||
* @param {string} userId the user to request verification with
|
||||
* @param {Array} methods array of verification methods to use. Defaults to
|
||||
* all known methods
|
||||
* @param {Array} devices array of device IDs to send requests to. Defaults to
|
||||
* all devices owned by the user
|
||||
*
|
||||
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
|
||||
* when the request is accepted by the other user
|
||||
* @returns {Promise<module:crypto/verification/request/VerificationRequest>} resolves to a VerificationRequest
|
||||
* when the request has been sent to the other party.
|
||||
*/
|
||||
MatrixClient.prototype.requestVerification = function(userId, methods, devices) {
|
||||
MatrixClient.prototype.requestVerification = function(userId, devices) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this._crypto.requestVerification(userId, methods, devices);
|
||||
return this._crypto.requestVerification(userId, devices);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1336,7 +1361,7 @@ MatrixClient.prototype.forceDiscardSession = function(roomId) {
|
||||
*
|
||||
* This should be encrypted before returning it to the user.
|
||||
*
|
||||
* @return {module:client.Promise} a promise which resolves to a list of
|
||||
* @return {Promise} a promise which resolves to a list of
|
||||
* session export objects
|
||||
*/
|
||||
MatrixClient.prototype.exportRoomKeys = function() {
|
||||
@@ -1351,7 +1376,7 @@ MatrixClient.prototype.exportRoomKeys = function() {
|
||||
*
|
||||
* @param {Object[]} keys a list of session export objects
|
||||
*
|
||||
* @return {module:client.Promise} a promise which resolves when the keys
|
||||
* @return {Promise} a promise which resolves when the keys
|
||||
* have been imported
|
||||
*/
|
||||
MatrixClient.prototype.importRoomKeys = function(keys) {
|
||||
@@ -1616,7 +1641,7 @@ MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version)
|
||||
* @param {string} sessionId ID of the session that the keys are for Optional.
|
||||
* @param {integer} version backup version Optional.
|
||||
* @param {object} data Object keys to send
|
||||
* @return {module:client.Promise} a promise that will resolve when the keys
|
||||
* @return {Promise} a promise that will resolve when the keys
|
||||
* are uploaded
|
||||
*/
|
||||
MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data) {
|
||||
@@ -1842,7 +1867,7 @@ MatrixClient.prototype.getGroups = function() {
|
||||
/**
|
||||
* Get the config for the media repository.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves with an object containing the config.
|
||||
* @return {Promise} Resolves with an object containing the config.
|
||||
*/
|
||||
MatrixClient.prototype.getMediaConfig = function(callback) {
|
||||
return this._http.authedRequest(
|
||||
@@ -1932,7 +1957,7 @@ MatrixClient.prototype.getUsers = function() {
|
||||
* @param {string} eventType The event type
|
||||
* @param {Object} contents the contents object for the event
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.setAccountData = function(eventType, contents, callback) {
|
||||
@@ -1959,7 +1984,7 @@ MatrixClient.prototype.getAccountData = function(eventType) {
|
||||
* gets account data directly from the homeserver if the local store is not
|
||||
* ready, which can be useful very early in startup before the initial sync.
|
||||
* @param {string} eventType The event type
|
||||
* @return {module:client.Promise} Resolves: The contents of the given account
|
||||
* @return {Promise} Resolves: The contents of the given account
|
||||
* data event.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
@@ -1996,7 +2021,7 @@ MatrixClient.prototype.getIgnoredUsers = function() {
|
||||
* Sets the users that the current user should ignore.
|
||||
* @param {string[]} userIds the user IDs to ignore
|
||||
* @param {module:client.callback} [callback] Optional.
|
||||
* @return {module:client.Promise} Resolves: Account data event
|
||||
* @return {Promise} Resolves: Account data event
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.setIgnoredUsers = function(userIds, callback) {
|
||||
@@ -2029,7 +2054,7 @@ MatrixClient.prototype.isUserIgnored = function(userId) {
|
||||
* @param {string[]} opts.viaServers The server names to try and join through in
|
||||
* addition to those that are automatically chosen.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: Room object.
|
||||
* @return {Promise} Resolves: Room object.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
|
||||
@@ -2097,7 +2122,7 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
|
||||
* @param {MatrixEvent} event The event to resend.
|
||||
* @param {Room} room Optional. The room the event is in. Will update the
|
||||
* timeline entry if provided.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.resendEvent = function(event, room) {
|
||||
@@ -2131,7 +2156,7 @@ MatrixClient.prototype.cancelPendingEvent = function(event) {
|
||||
* @param {string} roomId
|
||||
* @param {string} name
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.setRoomName = function(roomId, name, callback) {
|
||||
@@ -2143,7 +2168,7 @@ MatrixClient.prototype.setRoomName = function(roomId, name, callback) {
|
||||
* @param {string} roomId
|
||||
* @param {string} topic
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.setRoomTopic = function(roomId, topic, callback) {
|
||||
@@ -2154,7 +2179,7 @@ MatrixClient.prototype.setRoomTopic = function(roomId, topic, callback) {
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.getRoomTags = function(roomId, callback) {
|
||||
@@ -2172,7 +2197,7 @@ MatrixClient.prototype.getRoomTags = function(roomId, callback) {
|
||||
* @param {string} tagName name of room tag to be set
|
||||
* @param {object} metadata associated with that tag to be stored
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.setRoomTag = function(roomId, tagName, metadata, callback) {
|
||||
@@ -2190,7 +2215,7 @@ MatrixClient.prototype.setRoomTag = function(roomId, tagName, metadata, callback
|
||||
* @param {string} roomId
|
||||
* @param {string} tagName name of room tag to be removed
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.deleteRoomTag = function(roomId, tagName, callback) {
|
||||
@@ -2209,7 +2234,7 @@ MatrixClient.prototype.deleteRoomTag = function(roomId, tagName, callback) {
|
||||
* @param {string} eventType event type to be set
|
||||
* @param {object} content event content
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.setRoomAccountData = function(roomId, eventType,
|
||||
@@ -2231,7 +2256,7 @@ MatrixClient.prototype.setRoomAccountData = function(roomId, eventType,
|
||||
* @param {Number} powerLevel
|
||||
* @param {MatrixEvent} event
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.setPowerLevel = function(roomId, userId, powerLevel,
|
||||
@@ -2259,7 +2284,7 @@ MatrixClient.prototype.setPowerLevel = function(roomId, userId, powerLevel,
|
||||
* @param {Object} content
|
||||
* @param {string} txnId Optional.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
|
||||
@@ -2274,7 +2299,7 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
|
||||
* @param {object} eventObject An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added.
|
||||
* @param {string} txnId the txnId.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype._sendCompleteEvent = function(roomId, eventObject, txnId,
|
||||
@@ -2414,7 +2439,7 @@ function _sendEvent(client, room, event, callback) {
|
||||
* @param {module:models/room?} room destination room. Null if the destination
|
||||
* is not a room we have seen over the sync pipe.
|
||||
*
|
||||
* @return {module:client.Promise?} Promise which resolves when the event has been
|
||||
* @return {Promise?} Promise which resolves when the event has been
|
||||
* encrypted, or null if nothing was needed
|
||||
*/
|
||||
|
||||
@@ -2523,7 +2548,7 @@ function _sendEventHttpRequest(client, event) {
|
||||
* @param {string} [txnId] transaction id. One will be made up if not
|
||||
* supplied.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.redactEvent = function(roomId, eventId, txnId, callback) {
|
||||
@@ -2539,7 +2564,7 @@ MatrixClient.prototype.redactEvent = function(roomId, eventId, txnId, callback)
|
||||
* @param {Object} content
|
||||
* @param {string} txnId Optional.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.sendMessage = function(roomId, content, txnId, callback) {
|
||||
@@ -2556,7 +2581,7 @@ MatrixClient.prototype.sendMessage = function(roomId, content, txnId, callback)
|
||||
* @param {string} body
|
||||
* @param {string} txnId Optional.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.sendTextMessage = function(roomId, body, txnId, callback) {
|
||||
@@ -2569,7 +2594,7 @@ MatrixClient.prototype.sendTextMessage = function(roomId, body, txnId, callback)
|
||||
* @param {string} body
|
||||
* @param {string} txnId Optional.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.sendNotice = function(roomId, body, txnId, callback) {
|
||||
@@ -2582,7 +2607,7 @@ MatrixClient.prototype.sendNotice = function(roomId, body, txnId, callback) {
|
||||
* @param {string} body
|
||||
* @param {string} txnId Optional.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.sendEmoteMessage = function(roomId, body, txnId, callback) {
|
||||
@@ -2596,7 +2621,7 @@ MatrixClient.prototype.sendEmoteMessage = function(roomId, body, txnId, callback
|
||||
* @param {Object} info
|
||||
* @param {string} text
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.sendImageMessage = function(roomId, url, info, text, callback) {
|
||||
@@ -2621,7 +2646,7 @@ MatrixClient.prototype.sendImageMessage = function(roomId, url, info, text, call
|
||||
* @param {Object} info
|
||||
* @param {string} text
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.sendStickerMessage = function(roomId, url, info, text, callback) {
|
||||
@@ -2646,7 +2671,7 @@ MatrixClient.prototype.sendStickerMessage = function(roomId, url, info, text, ca
|
||||
* @param {string} body
|
||||
* @param {string} htmlBody
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.sendHtmlMessage = function(roomId, body, htmlBody, callback) {
|
||||
@@ -2659,7 +2684,7 @@ MatrixClient.prototype.sendHtmlMessage = function(roomId, body, htmlBody, callba
|
||||
* @param {string} body
|
||||
* @param {string} htmlBody
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.sendHtmlNotice = function(roomId, body, htmlBody, callback) {
|
||||
@@ -2672,7 +2697,7 @@ MatrixClient.prototype.sendHtmlNotice = function(roomId, body, htmlBody, callbac
|
||||
* @param {string} body
|
||||
* @param {string} htmlBody
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.sendHtmlEmote = function(roomId, body, htmlBody, callback) {
|
||||
@@ -2686,7 +2711,7 @@ MatrixClient.prototype.sendHtmlEmote = function(roomId, body, htmlBody, callback
|
||||
* @param {string} receiptType The kind of receipt e.g. "m.read"
|
||||
* @param {object} opts Additional content to send alongside the receipt.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.sendReceipt = function(event, receiptType, opts, callback) {
|
||||
@@ -2723,7 +2748,7 @@ MatrixClient.prototype.sendReceipt = function(event, receiptType, opts, callback
|
||||
* other users and homeservers. Default false (send to everyone). <b>This
|
||||
* property is unstable and may change in the future.</b>
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.sendReadReceipt = async function(event, opts, callback) {
|
||||
@@ -2759,7 +2784,7 @@ MatrixClient.prototype.sendReadReceipt = async function(event, opts, callback) {
|
||||
* @param {object} opts Options for the read markers
|
||||
* @param {object} opts.hidden True to hide the receipt from other users and homeservers.
|
||||
* <b>This property is unstable and may change in the future.</b>
|
||||
* @return {module:client.Promise} Resolves: the empty object, {}.
|
||||
* @return {Promise} Resolves: the empty object, {}.
|
||||
*/
|
||||
MatrixClient.prototype.setRoomReadMarkers = async function(
|
||||
roomId, rmEventId, rrEvent, opts,
|
||||
@@ -2795,7 +2820,7 @@ MatrixClient.prototype.setRoomReadMarkers = async function(
|
||||
* recent one preceding this timestamp if available, or failing that the next
|
||||
* most recent available preview.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: Object of OG metadata.
|
||||
* @return {Promise} Resolves: Object of OG metadata.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* May return synthesized attributes if the URL lacked OG meta.
|
||||
*/
|
||||
@@ -2826,7 +2851,7 @@ MatrixClient.prototype.getUrlPreview = function(url, ts, callback) {
|
||||
* @param {boolean} isTyping
|
||||
* @param {Number} timeoutMs
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.sendTyping = function(roomId, isTyping, timeoutMs, callback) {
|
||||
@@ -2934,7 +2959,7 @@ MatrixClient.prototype.getRoomUpgradeHistory = function(roomId, verifyLinks=fals
|
||||
* @param {string} roomId
|
||||
* @param {string} userId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.invite = function(roomId, userId, callback) {
|
||||
@@ -2947,7 +2972,7 @@ MatrixClient.prototype.invite = function(roomId, userId, callback) {
|
||||
* @param {string} roomId The room to invite the user to.
|
||||
* @param {string} email The email address to invite.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.inviteByEmail = function(roomId, email, callback) {
|
||||
@@ -2962,7 +2987,7 @@ MatrixClient.prototype.inviteByEmail = function(roomId, email, callback) {
|
||||
* @param {string} medium The medium to invite the user e.g. "email".
|
||||
* @param {string} address The address for the specified medium.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.inviteByThreePid = async function(
|
||||
@@ -3006,7 +3031,7 @@ MatrixClient.prototype.inviteByThreePid = async function(
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.leave = function(roomId, callback) {
|
||||
@@ -3022,7 +3047,7 @@ MatrixClient.prototype.leave = function(roomId, callback) {
|
||||
* @param {string} roomId The room ID to start leaving at
|
||||
* @param {boolean} includeFuture If true, the whole chain (past and future) of
|
||||
* upgraded rooms will be left.
|
||||
* @return {module:client.Promise} Resolves when completed with an object keyed
|
||||
* @return {Promise} Resolves when completed with an object keyed
|
||||
* by room ID and value of the error encountered when leaving or null.
|
||||
*/
|
||||
MatrixClient.prototype.leaveRoomChain = function(roomId, includeFuture=true) {
|
||||
@@ -3063,7 +3088,7 @@ MatrixClient.prototype.leaveRoomChain = function(roomId, includeFuture=true) {
|
||||
* @param {string} userId
|
||||
* @param {string} reason Optional.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.ban = function(roomId, userId, reason, callback) {
|
||||
@@ -3076,7 +3101,7 @@ MatrixClient.prototype.ban = function(roomId, userId, reason, callback) {
|
||||
* @param {boolean} deleteRoom True to delete the room from the store on success.
|
||||
* Default: true.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.forget = function(roomId, deleteRoom, callback) {
|
||||
@@ -3100,7 +3125,7 @@ MatrixClient.prototype.forget = function(roomId, deleteRoom, callback) {
|
||||
* @param {string} roomId
|
||||
* @param {string} userId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: Object (currently empty)
|
||||
* @return {Promise} Resolves: Object (currently empty)
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.unban = function(roomId, userId, callback) {
|
||||
@@ -3125,7 +3150,7 @@ MatrixClient.prototype.unban = function(roomId, userId, callback) {
|
||||
* @param {string} userId
|
||||
* @param {string} reason Optional.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.kick = function(roomId, userId, reason, callback) {
|
||||
@@ -3142,7 +3167,7 @@ MatrixClient.prototype.kick = function(roomId, userId, reason, callback) {
|
||||
* @param {string} membershipValue
|
||||
* @param {string} reason
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
function _setMembershipState(client, roomId, userId, membershipValue, reason,
|
||||
@@ -3170,7 +3195,7 @@ function _setMembershipState(client, roomId, userId, membershipValue, reason,
|
||||
* @param {string} membership
|
||||
* @param {string} reason
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
function _membershipChange(client, roomId, userId, membership, reason, callback) {
|
||||
@@ -3210,7 +3235,7 @@ MatrixClient.prototype.getPushActionsForEvent = function(event) {
|
||||
* @param {string} info The kind of info to set (e.g. 'avatar_url')
|
||||
* @param {Object} data The JSON object to set.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.setProfileInfo = function(info, data, callback) {
|
||||
@@ -3226,7 +3251,7 @@ MatrixClient.prototype.setProfileInfo = function(info, data, callback) {
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.setDisplayName = function(name, callback) {
|
||||
@@ -3238,7 +3263,7 @@ MatrixClient.prototype.setDisplayName = function(name, callback) {
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.setAvatarUrl = function(url, callback) {
|
||||
@@ -3271,7 +3296,7 @@ MatrixClient.prototype.mxcUrlToHttp =
|
||||
* Sets a new status message for the user. The message may be null/falsey
|
||||
* to clear the message.
|
||||
* @param {string} newMessage The new message to set.
|
||||
* @return {module:client.Promise} Resolves: to nothing
|
||||
* @return {Promise} Resolves: to nothing
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype._unstable_setStatusMessage = function(newMessage) {
|
||||
@@ -3298,7 +3323,7 @@ MatrixClient.prototype._unstable_setStatusMessage = function(newMessage) {
|
||||
* @param {string} opts.presence One of "online", "offline" or "unavailable"
|
||||
* @param {string} opts.status_msg The status message to attach.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @throws If 'presence' isn't a valid presence enum value.
|
||||
*/
|
||||
@@ -3330,7 +3355,7 @@ function _presenceList(callback, client, opts, method) {
|
||||
/**
|
||||
* Retrieve current user presence list.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.getPresenceList = function(callback) {
|
||||
@@ -3341,7 +3366,7 @@ MatrixClient.prototype.getPresenceList = function(callback) {
|
||||
* Add users to the current user presence list.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @param {string[]} userIds
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.inviteToPresenceList = function(callback, userIds) {
|
||||
@@ -3353,7 +3378,7 @@ MatrixClient.prototype.inviteToPresenceList = function(callback, userIds) {
|
||||
* Drop users from the current user presence list.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @param {string[]} userIds
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
**/
|
||||
MatrixClient.prototype.dropFromPresenceList = function(callback, userIds) {
|
||||
@@ -3373,7 +3398,7 @@ MatrixClient.prototype.dropFromPresenceList = function(callback, userIds) {
|
||||
* @param {Integer} limit Optional. The maximum number of previous events to
|
||||
* pull in. Default: 30.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: Room. If you are at the beginning
|
||||
* @return {Promise} Resolves: Room. If you are at the beginning
|
||||
* of the timeline, <code>Room.oldState.paginationToken</code> will be
|
||||
* <code>null</code>.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
@@ -3456,7 +3481,7 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) {
|
||||
* @param {EventTimelineSet} timelineSet The timelineSet to look for the event in
|
||||
* @param {string} eventId The ID of the event to look for
|
||||
*
|
||||
* @return {module:client.Promise} Resolves:
|
||||
* @return {Promise} Resolves:
|
||||
* {@link module:models/event-timeline~EventTimeline} including the given
|
||||
* event
|
||||
*/
|
||||
@@ -3586,7 +3611,7 @@ function(roomId, fromToken, limit, dir, timelineFilter = undefined) {
|
||||
* false to go forwards
|
||||
* @param {number} [opts.limit = 30] number of events to request
|
||||
*
|
||||
* @return {module:client.Promise} Resolves to a boolean: false if there are no
|
||||
* @return {Promise} Resolves to a boolean: false if there are no
|
||||
* events and we reached either end of the timeline; else true.
|
||||
*/
|
||||
MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
|
||||
@@ -3738,7 +3763,7 @@ MatrixClient.prototype.resetNotifTimelineSet = function() {
|
||||
* Peek into a room and receive updates about the room. This only works if the
|
||||
* history visibility for the room is world_readable.
|
||||
* @param {String} roomId The room to attempt to peek into.
|
||||
* @return {module:client.Promise} Resolves: Room object
|
||||
* @return {Promise} Resolves: Room object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.peekInRoom = function(roomId) {
|
||||
@@ -3769,7 +3794,7 @@ MatrixClient.prototype.stopPeeking = function() {
|
||||
* @param {boolean} opts.allowRead True to set history visibility to
|
||||
* be world_readable. This gives guests read access *from this point forward*.
|
||||
* If false or not given, history visibility is not modified.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.setGuestAccess = function(roomId, opts) {
|
||||
@@ -3802,7 +3827,7 @@ MatrixClient.prototype.setGuestAccess = function(roomId, opts) {
|
||||
* @param {string} clientSecret As requestEmailToken
|
||||
* @param {number} sendAttempt As requestEmailToken
|
||||
* @param {string} nextLink As requestEmailToken
|
||||
* @return {module:client.Promise} Resolves: As requestEmailToken
|
||||
* @return {Promise} Resolves: As requestEmailToken
|
||||
*/
|
||||
MatrixClient.prototype.requestRegisterEmailToken = function(email, clientSecret,
|
||||
sendAttempt, nextLink) {
|
||||
@@ -3829,7 +3854,7 @@ MatrixClient.prototype.requestRegisterEmailToken = function(email, clientSecret,
|
||||
* @param {string} clientSecret As requestEmailToken
|
||||
* @param {number} sendAttempt As requestEmailToken
|
||||
* @param {string} nextLink As requestEmailToken
|
||||
* @return {module:client.Promise} Resolves: As requestEmailToken
|
||||
* @return {Promise} Resolves: As requestEmailToken
|
||||
*/
|
||||
MatrixClient.prototype.requestRegisterMsisdnToken = function(phoneCountry, phoneNumber,
|
||||
clientSecret, sendAttempt, nextLink) {
|
||||
@@ -3860,7 +3885,7 @@ MatrixClient.prototype.requestRegisterMsisdnToken = function(phoneCountry, phone
|
||||
* @param {string} clientSecret As requestEmailToken
|
||||
* @param {number} sendAttempt As requestEmailToken
|
||||
* @param {string} nextLink As requestEmailToken
|
||||
* @return {module:client.Promise} Resolves: As requestEmailToken
|
||||
* @return {Promise} Resolves: As requestEmailToken
|
||||
*/
|
||||
MatrixClient.prototype.requestAdd3pidEmailToken = function(email, clientSecret,
|
||||
sendAttempt, nextLink) {
|
||||
@@ -3887,7 +3912,7 @@ MatrixClient.prototype.requestAdd3pidEmailToken = function(email, clientSecret,
|
||||
* @param {string} clientSecret As requestEmailToken
|
||||
* @param {number} sendAttempt As requestEmailToken
|
||||
* @param {string} nextLink As requestEmailToken
|
||||
* @return {module:client.Promise} Resolves: As requestEmailToken
|
||||
* @return {Promise} Resolves: As requestEmailToken
|
||||
*/
|
||||
MatrixClient.prototype.requestAdd3pidMsisdnToken = function(phoneCountry, phoneNumber,
|
||||
clientSecret, sendAttempt, nextLink) {
|
||||
@@ -3920,7 +3945,7 @@ MatrixClient.prototype.requestAdd3pidMsisdnToken = function(phoneCountry, phoneN
|
||||
* @param {number} sendAttempt As requestEmailToken
|
||||
* @param {string} nextLink As requestEmailToken
|
||||
* @param {module:client.callback} callback Optional. As requestEmailToken
|
||||
* @return {module:client.Promise} Resolves: As requestEmailToken
|
||||
* @return {Promise} Resolves: As requestEmailToken
|
||||
*/
|
||||
MatrixClient.prototype.requestPasswordEmailToken = function(email, clientSecret,
|
||||
sendAttempt, nextLink) {
|
||||
@@ -3946,7 +3971,7 @@ MatrixClient.prototype.requestPasswordEmailToken = function(email, clientSecret,
|
||||
* @param {string} clientSecret As requestEmailToken
|
||||
* @param {number} sendAttempt As requestEmailToken
|
||||
* @param {string} nextLink As requestEmailToken
|
||||
* @return {module:client.Promise} Resolves: As requestEmailToken
|
||||
* @return {Promise} Resolves: As requestEmailToken
|
||||
*/
|
||||
MatrixClient.prototype.requestPasswordMsisdnToken = function(phoneCountry, phoneNumber,
|
||||
clientSecret, sendAttempt, nextLink) {
|
||||
@@ -3968,7 +3993,7 @@ MatrixClient.prototype.requestPasswordMsisdnToken = function(phoneCountry, phone
|
||||
*
|
||||
* @param {string} endpoint The endpoint to send the request to
|
||||
* @param {object} params Parameters for the POST request
|
||||
* @return {module:client.Promise} Resolves: As requestEmailToken
|
||||
* @return {Promise} Resolves: As requestEmailToken
|
||||
*/
|
||||
MatrixClient.prototype._requestTokenFromEndpoint = async function(endpoint, params) {
|
||||
const postParams = Object.assign({}, params);
|
||||
@@ -4033,7 +4058,7 @@ MatrixClient.prototype.getRoomPushRule = function(scope, roomId) {
|
||||
* @param {string} scope "global" or device-specific.
|
||||
* @param {string} roomId the id of the room.
|
||||
* @param {string} mute the mute state.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) {
|
||||
@@ -4113,7 +4138,7 @@ MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) {
|
||||
* @param {string=} opts.keys The keys to search on. Defaults to all keys. One
|
||||
* of "content.body", "content.name", "content.topic".
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.searchMessageText = function(opts, callback) {
|
||||
@@ -4151,7 +4176,7 @@ MatrixClient.prototype.searchMessageText = function(opts, callback) {
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.term the term to search for
|
||||
* @param {Object} opts.filter a JSON filter object to pass in the request
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.searchRoomEvents = function(opts) {
|
||||
@@ -4187,7 +4212,7 @@ MatrixClient.prototype.searchRoomEvents = function(opts) {
|
||||
* Take a result from an earlier searchRoomEvents call, and backfill results.
|
||||
*
|
||||
* @param {object} searchResults the results object to be updated
|
||||
* @return {module:client.Promise} Resolves: updated result object
|
||||
* @return {Promise} Resolves: updated result object
|
||||
* @return {Error} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.backPaginateRoomEventsSearch = function(searchResults) {
|
||||
@@ -4257,7 +4282,7 @@ MatrixClient.prototype._processRoomEventsSearch = function(searchResults, respon
|
||||
|
||||
/**
|
||||
* Populate the store with rooms the user has left.
|
||||
* @return {module:client.Promise} Resolves: TODO - Resolved when the rooms have
|
||||
* @return {Promise} Resolves: TODO - Resolved when the rooms have
|
||||
* been added to the data store.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
@@ -4316,7 +4341,7 @@ MatrixClient.prototype.createFilter = function(content) {
|
||||
* @param {string} filterId The filter ID to retrieve
|
||||
* @param {boolean} allowCached True to allow cached filters to be returned.
|
||||
* Default: True.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.getFilter = function(userId, filterId, allowCached) {
|
||||
@@ -4415,7 +4440,7 @@ MatrixClient.prototype.getOrCreateFilter = function(filterName, filter) {
|
||||
* Gets a bearer token from the Home Server that the user can
|
||||
* present to a third party in order to prove their ownership
|
||||
* of the Matrix account they are logged into.
|
||||
* @return {module:client.Promise} Resolves: Token object
|
||||
* @return {Promise} Resolves: Token object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.getOpenIdToken = function() {
|
||||
@@ -4434,7 +4459,7 @@ MatrixClient.prototype.getOpenIdToken = function() {
|
||||
|
||||
/**
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.turnServer = function(callback) {
|
||||
@@ -4691,6 +4716,7 @@ MatrixClient.prototype.isVersionSupported = async function(version) {
|
||||
*/
|
||||
MatrixClient.prototype.doesServerSupportLazyLoading = async function() {
|
||||
const response = await this.getVersions();
|
||||
if (!response) return false;
|
||||
|
||||
const versions = response["versions"];
|
||||
const unstableFeatures = response["unstable_features"];
|
||||
@@ -4706,6 +4732,7 @@ MatrixClient.prototype.doesServerSupportLazyLoading = async function() {
|
||||
*/
|
||||
MatrixClient.prototype.doesServerRequireIdServerParam = async function() {
|
||||
const response = await this.getVersions();
|
||||
if (!response) return true;
|
||||
|
||||
const versions = response["versions"];
|
||||
|
||||
@@ -4715,6 +4742,7 @@ MatrixClient.prototype.doesServerRequireIdServerParam = async function() {
|
||||
}
|
||||
|
||||
const unstableFeatures = response["unstable_features"];
|
||||
if (!unstableFeatures) return true;
|
||||
if (unstableFeatures["m.require_identity_server"] === undefined) {
|
||||
return true;
|
||||
} else {
|
||||
@@ -4730,10 +4758,10 @@ MatrixClient.prototype.doesServerRequireIdServerParam = async function() {
|
||||
*/
|
||||
MatrixClient.prototype.doesServerAcceptIdentityAccessToken = async function() {
|
||||
const response = await this.getVersions();
|
||||
if (!response) return false;
|
||||
|
||||
const versions = response["versions"];
|
||||
const unstableFeatures = response["unstable_features"];
|
||||
|
||||
return (versions && versions.includes("r0.6.0"))
|
||||
|| (unstableFeatures && unstableFeatures["m.id_access_token"]);
|
||||
};
|
||||
@@ -4746,6 +4774,7 @@ MatrixClient.prototype.doesServerAcceptIdentityAccessToken = async function() {
|
||||
*/
|
||||
MatrixClient.prototype.doesServerSupportSeparateAddAndBind = async function() {
|
||||
const response = await this.getVersions();
|
||||
if (!response) return false;
|
||||
|
||||
const versions = response["versions"];
|
||||
const unstableFeatures = response["unstable_features"];
|
||||
@@ -5530,14 +5559,3 @@ MatrixClient.prototype.generateClientSecret = function() {
|
||||
* @param {Object} err The error value, the "rejected" value or null.
|
||||
* @param {Object} data The data returned, the "resolved" value.
|
||||
*/
|
||||
|
||||
/**
|
||||
* {@link https://github.com/kriskowal/q|A promise implementation (Q)}. Functions
|
||||
* which return this will specify 2 return arguments. These arguments map to the
|
||||
* "onFulfilled" and "onRejected" values of the Promise.
|
||||
* @typedef {Object} Promise
|
||||
* @static
|
||||
* @property {Function} then promise.then(onFulfilled, onRejected, onProgress)
|
||||
* @property {Function} catch promise.catch(onRejected)
|
||||
* @property {Function} finally promise.finally(callback)
|
||||
*/
|
||||
|
||||
@@ -348,6 +348,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
|
||||
async signUser(key) {
|
||||
if (!this.keys.user_signing) {
|
||||
logger.info("No user signing key: not signing user");
|
||||
return;
|
||||
}
|
||||
return this.signObject(key.keys.master, "user_signing");
|
||||
@@ -360,6 +361,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
);
|
||||
}
|
||||
if (!this.keys.self_signing) {
|
||||
logger.info("No self signing key: not signing device");
|
||||
return;
|
||||
}
|
||||
return this.signObject(
|
||||
|
||||
@@ -594,7 +594,7 @@ export class DeviceList extends EventEmitter {
|
||||
*
|
||||
* @param {String[]} users list of userIds
|
||||
*
|
||||
* @return {module:client.Promise} resolves when all the users listed have
|
||||
* @return {Promise} resolves when all the users listed have
|
||||
* been updated. rejects if there was a problem updating any of the
|
||||
* users.
|
||||
*/
|
||||
@@ -699,7 +699,7 @@ class DeviceListUpdateSerialiser {
|
||||
* @param {String} syncToken sync token to pass in the query request, to
|
||||
* help the HS give the most recent results
|
||||
*
|
||||
* @return {module:client.Promise} resolves when all the users listed have
|
||||
* @return {Promise} resolves when all the users listed have
|
||||
* been updated. rejects if there was a problem updating any of the
|
||||
* users.
|
||||
*/
|
||||
|
||||
+131
-14
@@ -111,16 +111,52 @@ export function OlmDevice(cryptoStore) {
|
||||
* Initialise the OlmAccount. This must be called before any other operations
|
||||
* on the OlmDevice.
|
||||
*
|
||||
* Data from an exported Olm device can be provided
|
||||
* in order to re-create this device.
|
||||
*
|
||||
* Attempts to load the OlmAccount from the crypto store, or creates one if none is
|
||||
* found.
|
||||
*
|
||||
* Reads the device keys from the OlmAccount object.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {object} opts.fromExportedDevice (Optional) data from exported device
|
||||
* that must be re-created.
|
||||
* If present, opts.pickleKey is ignored
|
||||
* (exported data already provides a pickle key)
|
||||
* @param {object} opts.pickleKey (Optional) pickle key to set instead of default one
|
||||
*/
|
||||
OlmDevice.prototype.init = async function() {
|
||||
OlmDevice.prototype.init = async function(opts = {}) {
|
||||
let e2eKeys;
|
||||
const account = new global.Olm.Account();
|
||||
|
||||
const { pickleKey, fromExportedDevice } = opts;
|
||||
|
||||
try {
|
||||
await _initialiseAccount(this._cryptoStore, this._pickleKey, account);
|
||||
if (fromExportedDevice) {
|
||||
if (pickleKey) {
|
||||
console.warn(
|
||||
'ignoring opts.pickleKey'
|
||||
+ ' because opts.fromExportedDevice is present.',
|
||||
);
|
||||
}
|
||||
this._pickleKey = fromExportedDevice.pickleKey;
|
||||
await _initialiseFromExportedDevice(
|
||||
fromExportedDevice,
|
||||
this._cryptoStore,
|
||||
this._pickleKey,
|
||||
account,
|
||||
);
|
||||
} else {
|
||||
if (pickleKey) {
|
||||
this._pickleKey = pickleKey;
|
||||
}
|
||||
await _initialiseAccount(
|
||||
this._cryptoStore,
|
||||
this._pickleKey,
|
||||
account,
|
||||
);
|
||||
}
|
||||
e2eKeys = JSON.parse(account.identity_keys());
|
||||
|
||||
this._maxOneTimeKeys = account.max_number_of_one_time_keys();
|
||||
@@ -132,18 +168,67 @@ OlmDevice.prototype.init = async function() {
|
||||
this.deviceEd25519Key = e2eKeys.ed25519;
|
||||
};
|
||||
|
||||
async function _initialiseAccount(cryptoStore, pickleKey, account) {
|
||||
await cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||
cryptoStore.getAccount(txn, (pickledAccount) => {
|
||||
if (pickledAccount !== null) {
|
||||
account.unpickle(pickleKey, pickledAccount);
|
||||
} else {
|
||||
account.create();
|
||||
pickledAccount = account.pickle(pickleKey);
|
||||
cryptoStore.storeAccount(txn, pickledAccount);
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Populates the crypto store using data that was exported from an existing device.
|
||||
* Note that for now only the “account” and “sessions” stores are populated;
|
||||
* Other stores will be as with a new device.
|
||||
*
|
||||
* @param {Object} exportedData Data exported from another device
|
||||
* through the “export” method.
|
||||
* @param {module:crypto/store/base~CryptoStore} cryptoStore storage for the crypto layer
|
||||
* @param {string} pickleKey the key that was used to pickle the exported data
|
||||
* @param {Olm.Account} account an olm account to initialize
|
||||
*/
|
||||
async function _initialiseFromExportedDevice(
|
||||
exportedData,
|
||||
cryptoStore,
|
||||
pickleKey,
|
||||
account,
|
||||
) {
|
||||
await cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[
|
||||
IndexedDBCryptoStore.STORE_ACCOUNT,
|
||||
IndexedDBCryptoStore.STORE_SESSIONS,
|
||||
],
|
||||
(txn) => {
|
||||
cryptoStore.storeAccount(txn, exportedData.pickledAccount);
|
||||
exportedData.sessions.forEach((session) => {
|
||||
const {
|
||||
deviceKey,
|
||||
sessionId,
|
||||
} = session;
|
||||
const sessionInfo = {
|
||||
session: session.session,
|
||||
lastReceivedMessageTs: session.lastReceivedMessageTs,
|
||||
};
|
||||
cryptoStore.storeEndToEndSession(
|
||||
deviceKey,
|
||||
sessionId,
|
||||
sessionInfo,
|
||||
txn,
|
||||
);
|
||||
});
|
||||
});
|
||||
account.unpickle(pickleKey, exportedData.pickledAccount);
|
||||
}
|
||||
|
||||
async function _initialiseAccount(cryptoStore, pickleKey, account) {
|
||||
await cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
cryptoStore.getAccount(txn, (pickledAccount) => {
|
||||
if (pickledAccount !== null) {
|
||||
account.unpickle(pickleKey, pickledAccount);
|
||||
} else {
|
||||
account.create();
|
||||
pickledAccount = account.pickle(pickleKey);
|
||||
cryptoStore.storeAccount(txn, pickledAccount);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,6 +276,38 @@ OlmDevice.prototype._storeAccount = function(txn, account) {
|
||||
this._cryptoStore.storeAccount(txn, account.pickle(this._pickleKey));
|
||||
};
|
||||
|
||||
/**
|
||||
* Export data for re-creating the Olm device later.
|
||||
* TODO export data other than just account and (P2P) sessions.
|
||||
*
|
||||
* @return {Promise<object>} The exported data
|
||||
*/
|
||||
OlmDevice.prototype.export = async function() {
|
||||
const result = {
|
||||
pickleKey: this._pickleKey,
|
||||
};
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly',
|
||||
[
|
||||
IndexedDBCryptoStore.STORE_ACCOUNT,
|
||||
IndexedDBCryptoStore.STORE_SESSIONS,
|
||||
],
|
||||
(txn) => {
|
||||
this._cryptoStore.getAccount(txn, (pickledAccount) => {
|
||||
result.pickledAccount = pickledAccount;
|
||||
});
|
||||
result.sessions = [];
|
||||
// Note that the pickledSession object we get in the callback
|
||||
// is not exactly the same thing you get in method _getSession
|
||||
// see documentation of IndexedDBCryptoStore.getAllEndToEndSessions
|
||||
this._cryptoStore.getAllEndToEndSessions(txn, (pickledSession) => {
|
||||
result.sessions.push(pickledSession);
|
||||
});
|
||||
},
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* extract an OlmSession from the session store and call the given function
|
||||
* The session is useable only within the callback passed to this
|
||||
@@ -1110,7 +1227,7 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
*
|
||||
* @param {string} roomId room in which the message was received
|
||||
* @param {string} senderKey base64-encoded curve25519 key of the sender
|
||||
* @param {sring} sessionId session identifier
|
||||
* @param {string} sessionId session identifier
|
||||
*
|
||||
* @returns {Promise<boolean>} true if we have the keys to this session
|
||||
*/
|
||||
|
||||
+59
-12
@@ -253,12 +253,41 @@ export class SecretStorage extends EventEmitter {
|
||||
*/
|
||||
storePassthrough(name, keyId) {
|
||||
return this._baseApis.setAccountData(name, {
|
||||
[keyId]: {
|
||||
passthrough: true,
|
||||
encrypted: {
|
||||
[keyId]: {
|
||||
passthrough: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary method to fix up existing accounts where secrets
|
||||
* are incorrectly stored without the 'encrypted' level
|
||||
*
|
||||
* @param {string} name The name of the secret
|
||||
* @param {object} secretInfo The account data object
|
||||
* @returns {object} The fixed object or null if no fix was performed
|
||||
*/
|
||||
async _fixupStoredSecret(name, secretInfo) {
|
||||
// We assume the secret was only stored passthrough for 1
|
||||
// key - this was all the broken code supported.
|
||||
const keys = Object.keys(secretInfo);
|
||||
if (
|
||||
keys.length === 1 && keys[0] !== 'encrypted' &&
|
||||
secretInfo[keys[0]].passthrough
|
||||
) {
|
||||
const hasKey = await this.hasKey(keys[0]);
|
||||
if (hasKey) {
|
||||
console.log("Fixing up passthrough secret: " + name);
|
||||
await this.storePassthrough(name, keys[0]);
|
||||
const newData = await this._baseApis.getAccountDataFromServer(name);
|
||||
return newData;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a secret from storage.
|
||||
*
|
||||
@@ -267,12 +296,16 @@ export class SecretStorage extends EventEmitter {
|
||||
* @return {string} the contents of the secret
|
||||
*/
|
||||
async get(name) {
|
||||
const secretInfo = await this._baseApis.getAccountDataFromServer(name);
|
||||
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
|
||||
if (!secretInfo) {
|
||||
return;
|
||||
}
|
||||
if (!secretInfo.encrypted) {
|
||||
throw new Error("Content is not encrypted!");
|
||||
// try to fix it up
|
||||
secretInfo = await this._fixupStoredSecret(name, secretInfo);
|
||||
if (!secretInfo || !secretInfo.encrypted) {
|
||||
throw new Error("Content is not encrypted!");
|
||||
}
|
||||
}
|
||||
|
||||
// get possible keys to decrypt
|
||||
@@ -285,8 +318,12 @@ export class SecretStorage extends EventEmitter {
|
||||
const encInfo = secretInfo.encrypted[keyId];
|
||||
switch (keyInfo.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
|
||||
&& encInfo.ephemeral) {
|
||||
if (
|
||||
keyInfo.pubkey && (
|
||||
(encInfo.ciphertext && encInfo.mac && encInfo.ephemeral) ||
|
||||
encInfo.passthrough
|
||||
)
|
||||
) {
|
||||
keys[keyId] = keyInfo;
|
||||
}
|
||||
break;
|
||||
@@ -299,7 +336,7 @@ export class SecretStorage extends EventEmitter {
|
||||
let decryption;
|
||||
try {
|
||||
// fetch private key from app
|
||||
[keyId, decryption] = await this._getSecretStorageKey(keys);
|
||||
[keyId, decryption] = await this._getSecretStorageKey(keys, name);
|
||||
|
||||
const encInfo = secretInfo.encrypted[keyId];
|
||||
|
||||
@@ -329,9 +366,14 @@ export class SecretStorage extends EventEmitter {
|
||||
*/
|
||||
async isStored(name, checkKey) {
|
||||
// check if secret exists
|
||||
const secretInfo = await this._baseApis.getAccountDataFromServer(name);
|
||||
if (!secretInfo || !secretInfo.encrypted) {
|
||||
return false;
|
||||
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
|
||||
if (!secretInfo) return false;
|
||||
if (!secretInfo.encrypted) {
|
||||
// try to fix it up
|
||||
secretInfo = await this._fixupStoredSecret(name, secretInfo);
|
||||
if (!secretInfo || !secretInfo.encrypted) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkKey === undefined) checkKey = true;
|
||||
@@ -352,6 +394,11 @@ export class SecretStorage extends EventEmitter {
|
||||
this._crossSigningInfo.userId,
|
||||
);
|
||||
}
|
||||
|
||||
// We don't actually need the decryption object if it's a passthrough
|
||||
// since we just want to return the key itself.
|
||||
if (encInfo.passthrough) return true;
|
||||
|
||||
switch (keyInfo.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
|
||||
@@ -540,12 +587,12 @@ export class SecretStorage extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async _getSecretStorageKey(keys) {
|
||||
async _getSecretStorageKey(keys, name) {
|
||||
if (!this._cryptoCallbacks.getSecretStorageKey) {
|
||||
throw new Error("No getSecretStorageKey callback supplied");
|
||||
}
|
||||
|
||||
const returned = await this._cryptoCallbacks.getSecretStorageKey({ keys });
|
||||
const returned = await this._cryptoCallbacks.getSecretStorageKey({ keys }, name);
|
||||
|
||||
if (!returned) {
|
||||
throw new Error("getSecretStorageKey callback returned falsey");
|
||||
|
||||
@@ -70,7 +70,7 @@ export class EncryptionAlgorithm {
|
||||
* @param {string} eventType
|
||||
* @param {object} plaintext event content
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the new event body
|
||||
* @return {Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -186,7 +186,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
|
||||
* @param {Object} devicesInRoom The devices in this room, indexed by user ID
|
||||
* @param {Object} blocked The devices that are blocked, indexed by user ID
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the
|
||||
* @return {Promise} Promise which resolves to the
|
||||
* OutboundSessionInfo when setup is complete.
|
||||
*/
|
||||
MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
@@ -497,7 +497,7 @@ MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
|
||||
*
|
||||
* @param {object} payload fields to include in the encrypted payload
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves once the key sharing
|
||||
* @return {Promise} Promise which resolves once the key sharing
|
||||
* for the given userDeviceMap is generated and has been sent.
|
||||
*/
|
||||
MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
|
||||
@@ -558,7 +558,7 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
|
||||
*
|
||||
* @param {object} payload fields to include in the notification payload
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves once the notifications
|
||||
* @return {Promise} Promise which resolves once the notifications
|
||||
* for the given userDeviceMap is generated and has been sent.
|
||||
*/
|
||||
MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function(
|
||||
@@ -779,7 +779,7 @@ MegolmEncryption.prototype._notifyBlockedDevices = async function(
|
||||
* @param {string} eventType
|
||||
* @param {object} content plaintext event content
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the new event body
|
||||
* @return {Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
MegolmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
|
||||
const self = this;
|
||||
@@ -866,7 +866,7 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) {
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to an array whose
|
||||
* @return {Promise} Promise which resolves to an array whose
|
||||
* first element is a map from userId to deviceId to deviceInfo indicating
|
||||
* the devices that messages should be encrypted to, and whose second
|
||||
* element is a map from userId to deviceId to data indicating the devices
|
||||
|
||||
@@ -54,7 +54,7 @@ utils.inherits(OlmEncryption, EncryptionAlgorithm);
|
||||
* @private
|
||||
|
||||
* @param {string[]} roomMembers list of currently-joined users in the room
|
||||
* @return {module:client.Promise} Promise which resolves when setup is complete
|
||||
* @return {Promise} Promise which resolves when setup is complete
|
||||
*/
|
||||
OlmEncryption.prototype._ensureSession = function(roomMembers) {
|
||||
if (this._prepPromise) {
|
||||
@@ -85,7 +85,7 @@ OlmEncryption.prototype._ensureSession = function(roomMembers) {
|
||||
* @param {string} eventType
|
||||
* @param {object} content plaintext event content
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the new event body
|
||||
* @return {Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
OlmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
|
||||
// pick the list of recipients based on the membership list.
|
||||
|
||||
+179
-148
@@ -41,29 +41,38 @@ import {
|
||||
import {SECRET_STORAGE_ALGORITHM_V1, SecretStorage} from './SecretStorage';
|
||||
import {OutgoingRoomKeyRequestManager} from './OutgoingRoomKeyRequestManager';
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
import {ScanQRCode, ShowQRCode} from './verification/QRCode';
|
||||
import {
|
||||
ReciprocateQRCode,
|
||||
SCAN_QR_CODE_METHOD,
|
||||
SHOW_QR_CODE_METHOD,
|
||||
} from './verification/QRCode';
|
||||
import {SAS} from './verification/SAS';
|
||||
import {keyFromPassphrase} from './key_passphrase';
|
||||
import {encodeRecoveryKey} from './recoverykey';
|
||||
import {VerificationRequest} from "./verification/request/VerificationRequest";
|
||||
import {InRoomChannel} from "./verification/request/InRoomChannel";
|
||||
import {ToDeviceChannel} from "./verification/request/ToDeviceChannel";
|
||||
import {InRoomChannel, InRoomRequests} from "./verification/request/InRoomChannel";
|
||||
import {ToDeviceChannel, ToDeviceRequests} from "./verification/request/ToDeviceChannel";
|
||||
import * as httpApi from "../http-api";
|
||||
import {IllegalMethod} from "./verification/IllegalMethod";
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
const defaultVerificationMethods = {
|
||||
[ScanQRCode.NAME]: ScanQRCode,
|
||||
[ShowQRCode.NAME]: ShowQRCode,
|
||||
[ReciprocateQRCode.NAME]: ReciprocateQRCode,
|
||||
[SAS.NAME]: SAS,
|
||||
|
||||
// These two can't be used for actual verification, but we do
|
||||
// need to be able to define them here for the verification flows
|
||||
// to start.
|
||||
[SHOW_QR_CODE_METHOD]: IllegalMethod,
|
||||
[SCAN_QR_CODE_METHOD]: IllegalMethod,
|
||||
};
|
||||
|
||||
/**
|
||||
* verification method names
|
||||
*/
|
||||
export const verificationMethods = {
|
||||
QR_CODE_SCAN: ScanQRCode.NAME,
|
||||
QR_CODE_SHOW: ShowQRCode.NAME,
|
||||
RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME,
|
||||
SAS: SAS.NAME,
|
||||
};
|
||||
|
||||
@@ -117,8 +126,8 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
this._clientStore = clientStore;
|
||||
this._cryptoStore = cryptoStore;
|
||||
this._roomList = roomList;
|
||||
this._verificationMethods = new Map();
|
||||
if (verificationMethods) {
|
||||
this._verificationMethods = new Map();
|
||||
for (const method of verificationMethods) {
|
||||
if (typeof method === "string") {
|
||||
if (defaultVerificationMethods[method]) {
|
||||
@@ -132,10 +141,13 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
method.NAME,
|
||||
method,
|
||||
);
|
||||
} else {
|
||||
console.warn(`Excluding unknown verification method ${method}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._verificationMethods = defaultVerificationMethods;
|
||||
}
|
||||
|
||||
// track whether this device's megolm keys are being backed up incrementally
|
||||
// to the server or not.
|
||||
// XXX: this should probably have a single source of truth from OlmAccount
|
||||
@@ -204,8 +216,8 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
// }
|
||||
this._lastNewSessionForced = {};
|
||||
|
||||
this._toDeviceVerificationRequests = new Map();
|
||||
this._inRoomVerificationRequests = new Map();
|
||||
this._toDeviceVerificationRequests = new ToDeviceRequests();
|
||||
this._inRoomVerificationRequests = new InRoomRequests();
|
||||
|
||||
const cryptoCallbacks = this._baseApis._cryptoCallbacks || {};
|
||||
|
||||
@@ -228,12 +240,24 @@ utils.inherits(Crypto, EventEmitter);
|
||||
* Initialise the crypto module so that it is ready for use
|
||||
*
|
||||
* Returns a promise which resolves once the crypto module is ready for use.
|
||||
*
|
||||
* @param {Object} opts keyword arguments.
|
||||
* @param {string} opts.exportedOlmDevice (Optional) data from exported device
|
||||
* that must be re-created.
|
||||
*/
|
||||
Crypto.prototype.init = async function() {
|
||||
Crypto.prototype.init = async function(opts) {
|
||||
const {
|
||||
exportedOlmDevice,
|
||||
} = opts || {};
|
||||
|
||||
logger.log("Crypto: initialising Olm...");
|
||||
await global.Olm.init();
|
||||
logger.log("Crypto: initialising Olm device...");
|
||||
await this._olmDevice.init();
|
||||
logger.log(
|
||||
exportedOlmDevice
|
||||
? "Crypto: initialising Olm device from exported device..."
|
||||
: "Crypto: initialising Olm device...",
|
||||
);
|
||||
await this._olmDevice.init({ fromExportedDevice: exportedOlmDevice });
|
||||
logger.log("Crypto: loading device list...");
|
||||
await this._deviceList.load();
|
||||
|
||||
@@ -336,6 +360,10 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) {
|
||||
* called to await a secret storage key creation flow.
|
||||
* @param {object} [opts.keyBackupInfo] The current key backup object. If passed,
|
||||
* the passphrase and recovery key from this backup will be used.
|
||||
* @param {bool} [opts.setupNewKeyBackup] If true, a new key backup version will be
|
||||
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
|
||||
* is supplied.
|
||||
* @param {bool} [opts.setupNewSecretStorage] Optional. Reset even if keys already exist.
|
||||
* Returns:
|
||||
* {Promise} A promise which resolves to key creation data for
|
||||
* SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields.
|
||||
@@ -344,6 +372,8 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
authUploadDeviceSigningKeys,
|
||||
createSecretStorageKey = async () => { },
|
||||
keyBackupInfo,
|
||||
setupNewKeyBackup,
|
||||
setupNewSecretStorage,
|
||||
} = {}) {
|
||||
logger.log("Bootstrapping Secure Secret Storage");
|
||||
|
||||
@@ -351,7 +381,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
// key with the cross-signing master key. The cross-signing master key is also used
|
||||
// to verify the signature on the SSSS default key when adding secrets, so we
|
||||
// effectively need it for both reading and writing secrets.
|
||||
let crossSigningPrivateKeys = {};
|
||||
const crossSigningPrivateKeys = {};
|
||||
|
||||
// If we happen to reset cross-signing keys here, then we want access to the
|
||||
// cross-signing private keys, but only for the scope of this method, so we
|
||||
@@ -359,7 +389,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
const appCallbacks = Object.assign({}, this._baseApis._cryptoCallbacks);
|
||||
|
||||
try {
|
||||
const inStorage =
|
||||
const inStorage = !setupNewSecretStorage &&
|
||||
await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
|
||||
if (!this._crossSigningInfo.getId() || !inStorage) {
|
||||
logger.log(
|
||||
@@ -375,7 +405,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
"creating new keys",
|
||||
);
|
||||
this._baseApis._cryptoCallbacks.saveCrossSigningKeys =
|
||||
keys => crossSigningPrivateKeys = keys;
|
||||
keys => Object.assign(crossSigningPrivateKeys, keys);
|
||||
this._baseApis._cryptoCallbacks.getCrossSigningKey =
|
||||
name => crossSigningPrivateKeys[name];
|
||||
await this.resetCrossSigningKeys(
|
||||
@@ -389,7 +419,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
|
||||
// Check if Secure Secret Storage has a default key. If we don't have one, create
|
||||
// the default key (which will also be signed by the cross-signing master key).
|
||||
if (!await this.hasSecretStorageKey()) {
|
||||
if (setupNewSecretStorage || !await this.hasSecretStorageKey()) {
|
||||
let newKeyId;
|
||||
if (keyBackupInfo) {
|
||||
logger.log("Secret storage default key not found, using key backup key");
|
||||
@@ -467,8 +497,24 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (setupNewKeyBackup && !keyBackupInfo) {
|
||||
const info = await this._baseApis.prepareKeyBackupVersion(
|
||||
null /* random key */,
|
||||
{ secureSecretStorage: true },
|
||||
);
|
||||
await this._baseApis.createKeyBackupVersion(info);
|
||||
}
|
||||
} finally {
|
||||
this._baseApis._cryptoCallbacks = appCallbacks;
|
||||
// Restore the original callbacks. NB. we must do this by manipulating
|
||||
// the same object since the CrossSigning class has a reference to the
|
||||
// object, so if we assign the object here then our callbacks will change
|
||||
// but the instances of the CrossSigning class will be left with our
|
||||
// random, otherwise dead closures.
|
||||
for (const cb of Object.keys(this._baseApis._cryptoCallbacks)) {
|
||||
delete this._baseApis._cryptoCallbacks[cb];
|
||||
}
|
||||
Object.assign(this._baseApis._cryptoCallbacks, appCallbacks);
|
||||
}
|
||||
|
||||
logger.log("Secure Secret Storage ready");
|
||||
@@ -811,6 +857,10 @@ Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) {
|
||||
Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
const userId = this._userId;
|
||||
|
||||
// Before proceeding, ensure our cross-signing public keys have been
|
||||
// downloaded via the device list.
|
||||
await this.downloadKeys([this._userId]);
|
||||
|
||||
// If we see an update to our own master key, check it against the master
|
||||
// key we have and, if it matches, mark it as verified
|
||||
|
||||
@@ -850,13 +900,7 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
const oldUserSigningId = this._crossSigningInfo.getId("user_signing");
|
||||
|
||||
// Update the version of our keys in our cross-signing object and the local store
|
||||
this._crossSigningInfo.setKeys(newCrossSigning.keys);
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys);
|
||||
},
|
||||
);
|
||||
this._storeTrustedSelfKeys(newCrossSigning.keys);
|
||||
|
||||
const keySignatures = {};
|
||||
|
||||
@@ -896,6 +940,21 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
// the backup with the new key (if not already signed)?
|
||||
};
|
||||
|
||||
/**
|
||||
* Store a set of keys as our own, trusted, cross-signing keys.
|
||||
*
|
||||
* @param {object} keys The new trusted set of keys
|
||||
*/
|
||||
Crypto.prototype._storeTrustedSelfKeys = async function(keys) {
|
||||
this._crossSigningInfo.setKeys(keys);
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the master key is signed by a verified device, and if so, prompt
|
||||
* the application to mark it as verified.
|
||||
@@ -1147,17 +1206,13 @@ Crypto.prototype.registerEventHandlers = function(eventEmitter) {
|
||||
}
|
||||
});
|
||||
|
||||
eventEmitter.on("toDeviceEvent", function(event) {
|
||||
crypto._onToDeviceEvent(event);
|
||||
});
|
||||
eventEmitter.on("toDeviceEvent", crypto._onToDeviceEvent.bind(crypto));
|
||||
|
||||
eventEmitter.on("Room.timeline", function(event) {
|
||||
crypto._onTimelineEvent(event);
|
||||
});
|
||||
const timelineHandler = crypto._onTimelineEvent.bind(crypto);
|
||||
|
||||
eventEmitter.on("Event.decrypted", function(event) {
|
||||
crypto._onTimelineEvent(event);
|
||||
});
|
||||
eventEmitter.on("Room.timeline", timelineHandler);
|
||||
|
||||
eventEmitter.on("Event.decrypted", timelineHandler);
|
||||
};
|
||||
|
||||
|
||||
@@ -1248,10 +1303,6 @@ Crypto.prototype.uploadDeviceKeys = function() {
|
||||
return crypto._signObject(deviceKeys).then(() => {
|
||||
return crypto._baseApis.uploadKeysRequest({
|
||||
device_keys: deviceKeys,
|
||||
}, {
|
||||
// for now, we set the device id explicitly, as we may not be using the
|
||||
// same one as used in login.
|
||||
device_id: deviceId,
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1348,9 +1399,7 @@ function _maybeUploadOneTimeKeys(crypto) {
|
||||
return Promise.resolve(crypto._oneTimeKeyCount);
|
||||
}
|
||||
// ask the server how many keys we have
|
||||
return crypto._baseApis.uploadKeysRequest({}, {
|
||||
device_id: crypto._deviceId,
|
||||
}).then((res) => {
|
||||
return crypto._baseApis.uploadKeysRequest({}).then((res) => {
|
||||
return res.one_time_key_counts.signed_curve25519 || 0;
|
||||
});
|
||||
}).then((keyCount) => {
|
||||
@@ -1390,10 +1439,6 @@ async function _uploadOneTimeKeys(crypto) {
|
||||
|
||||
const res = await crypto._baseApis.uploadKeysRequest({
|
||||
one_time_keys: oneTimeJson,
|
||||
}, {
|
||||
// for now, we set the device id explicitly, as we may not be using the
|
||||
// same one as used in login.
|
||||
device_id: crypto._deviceId,
|
||||
});
|
||||
|
||||
await crypto._olmDevice.markKeysAsPublished();
|
||||
@@ -1492,17 +1537,32 @@ Crypto.prototype.setDeviceVerification = async function(
|
||||
if (!verified) {
|
||||
throw new Error("Cannot set a cross-signing key as unverified");
|
||||
}
|
||||
const device = await this._crossSigningInfo.signUser(xsk);
|
||||
if (device) {
|
||||
await this._baseApis.uploadKeySignatures({
|
||||
[userId]: {
|
||||
[deviceId]: device,
|
||||
},
|
||||
});
|
||||
// This will emit events when it comes back down the sync
|
||||
// (we could do local echo to speed things up)
|
||||
|
||||
if (!this._crossSigningInfo.getId() && userId === this._crossSigningInfo.userId) {
|
||||
this._storeTrustedSelfKeys(xsk.keys);
|
||||
}
|
||||
|
||||
// Now sign the master key with our user signing key (unless it's ourself)
|
||||
if (userId !== this._userId) {
|
||||
logger.info(
|
||||
"Master key " + xsk.getId() + " for " + userId +
|
||||
" marked verified. Signing...",
|
||||
);
|
||||
const device = await this._crossSigningInfo.signUser(xsk);
|
||||
if (device) {
|
||||
logger.info("Uploading signature for " + userId + "...");
|
||||
await this._baseApis.uploadKeySignatures({
|
||||
[userId]: {
|
||||
[deviceId]: device,
|
||||
},
|
||||
});
|
||||
// This will emit events when it comes back down the sync
|
||||
// (we could do local echo to speed things up)
|
||||
}
|
||||
return device;
|
||||
} else {
|
||||
return xsk;
|
||||
}
|
||||
return device;
|
||||
}
|
||||
|
||||
const devices = this._deviceList.getRawStoredDevicesForUser(userId);
|
||||
@@ -1539,10 +1599,12 @@ Crypto.prototype.setDeviceVerification = async function(
|
||||
|
||||
// do cross-signing
|
||||
if (verified && userId === this._userId) {
|
||||
logger.info("Own device " + deviceId + " marked verified: signing");
|
||||
const device = await this._crossSigningInfo.signDevice(
|
||||
userId, DeviceInfo.fromStorage(dev, deviceId),
|
||||
);
|
||||
if (device) {
|
||||
logger.info("Uploading signature for " + deviceId);
|
||||
await this._baseApis.uploadKeySignatures({
|
||||
[userId]: {
|
||||
[deviceId]: device,
|
||||
@@ -1557,98 +1619,65 @@ Crypto.prototype.setDeviceVerification = async function(
|
||||
return deviceObj;
|
||||
};
|
||||
|
||||
Crypto.prototype.requestVerificationDM = async function(userId, roomId, methods) {
|
||||
Crypto.prototype.requestVerificationDM = function(userId, roomId) {
|
||||
const channel = new InRoomChannel(this._baseApis, roomId, userId);
|
||||
const request = await this._requestVerificationWithChannel(
|
||||
return this._requestVerificationWithChannel(
|
||||
userId,
|
||||
methods,
|
||||
channel,
|
||||
this._inRoomVerificationRequests,
|
||||
);
|
||||
return await request.waitForVerifier();
|
||||
};
|
||||
|
||||
Crypto.prototype.acceptVerificationDM = function(event, method) {
|
||||
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) {
|
||||
Crypto.prototype.requestVerification = function(userId, devices) {
|
||||
if (!devices) {
|
||||
devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(userId));
|
||||
}
|
||||
const channel = new ToDeviceChannel(this._baseApis, userId, devices);
|
||||
const request = await this._requestVerificationWithChannel(
|
||||
return this._requestVerificationWithChannel(
|
||||
userId,
|
||||
methods,
|
||||
channel,
|
||||
this._toDeviceVerificationRequests,
|
||||
);
|
||||
return await request.waitForVerifier();
|
||||
};
|
||||
|
||||
Crypto.prototype._requestVerificationWithChannel = async function(
|
||||
userId, methods, channel, requestsMap,
|
||||
userId, channel, requestsMap,
|
||||
) {
|
||||
if (!methods) {
|
||||
// .keys() returns an iterator, so we need to explicitly turn it into an array
|
||||
methods = [...this._verificationMethods.keys()];
|
||||
}
|
||||
// TODO: filter by given methods
|
||||
const request = new VerificationRequest(
|
||||
channel, this._verificationMethods, userId, this._baseApis);
|
||||
let request = new VerificationRequest(
|
||||
channel, this._verificationMethods, this._baseApis);
|
||||
await request.sendRequest();
|
||||
|
||||
let requestsByTxnId = requestsMap.get(userId);
|
||||
if (!requestsByTxnId) {
|
||||
requestsByTxnId = new Map();
|
||||
requestsMap.set(userId, requestsByTxnId);
|
||||
// don't replace the request created by a racing remote echo
|
||||
const racingRequest = requestsMap.getRequestByChannel(channel);
|
||||
if (racingRequest) {
|
||||
request = racingRequest;
|
||||
} 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;
|
||||
};
|
||||
|
||||
Crypto.prototype.beginKeyVerification = function(
|
||||
method, userId, deviceId, transactionId = null,
|
||||
) {
|
||||
let requestsByTxnId = this._toDeviceVerificationRequests.get(userId);
|
||||
if (!requestsByTxnId) {
|
||||
requestsByTxnId = new Map();
|
||||
this._toDeviceVerificationRequests.set(userId, requestsByTxnId);
|
||||
}
|
||||
let request;
|
||||
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 {
|
||||
transactionId = ToDeviceChannel.makeTransactionId();
|
||||
const channel = new ToDeviceChannel(
|
||||
this._baseApis, userId, [deviceId], transactionId, deviceId);
|
||||
request = new VerificationRequest(
|
||||
channel, this._verificationMethods, userId, this._baseApis);
|
||||
requestsByTxnId.set(transactionId, request);
|
||||
}
|
||||
if (!request) {
|
||||
throw new Error(
|
||||
`No request found for user ${userId} with transactionId ${transactionId}`);
|
||||
channel, this._verificationMethods, this._baseApis);
|
||||
this._toDeviceVerificationRequests.setRequestBySenderAndTxnId(
|
||||
userId, transactionId, request);
|
||||
}
|
||||
return request.beginKeyVerification(method, {userId, deviceId});
|
||||
};
|
||||
@@ -1899,7 +1928,7 @@ Crypto.prototype.trackRoomDevices = function(roomId) {
|
||||
*
|
||||
* @param {string[]} users list of user ids
|
||||
*
|
||||
* @return {module:client.Promise} resolves once the sessions are complete, to
|
||||
* @return {Promise} resolves once the sessions are complete, to
|
||||
* an Object mapping from userId to deviceId to
|
||||
* {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
@@ -1962,7 +1991,7 @@ Crypto.prototype.exportRoomKeys = async function() {
|
||||
* Import a list of room keys previously exported by exportRoomKeys
|
||||
*
|
||||
* @param {Object[]} keys a list of session export objects
|
||||
* @return {module:client.Promise} a promise which resolves once the keys have been imported
|
||||
* @return {Promise} a promise which resolves once the keys have been imported
|
||||
*/
|
||||
Crypto.prototype.importRoomKeys = function(keys) {
|
||||
return Promise.all(keys.map((key) => {
|
||||
@@ -2158,7 +2187,7 @@ Crypto.prototype.flagAllGroupSessionsForBackup = async function() {
|
||||
*
|
||||
* @param {module:models/room} room destination room.
|
||||
*
|
||||
* @return {module:client.Promise?} Promise which resolves when the event has been
|
||||
* @return {Promise?} Promise which resolves when the event has been
|
||||
* encrypted, or null if nothing was needed
|
||||
*/
|
||||
/* eslint-enable valid-jsdoc */
|
||||
@@ -2534,7 +2563,6 @@ Crypto.prototype._onKeyVerificationMessage = function(event) {
|
||||
if (!ToDeviceChannel.validateEvent(event, this._baseApis)) {
|
||||
return;
|
||||
}
|
||||
const transactionId = ToDeviceChannel.getTransactionId(event);
|
||||
const createRequest = event => {
|
||||
if (!ToDeviceChannel.canCreateRequest(ToDeviceChannel.getEventType(event))) {
|
||||
return;
|
||||
@@ -2551,10 +2579,13 @@ Crypto.prototype._onKeyVerificationMessage = function(event) {
|
||||
[deviceId],
|
||||
);
|
||||
return new VerificationRequest(
|
||||
channel, this._verificationMethods, userId, this._baseApis);
|
||||
channel, this._verificationMethods, this._baseApis);
|
||||
};
|
||||
this._handleVerificationEvent(event, transactionId,
|
||||
this._toDeviceVerificationRequests, createRequest);
|
||||
this._handleVerificationEvent(
|
||||
event,
|
||||
this._toDeviceVerificationRequests,
|
||||
createRequest,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2562,65 +2593,65 @@ Crypto.prototype._onKeyVerificationMessage = function(event) {
|
||||
*
|
||||
* @private
|
||||
* @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)) {
|
||||
return;
|
||||
}
|
||||
const transactionId = InRoomChannel.getTransactionId(event);
|
||||
const createRequest = event => {
|
||||
if (!InRoomChannel.canCreateRequest(InRoomChannel.getEventType(event))) {
|
||||
return;
|
||||
}
|
||||
const userId = event.getSender();
|
||||
const channel = new InRoomChannel(
|
||||
this._baseApis,
|
||||
event.getRoomId(),
|
||||
userId,
|
||||
);
|
||||
return new VerificationRequest(
|
||||
channel, this._verificationMethods, userId, this._baseApis);
|
||||
channel, this._verificationMethods, this._baseApis);
|
||||
};
|
||||
this._handleVerificationEvent(event, transactionId,
|
||||
this._inRoomVerificationRequests, createRequest);
|
||||
this._handleVerificationEvent(
|
||||
event,
|
||||
this._inRoomVerificationRequests,
|
||||
createRequest,
|
||||
liveEvent,
|
||||
);
|
||||
};
|
||||
|
||||
Crypto.prototype._handleVerificationEvent = async function(
|
||||
event, transactionId, requestsMap, createRequest,
|
||||
event, requestsMap, createRequest, isLiveEvent = true,
|
||||
) {
|
||||
const sender = event.getSender();
|
||||
let requestsByTxnId = requestsMap.get(sender);
|
||||
let request = requestsMap.getRequest(event);
|
||||
let isNewRequest = false;
|
||||
let request = requestsByTxnId && requestsByTxnId.get(transactionId);
|
||||
if (!request) {
|
||||
request = createRequest(event);
|
||||
// a request could not be made from this event, so ignore event
|
||||
if (!request) {
|
||||
logger.log(`Crypto: could not find VerificationRequest for ` +
|
||||
`${event.getType()}, and could not create one, so ignoring.`);
|
||||
return;
|
||||
}
|
||||
isNewRequest = true;
|
||||
if (!requestsByTxnId) {
|
||||
requestsByTxnId = new Map();
|
||||
requestsMap.set(sender, requestsByTxnId);
|
||||
}
|
||||
requestsByTxnId.set(transactionId, request);
|
||||
requestsMap.setRequest(event, request);
|
||||
}
|
||||
event.setVerificationRequest(request);
|
||||
try {
|
||||
const hadVerifier = !!request.verifier;
|
||||
await request.channel.handleEvent(event, request);
|
||||
await request.channel.handleEvent(event, request, isLiveEvent);
|
||||
// emit start event when verifier got set
|
||||
if (!hadVerifier && request.verifier) {
|
||||
this._baseApis.emit("crypto.verification.start", request.verifier);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("error while handling verification event", event, err);
|
||||
logger.error("error while handling verification event: " + err.message);
|
||||
}
|
||||
if (!request.pending) {
|
||||
requestsByTxnId.delete(transactionId);
|
||||
if (requestsByTxnId.size === 0) {
|
||||
requestsMap.delete(sender);
|
||||
}
|
||||
} else if (isNewRequest && !request.initiatedByMe) {
|
||||
const shouldEmit = isNewRequest &&
|
||||
!request.initiatedByMe &&
|
||||
!request.invalid && // check it has enough events to pass the UNSENT stage
|
||||
!request.observeOnly;
|
||||
if (shouldEmit) {
|
||||
this._baseApis.emit("crypto.verification.request", request);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,10 +123,10 @@ export async function encryptMessageForDevice(
|
||||
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||
* map from userid to list of devices to ensure sessions for
|
||||
*
|
||||
* @param {bolean} force If true, establish a new session even if one already exists.
|
||||
* @param {boolean} force If true, establish a new session even if one already exists.
|
||||
* Optional.
|
||||
*
|
||||
* @return {module:client.Promise} resolves once the sessions are complete, to
|
||||
* @return {Promise} resolves once the sessions are complete, to
|
||||
* an Object mapping from userId to deviceId to
|
||||
* {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
|
||||
@@ -263,11 +263,15 @@ export class MemoryCryptoStore {
|
||||
}
|
||||
|
||||
getAllEndToEndSessions(txn, func) {
|
||||
for (const deviceSessions of Object.values(this._sessions)) {
|
||||
for (const sess of Object.values(deviceSessions)) {
|
||||
func(sess);
|
||||
}
|
||||
}
|
||||
Object.entries(this._sessions).forEach(([deviceKey, deviceSessions]) => {
|
||||
Object.entries(deviceSessions).forEach(([sessionId, session]) => {
|
||||
func({
|
||||
...session,
|
||||
deviceKey,
|
||||
sessionId,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
|
||||
|
||||
@@ -27,6 +27,13 @@ import {newTimeoutError} from "./Error";
|
||||
|
||||
const timeoutException = new Error("Verification timed out");
|
||||
|
||||
export class SwitchStartEventError extends Error {
|
||||
constructor(startEvent) {
|
||||
super();
|
||||
this.startEvent = startEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export class VerificationBase extends EventEmitter {
|
||||
/**
|
||||
* Base class for verification methods.
|
||||
@@ -67,9 +74,19 @@ export class VerificationBase extends EventEmitter {
|
||||
this._done = false;
|
||||
this._promise = null;
|
||||
this._transactionTimeoutTimer = null;
|
||||
}
|
||||
|
||||
// At this point, the verification request was received so start the timeout timer.
|
||||
this._resetTimer();
|
||||
get initiatedByMe() {
|
||||
// if there is no start event yet,
|
||||
// we probably want to send it,
|
||||
// which happens if we initiate
|
||||
if (!this.startEvent) {
|
||||
return true;
|
||||
}
|
||||
const sender = this.startEvent.getSender();
|
||||
const content = this.startEvent.getContent();
|
||||
return sender === this._baseApis.getUserId() &&
|
||||
content.from_device === this._baseApis.getDeviceId();
|
||||
}
|
||||
|
||||
_resetTimer() {
|
||||
@@ -107,6 +124,22 @@ export class VerificationBase extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
canSwitchStartEvent() {
|
||||
return false;
|
||||
}
|
||||
|
||||
switchStartEvent(event) {
|
||||
if (this.canSwitchStartEvent(event)) {
|
||||
if (this._rejectEvent) {
|
||||
const reject = this._rejectEvent;
|
||||
this._rejectEvent = undefined;
|
||||
reject(new SwitchStartEventError(event));
|
||||
} else {
|
||||
this.startEvent = event;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent(e) {
|
||||
if (this._done) {
|
||||
return;
|
||||
@@ -122,8 +155,15 @@ export class VerificationBase extends EventEmitter {
|
||||
} else if (e.getType() === "m.key.verification.cancel") {
|
||||
const reject = this._reject;
|
||||
this._reject = undefined;
|
||||
reject(new Error("Other side cancelled verification"));
|
||||
} else {
|
||||
const content = e.getContent();
|
||||
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(
|
||||
"Unexpected message: expecting " + this._expectedEvent
|
||||
+ " but got " + e.getType(),
|
||||
@@ -263,6 +303,13 @@ export class VerificationBase extends EventEmitter {
|
||||
throw new Error("No devices could be verified");
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Verification completed! Marking devices verified: ",
|
||||
verifiedDevices,
|
||||
);
|
||||
// TODO: There should probably be a batch version of this, otherwise it's going
|
||||
// to upload each signature in a separate API call which is silly because the
|
||||
// API supports as many signatures as you like.
|
||||
for (const deviceId of verifiedDevices) {
|
||||
await this._baseApis.setDeviceVerified(userId, deviceId);
|
||||
}
|
||||
|
||||
@@ -23,12 +23,10 @@ limitations under the License.
|
||||
import {MatrixEvent} from "../../models/event";
|
||||
|
||||
export function newVerificationError(code, reason, extradata) {
|
||||
extradata = extradata || {};
|
||||
extradata.code = code;
|
||||
extradata.reason = reason;
|
||||
const content = Object.assign({}, {code, reason}, extradata);
|
||||
return new MatrixEvent({
|
||||
type: "m.key.verification.cancel",
|
||||
content: extradata,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Verification method that is illegal to have (cannot possibly
|
||||
* do verification with this method).
|
||||
* @module crypto/verification/IllegalMethod
|
||||
*/
|
||||
|
||||
import {VerificationBase as Base} from "./Base";
|
||||
|
||||
/**
|
||||
* @class crypto/verification/IllegalMethod/IllegalMethod
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class IllegalMethod extends Base {
|
||||
static factory(...args) {
|
||||
return new IllegalMethod(...args);
|
||||
}
|
||||
|
||||
static get NAME() {
|
||||
// Typically the name will be something else, but to complete
|
||||
// the contract we offer a default one here.
|
||||
return "org.matrix.illegal_method";
|
||||
}
|
||||
|
||||
async _doVerification() {
|
||||
throw new Error("Verification is not possible with this method");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 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.
|
||||
@@ -21,103 +22,92 @@ limitations under the License.
|
||||
|
||||
import {VerificationBase as Base} from "./Base";
|
||||
import {
|
||||
errorFactory,
|
||||
newKeyMismatchError,
|
||||
newUserCancelledError,
|
||||
newUserMismatchError,
|
||||
} from './Error';
|
||||
|
||||
const MATRIXTO_REGEXP = /^(?:https?:\/\/)?(?:www\.)?matrix\.to\/#\/([#@!+][^?]+)\?(.+)$/;
|
||||
const KEY_REGEXP = /^key_([^:]+:.+)$/;
|
||||
|
||||
const newQRCodeError = errorFactory("m.qr_code.invalid", "Invalid QR code");
|
||||
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
|
||||
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
|
||||
|
||||
/**
|
||||
* @class crypto/verification/QRCode/ShowQRCode
|
||||
* @class crypto/verification/QRCode/ReciprocateQRCode
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class ShowQRCode extends Base {
|
||||
_doVerification() {
|
||||
if (!this._done) {
|
||||
const url = "https://matrix.to/#/" + this._baseApis.getUserId()
|
||||
+ "?device=" + encodeURIComponent(this._baseApis.deviceId)
|
||||
+ "&action=verify&key_ed25519%3A"
|
||||
+ encodeURIComponent(this._baseApis.deviceId) + "="
|
||||
+ encodeURIComponent(this._baseApis.getDeviceEd25519Key());
|
||||
this.emit("show_qr_code", {
|
||||
url: url,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ShowQRCode.NAME = "m.qr_code.show.v1";
|
||||
|
||||
/**
|
||||
* @class crypto/verification/QRCode/ScanQRCode
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class ScanQRCode extends Base {
|
||||
export class ReciprocateQRCode extends Base {
|
||||
static factory(...args) {
|
||||
return new ScanQRCode(...args);
|
||||
return new ReciprocateQRCode(...args);
|
||||
}
|
||||
|
||||
static get NAME() {
|
||||
return "m.reciprocate.v1";
|
||||
}
|
||||
|
||||
async _doVerification() {
|
||||
const code = await new Promise((resolve, reject) => {
|
||||
this.emit("scan", {
|
||||
done: resolve,
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
});
|
||||
});
|
||||
|
||||
const match = code.match(MATRIXTO_REGEXP);
|
||||
let deviceId;
|
||||
const keys = {};
|
||||
if (!match) {
|
||||
throw newQRCodeError();
|
||||
}
|
||||
const userId = match[1];
|
||||
const params = match[2].split("&").map(
|
||||
(x) => x.split("=", 2).map(decodeURIComponent),
|
||||
);
|
||||
let action;
|
||||
for (const [name, value] of params) {
|
||||
if (name === "device") {
|
||||
deviceId = value;
|
||||
} else if (name === "action") {
|
||||
action = value;
|
||||
} else {
|
||||
const keyMatch = name.match(KEY_REGEXP);
|
||||
if (keyMatch) {
|
||||
keys[keyMatch[1]] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!deviceId || action !== "verify" || Object.keys(keys).length === 0) {
|
||||
throw newQRCodeError();
|
||||
if (!this.startEvent) {
|
||||
// TODO: Support scanning QR codes
|
||||
throw new Error("It is not currently possible to start verification" +
|
||||
"with this method yet.");
|
||||
}
|
||||
|
||||
const targetUserId = this.startEvent.getSender();
|
||||
if (!this.userId) {
|
||||
await new Promise((resolve, reject) => {
|
||||
console.log("Asking to confirm user ID");
|
||||
this.userId = await new Promise((resolve, reject) => {
|
||||
this.emit("confirm_user_id", {
|
||||
userId: userId,
|
||||
confirm: resolve,
|
||||
userId: targetUserId,
|
||||
confirm: resolve, // takes a userId
|
||||
cancel: () => reject(newUserMismatchError()),
|
||||
});
|
||||
});
|
||||
} else if (this.userId !== userId) {
|
||||
} else if (targetUserId !== this.userId) {
|
||||
throw newUserMismatchError({
|
||||
expected: this.userId,
|
||||
actual: userId,
|
||||
actual: targetUserId,
|
||||
});
|
||||
}
|
||||
|
||||
await this._verifyKeys(userId, keys, (keyId, device, key) => {
|
||||
if (device.keys[keyId] !== key) {
|
||||
if (this.startEvent.getContent()['secret'] !== this.request.encodedSharedSecret) {
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
|
||||
// If we've gotten this far, verify the user's master cross signing key
|
||||
const xsignInfo = this._baseApis.getStoredCrossSigningForUser(this.userId);
|
||||
if (!xsignInfo) throw new Error("Missing cross signing info");
|
||||
|
||||
const masterKey = xsignInfo.getId("master");
|
||||
const masterKeyId = `ed25519:${masterKey}`;
|
||||
const keys = {[masterKeyId]: masterKey};
|
||||
|
||||
const devices = (await this._baseApis.getStoredDevicesForUser(this.userId)) || [];
|
||||
const targetDevice = devices.find(d => {
|
||||
return d.deviceId === this.request.estimatedTargetDevice.deviceId;
|
||||
});
|
||||
if (!targetDevice) throw new Error("Device not found, somehow");
|
||||
keys[`ed25519:${targetDevice.deviceId}`] = targetDevice.getFingerprint();
|
||||
|
||||
if (this.request.requestingUserId === this.request.receivingUserId) {
|
||||
delete keys[masterKeyId];
|
||||
}
|
||||
|
||||
await this._verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
|
||||
const targetKey = keys[keyId];
|
||||
if (!targetKey) throw newKeyMismatchError();
|
||||
|
||||
if (keyInfo !== targetKey) {
|
||||
console.error("key ID from key info does not match");
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
for (const deviceKeyId in device.keys) {
|
||||
if (!deviceKeyId.startsWith("ed25519")) continue;
|
||||
const deviceTargetKey = keys[deviceKeyId];
|
||||
if (!deviceTargetKey) throw newKeyMismatchError();
|
||||
if (device.keys[deviceKeyId] !== deviceTargetKey) {
|
||||
console.error("master key does not match");
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise it is probably fine
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ScanQRCode.NAME = "m.qr_code.scan.v1";
|
||||
|
||||
@@ -19,7 +19,7 @@ limitations under the License.
|
||||
* @module crypto/verification/SAS
|
||||
*/
|
||||
|
||||
import {VerificationBase as Base} from "./Base";
|
||||
import {VerificationBase as Base, SwitchStartEventError} from "./Base";
|
||||
import anotherjson from 'another-json';
|
||||
import {
|
||||
errorFactory,
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
newUserCancelledError,
|
||||
} from './Error';
|
||||
|
||||
const START_TYPE = "m.key.verification.start";
|
||||
|
||||
const EVENTS = [
|
||||
"m.key.verification.accept",
|
||||
"m.key.verification.key",
|
||||
@@ -201,16 +203,37 @@ export class SAS extends Base {
|
||||
// make sure user's keys are downloaded
|
||||
await this._baseApis.downloadKeys([this.userId]);
|
||||
|
||||
if (this.startEvent) {
|
||||
return await this._doRespondVerification();
|
||||
} else {
|
||||
return await this._doSendVerification();
|
||||
}
|
||||
let retry = false;
|
||||
do {
|
||||
try {
|
||||
if (this.initiatedByMe) {
|
||||
return await this._doSendVerification();
|
||||
} else {
|
||||
return await this._doRespondVerification();
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof SwitchStartEventError) {
|
||||
// this changes what initiatedByMe returns
|
||||
this.startEvent = err.startEvent;
|
||||
retry = true;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} while (retry);
|
||||
}
|
||||
|
||||
async _doSendVerification() {
|
||||
const type = "m.key.verification.start";
|
||||
const initialMessage = this._channel.completeContent(type, {
|
||||
canSwitchStartEvent(event) {
|
||||
if (event.getType() !== START_TYPE) {
|
||||
return false;
|
||||
}
|
||||
const content = event.getContent();
|
||||
return content && content.method === SAS.NAME &&
|
||||
this._waitingForAccept;
|
||||
}
|
||||
|
||||
async _sendStart() {
|
||||
const startContent = this._channel.completeContent(START_TYPE, {
|
||||
method: SAS.NAME,
|
||||
from_device: this._baseApis.deviceId,
|
||||
key_agreement_protocols: KEY_AGREEMENT_LIST,
|
||||
@@ -219,11 +242,33 @@ export class SAS extends Base {
|
||||
// FIXME: allow app to specify what SAS methods can be used
|
||||
short_authentication_string: SAS_LIST,
|
||||
});
|
||||
// add the transaction id to the message beforehand because
|
||||
// it needs to be included in the commitment hash later on
|
||||
this._channel.sendCompleted(type, initialMessage);
|
||||
await this._channel.sendCompleted(START_TYPE, startContent);
|
||||
return startContent;
|
||||
}
|
||||
|
||||
let e = await this._waitForEvent("m.key.verification.accept");
|
||||
async _doSendVerification() {
|
||||
this._waitingForAccept = true;
|
||||
let startContent;
|
||||
if (this.startEvent) {
|
||||
startContent = this._channel.completedContentFromEvent(this.startEvent);
|
||||
} else {
|
||||
startContent = await this._sendStart();
|
||||
}
|
||||
|
||||
// we might have switched to a different start event,
|
||||
// but was we didn't call _waitForEvent there was no
|
||||
// call that could throw yet. So check manually that
|
||||
// we're still on the initiator side
|
||||
if (!this.initiatedByMe) {
|
||||
throw new SwitchStartEventError(this.startEvent);
|
||||
}
|
||||
|
||||
let e;
|
||||
try {
|
||||
e = await this._waitForEvent("m.key.verification.accept");
|
||||
} finally {
|
||||
this._waitingForAccept = false;
|
||||
}
|
||||
let content = e.getContent();
|
||||
const sasMethods
|
||||
= intersection(content.short_authentication_string, SAS_SET);
|
||||
@@ -248,7 +293,7 @@ export class SAS extends Base {
|
||||
e = await this._waitForEvent("m.key.verification.key");
|
||||
// FIXME: make sure event is properly formed
|
||||
content = e.getContent();
|
||||
const commitmentStr = content.key + anotherjson.stringify(initialMessage);
|
||||
const commitmentStr = content.key + anotherjson.stringify(startContent);
|
||||
// TODO: use selected hash function (when we support multiple)
|
||||
if (olmutil.sha256(commitmentStr) !== hashCommitment) {
|
||||
throw newMismatchedCommitmentError();
|
||||
|
||||
@@ -15,7 +15,13 @@ See the License for the specific language governing permissions and
|
||||
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 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} 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._roomId = roomId;
|
||||
this._userId = userId;
|
||||
this.userId = userId;
|
||||
this._requestEventId = null;
|
||||
}
|
||||
|
||||
@@ -43,16 +49,41 @@ export class InRoomChannel {
|
||||
return true;
|
||||
}
|
||||
|
||||
get receiveStartFromOtherDevices() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get roomId() {
|
||||
return this._roomId;
|
||||
}
|
||||
|
||||
/** The transaction id generated/used by this verification channel */
|
||||
get transactionId() {
|
||||
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;
|
||||
|
||||
if (sender === ownUserId) {
|
||||
return receiver;
|
||||
} else if (receiver === ownUserId) {
|
||||
return sender;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MatrixEvent} event the event to get the timestamp of
|
||||
* @return {number} the timestamp when the event was sent
|
||||
*/
|
||||
static getTimestamp(event) {
|
||||
getTimestamp(event) {
|
||||
return event.getTs();
|
||||
}
|
||||
|
||||
@@ -97,19 +128,26 @@ export class InRoomChannel {
|
||||
}
|
||||
const type = InRoomChannel.getEventType(event);
|
||||
const content = event.getContent();
|
||||
|
||||
// from here on we're fairly sure that this is supposed to be
|
||||
// part of a verification request, so be noisy when rejecting something
|
||||
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;
|
||||
}
|
||||
const ownUserId = client.getUserId();
|
||||
|
||||
// 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 VerificationRequest.validateEvent(
|
||||
type, event, InRoomChannel.getTimestamp(event), client);
|
||||
return VerificationRequest.validateEvent(type, event, client);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,28 +168,54 @@ export class InRoomChannel {
|
||||
}
|
||||
}
|
||||
}
|
||||
return type;
|
||||
if (type && type !== REQUEST_TYPE) {
|
||||
return type;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the state of the channel, request, and verifier in response to a key verification event.
|
||||
* @param {MatrixEvent} event to handle
|
||||
* @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.
|
||||
*/
|
||||
async handleEvent(event, request) {
|
||||
async handleEvent(event, request, isLiveEvent) {
|
||||
const type = InRoomChannel.getEventType(event);
|
||||
// do validations that need state (roomId, userId),
|
||||
// ignore if invalid
|
||||
if (event.getRoomId() !== this._roomId || event.getSender() !== this._userId) {
|
||||
|
||||
if (event.getRoomId() !== this._roomId) {
|
||||
return;
|
||||
}
|
||||
// set transactionId when receiving a .request
|
||||
if (!this._requestEventId && type === REQUEST_TYPE) {
|
||||
this._requestEventId = event.getId();
|
||||
// set userId if not set already
|
||||
if (this.userId === null) {
|
||||
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;
|
||||
const isSentByUs = event.getSender() === this._client.getUserId();
|
||||
|
||||
return await request.handleEvent(
|
||||
type, event, isLiveEvent, isRemoteEcho, isSentByUs);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,7 +244,7 @@ export class InRoomChannel {
|
||||
*/
|
||||
completeContent(type, 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();
|
||||
}
|
||||
if (type === REQUEST_TYPE) {
|
||||
@@ -191,7 +255,7 @@ export class InRoomChannel {
|
||||
"verification. You will need to use legacy key " +
|
||||
"verification to verify keys.",
|
||||
msgtype: REQUEST_TYPE,
|
||||
to: this._userId,
|
||||
to: this.userId,
|
||||
from_device: content.from_device,
|
||||
methods: content.methods,
|
||||
};
|
||||
@@ -232,3 +296,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 {
|
||||
CANCEL_TYPE,
|
||||
PHASE_STARTED,
|
||||
PHASE_READY,
|
||||
REQUEST_TYPE,
|
||||
READY_TYPE,
|
||||
START_TYPE,
|
||||
VerificationRequest,
|
||||
} from "./VerificationRequest";
|
||||
import {errorFromEvent, newUnexpectedMessageError} from "../Error";
|
||||
import {MatrixEvent} from "../../../models/event";
|
||||
|
||||
/**
|
||||
* A key verification channel that sends verification events over to_device messages.
|
||||
@@ -34,12 +37,20 @@ export class ToDeviceChannel {
|
||||
// userId and devices of user we're about to verify
|
||||
constructor(client, userId, devices, transactionId = null, deviceId = null) {
|
||||
this._client = client;
|
||||
this._userId = userId;
|
||||
this.userId = userId;
|
||||
this._devices = devices;
|
||||
this.transactionId = transactionId;
|
||||
this._deviceId = deviceId;
|
||||
}
|
||||
|
||||
get deviceId() {
|
||||
return this._deviceId;
|
||||
}
|
||||
|
||||
get needsDoneMessage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static getEventType(event) {
|
||||
return event.getType();
|
||||
}
|
||||
@@ -80,10 +91,12 @@ export class ToDeviceChannel {
|
||||
}
|
||||
const content = event.getContent();
|
||||
if (!content) {
|
||||
logger.warn("ToDeviceChannel.validateEvent: invalid: no content");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!content.transaction_id) {
|
||||
logger.warn("ToDeviceChannel.validateEvent: invalid: no transaction_id");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -91,6 +104,7 @@ export class ToDeviceChannel {
|
||||
|
||||
if (type === REQUEST_TYPE) {
|
||||
if (!Number.isFinite(content.timestamp)) {
|
||||
logger.warn("ToDeviceChannel.validateEvent: invalid: no timestamp");
|
||||
return false;
|
||||
}
|
||||
if (event.getSender() === client.getUserId() &&
|
||||
@@ -98,19 +112,19 @@ export class ToDeviceChannel {
|
||||
) {
|
||||
// ignore requests from ourselves, because it doesn't make sense for a
|
||||
// device to verify itself
|
||||
logger.warn("ToDeviceChannel.validateEvent: invalid: from own device");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return VerificationRequest.validateEvent(
|
||||
type, event, ToDeviceChannel.getTimestamp(event), client);
|
||||
return VerificationRequest.validateEvent(type, event, client);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MatrixEvent} event the event to get the timestamp of
|
||||
* @return {number} the timestamp when the event was sent
|
||||
*/
|
||||
static getTimestamp(event) {
|
||||
getTimestamp(event) {
|
||||
const content = event.getContent();
|
||||
return content && content.timestamp;
|
||||
}
|
||||
@@ -119,12 +133,13 @@ export class ToDeviceChannel {
|
||||
* Changes the state of the channel, request, and verifier in response to a key verification event.
|
||||
* @param {MatrixEvent} event to handle
|
||||
* @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.
|
||||
*/
|
||||
async handleEvent(event, request) {
|
||||
async handleEvent(event, request, isLiveEvent) {
|
||||
const type = event.getType();
|
||||
const content = event.getContent();
|
||||
if (type === REQUEST_TYPE || type === START_TYPE) {
|
||||
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
|
||||
if (!this.transactionId) {
|
||||
this.transactionId = content.transaction_id;
|
||||
}
|
||||
@@ -143,14 +158,17 @@ export class ToDeviceChannel {
|
||||
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, ToDeviceChannel.getTimestamp(event));
|
||||
const isStarted = request.phase === PHASE_STARTED;
|
||||
await request.handleEvent(event.getType(), event, isLiveEvent, false, false);
|
||||
|
||||
// the request has picked a start event, tell the other devices about it
|
||||
if (type === START_TYPE && !wasStarted && isStarted && this._deviceId) {
|
||||
const isStarted = request.phase === PHASE_STARTED ||
|
||||
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);
|
||||
if (nonChosenDevices.length) {
|
||||
const message = this.completeContent({
|
||||
@@ -186,7 +204,7 @@ export class ToDeviceChannel {
|
||||
if (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();
|
||||
}
|
||||
if (type === REQUEST_TYPE) {
|
||||
@@ -216,12 +234,28 @@ export class ToDeviceChannel {
|
||||
* @param {object} content
|
||||
* @returns {Promise} the promise of the request
|
||||
*/
|
||||
sendCompleted(type, content) {
|
||||
async sendCompleted(type, content) {
|
||||
let result;
|
||||
if (type === REQUEST_TYPE) {
|
||||
return this._sendToDevices(type, content, this._devices);
|
||||
result = await this._sendToDevices(type, content, this._devices);
|
||||
} 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,
|
||||
/*isSentByUs=*/true,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
_sendToDevices(type, content, devices) {
|
||||
@@ -231,7 +265,7 @@ export class ToDeviceChannel {
|
||||
msgMap[deviceId] = content;
|
||||
}
|
||||
|
||||
return this._client.sendToDevice(type, {[this._userId]: msgMap});
|
||||
return this._client.sendToDevice(type, {[this.userId]: msgMap});
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -245,3 +279,60 @@ export class ToDeviceChannel {
|
||||
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 {RequestCallbackChannel} from "./RequestCallbackChannel";
|
||||
import {EventEmitter} from 'events';
|
||||
import {
|
||||
errorFactory,
|
||||
@@ -24,6 +23,7 @@ import {
|
||||
newUnexpectedMessageError,
|
||||
newUnknownMethodError,
|
||||
} from "../Error";
|
||||
import * as olmlib from "../../olmlib";
|
||||
|
||||
// the recommended amount of time before a verification request
|
||||
// should be (automatically) cancelled without user interaction
|
||||
@@ -41,11 +41,11 @@ export const REQUEST_TYPE = EVENT_PREFIX + "request";
|
||||
export const START_TYPE = EVENT_PREFIX + "start";
|
||||
export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
|
||||
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_REQUESTED = 2;
|
||||
// const PHASE_READY = 3;
|
||||
export const PHASE_READY = 3;
|
||||
export const PHASE_STARTED = 4;
|
||||
export const PHASE_CANCELLED = 5;
|
||||
export const PHASE_DONE = 6;
|
||||
@@ -58,17 +58,19 @@ export const PHASE_DONE = 6;
|
||||
* @event "change" whenever the state of the request object has changed.
|
||||
*/
|
||||
export class VerificationRequest extends EventEmitter {
|
||||
constructor(channel, verificationMethods, userId, client) {
|
||||
constructor(channel, verificationMethods, client) {
|
||||
super();
|
||||
this.channel = channel;
|
||||
this.channel._request = this;
|
||||
this._verificationMethods = verificationMethods;
|
||||
this._client = client;
|
||||
this._commonMethods = [];
|
||||
this._setPhase(PHASE_UNSENT, false);
|
||||
this._requestEvent = null;
|
||||
this._otherUserId = userId;
|
||||
this._initiatedByMe = null;
|
||||
this._startTimestamp = null;
|
||||
this._eventsByUs = new Map();
|
||||
this._eventsByThem = new Map();
|
||||
this._observeOnly = false;
|
||||
this._timeoutTimer = null;
|
||||
this._sharedSecret = null; // used for QR codes
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,37 +78,38 @@ export class VerificationRequest extends EventEmitter {
|
||||
* 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 {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
|
||||
* @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();
|
||||
|
||||
if (!type.startsWith(EVENT_PREFIX)) {
|
||||
|
||||
if (!type || !type.startsWith(EVENT_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type === REQUEST_TYPE) {
|
||||
if (!Array.isArray(content.methods)) {
|
||||
return false;
|
||||
}
|
||||
// from here on we're fairly sure that this is supposed to be
|
||||
// part of a verification request, so be noisy when rejecting something
|
||||
if (!content) {
|
||||
logger.log("VerificationRequest: validateEvent: no content");
|
||||
return false;
|
||||
}
|
||||
if (type === REQUEST_TYPE || type === START_TYPE) {
|
||||
if (typeof content.from_device !== "string" ||
|
||||
content.from_device.length === 0
|
||||
) {
|
||||
|
||||
if (type === REQUEST_TYPE || type === READY_TYPE) {
|
||||
if (!Array.isArray(content.methods)) {
|
||||
logger.log("VerificationRequest: validateEvent: " +
|
||||
"fail because methods");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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");
|
||||
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
|
||||
if (typeof content.from_device !== "string" ||
|
||||
content.from_device.length === 0
|
||||
) {
|
||||
logger.log("VerificationRequest: validateEvent: "+
|
||||
"fail because from_device");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -114,20 +117,56 @@ export class VerificationRequest extends EventEmitter {
|
||||
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() {
|
||||
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() {
|
||||
const elapsed = Date.now() - this._startTimestamp;
|
||||
return Math.max(0, VERIFICATION_REQUEST_TIMEOUT - elapsed);
|
||||
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 0;
|
||||
}
|
||||
|
||||
/** the m.key.verification.request event that started this request, provided for compatibility with previous verification code */
|
||||
get event() {
|
||||
return this._requestEvent;
|
||||
/**
|
||||
* The key verification request event.
|
||||
* @returns {MatrixEvent} The request event, or falsey if not found.
|
||||
*/
|
||||
get requestEvent() {
|
||||
return this._getEventByEither(REQUEST_TYPE);
|
||||
}
|
||||
|
||||
/** current phase of the request. Some properties might only be defined in a current phase. */
|
||||
@@ -142,8 +181,7 @@ export class VerificationRequest extends EventEmitter {
|
||||
|
||||
/** whether this request has sent it's initial event and needs more events to complete */
|
||||
get pending() {
|
||||
return this._phase !== PHASE_UNSENT
|
||||
&& this._phase !== PHASE_DONE
|
||||
return this._phase !== PHASE_DONE
|
||||
&& this._phase !== PHASE_CANCELLED;
|
||||
}
|
||||
|
||||
@@ -152,27 +190,108 @@ export class VerificationRequest extends EventEmitter {
|
||||
* For ToDeviceChannel, this is who sent the .start event
|
||||
*/
|
||||
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 */
|
||||
get requestingUserId() {
|
||||
if (this.initiatedByMe) {
|
||||
return this._client.getUserId();
|
||||
} 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() {
|
||||
if (this.initiatedByMe) {
|
||||
return this._otherUserId;
|
||||
return this.otherUserId;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* The cancellation code e.g m.user which is responsible for cancelling this verification
|
||||
*/
|
||||
get cancellationCode() {
|
||||
const ev = this._getEventByEither(CANCEL_TYPE);
|
||||
return ev ? ev.getContent().code : null;
|
||||
}
|
||||
|
||||
get observeOnly() {
|
||||
return this._observeOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* The unpadded base64 encoded shared secret. Primarily used for QR code
|
||||
* verification.
|
||||
*/
|
||||
get encodedSharedSecret() {
|
||||
if (!this._sharedSecret) this._generateSharedSecret();
|
||||
return this._sharedSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets which device the verification should be started with
|
||||
* given the events sent so far in the verification. This is the
|
||||
* same algorithm used to determine which device to send the
|
||||
* verification to when no specific device is specified.
|
||||
* @returns {{userId: *, deviceId: *}} The device information
|
||||
*/
|
||||
get 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;
|
||||
return {
|
||||
userId: this.otherUserId,
|
||||
deviceId: fromDevice,
|
||||
};
|
||||
}
|
||||
|
||||
/* 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.
|
||||
* @param {string} method the name of the verification method to use.
|
||||
@@ -182,8 +301,13 @@ export class VerificationRequest extends EventEmitter {
|
||||
*/
|
||||
beginKeyVerification(method, targetDevice = null) {
|
||||
// need to allow also when unsent in case of to_device
|
||||
if (!this._verifier) {
|
||||
if (this._hasValidPreStartPhase()) {
|
||||
if (!this.observeOnly && !this._verifier) {
|
||||
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
|
||||
// check the method is supported by both sides
|
||||
if (this._commonMethods.length && !this._commonMethods.includes(method)) {
|
||||
@@ -203,12 +327,10 @@ export class VerificationRequest extends EventEmitter {
|
||||
* @returns {Promise} resolves when the event has been sent.
|
||||
*/
|
||||
async sendRequest() {
|
||||
if (this._phase === PHASE_UNSENT) {
|
||||
this._initiatedByMe = true;
|
||||
this._setPhase(PHASE_REQUESTED, false);
|
||||
if (!this.observeOnly && this._phase === PHASE_UNSENT) {
|
||||
const methods = [...this._verificationMethods.keys()];
|
||||
await this.channel.send(REQUEST_TYPE, {methods});
|
||||
this.emit("change");
|
||||
this._generateSharedSecret();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,34 +341,63 @@ export class VerificationRequest extends EventEmitter {
|
||||
* @returns {Promise} resolves when the event has been sent.
|
||||
*/
|
||||
async cancel({reason = "User declined", code = "m.user"} = {}) {
|
||||
if (this._phase !== PHASE_CANCELLED) {
|
||||
if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
|
||||
if (this._verifier) {
|
||||
return this._verifier.cancel(errorFactory(code, reason));
|
||||
return this._verifier.cancel(errorFactory(code, reason)());
|
||||
} else {
|
||||
this._setPhase(PHASE_CANCELLED, false);
|
||||
this._cancellingUserId = this._client.getUserId();
|
||||
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() {
|
||||
if (this.verifier) {
|
||||
return Promise.resolve(this.verifier);
|
||||
} else {
|
||||
return new Promise(resolve => {
|
||||
const checkVerifier = () => {
|
||||
if (this.verifier) {
|
||||
this.off("change", checkVerifier);
|
||||
resolve(this.verifier);
|
||||
}
|
||||
};
|
||||
this.on("change", checkVerifier);
|
||||
});
|
||||
/**
|
||||
* Accepts the request, sending a .ready event to the other party
|
||||
* @returns {Promise} resolves when the event has been sent.
|
||||
*/
|
||||
async accept() {
|
||||
if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
|
||||
const methods = [...this._verificationMethods.keys()];
|
||||
await this.channel.send(READY_TYPE, {methods});
|
||||
this._generateSharedSecret();
|
||||
}
|
||||
}
|
||||
|
||||
_generateSharedSecret() {
|
||||
const secretBytes = new Uint8Array(32); // 256bits
|
||||
global.crypto.getRandomValues(secretBytes);
|
||||
this._sharedSecret = olmlib.encodeBase64(secretBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
if (!check()) {
|
||||
this.on("change", check);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_setPhase(phase, notify = true) {
|
||||
this._phase = phase;
|
||||
if (notify) {
|
||||
@@ -254,155 +405,297 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_getEventByEither(type) {
|
||||
return this._eventsByThem.get(type) || this._eventsByUs.get(type);
|
||||
}
|
||||
|
||||
_getEventBy(type, byThem) {
|
||||
if (byThem) {
|
||||
return this._eventsByThem.get(type);
|
||||
} else {
|
||||
return this._eventsByUs.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 hasRequestByThem = this._eventsByThem.has(REQUEST_TYPE);
|
||||
const requestEvent = this._getEventBy(REQUEST_TYPE, hasRequestByThem);
|
||||
if (requestEvent) {
|
||||
transitions.push({phase: PHASE_REQUESTED, event: requestEvent});
|
||||
}
|
||||
|
||||
const readyEvent =
|
||||
requestEvent && this._getEventBy(READY_TYPE, !hasRequestByThem);
|
||||
if (readyEvent && phase() === PHASE_REQUESTED) {
|
||||
transitions.push({phase: PHASE_READY, event: readyEvent});
|
||||
}
|
||||
|
||||
let startEvent;
|
||||
if (readyEvent || !requestEvent) {
|
||||
const theirStartEvent = this._eventsByThem.get(START_TYPE);
|
||||
const ourStartEvent = this._eventsByUs.get(START_TYPE);
|
||||
// any party can send .start after a .ready or unsent
|
||||
if (theirStartEvent && ourStartEvent) {
|
||||
startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ?
|
||||
theirStartEvent : ourStartEvent;
|
||||
} else {
|
||||
startEvent = theirStartEvent ? theirStartEvent : ourStartEvent;
|
||||
}
|
||||
} else {
|
||||
startEvent = this._getEventBy(START_TYPE, !hasRequestByThem);
|
||||
}
|
||||
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);
|
||||
if (ourDoneEvent && 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.
|
||||
* @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 {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
|
||||
* @param {bool} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers.
|
||||
* For InRoomChannel this means any device for the syncing user. For ToDeviceChannel, just the syncing device.
|
||||
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
||||
*/
|
||||
async handleEvent(type, event, timestamp) {
|
||||
const content = event.getContent();
|
||||
if (type === REQUEST_TYPE || type === START_TYPE) {
|
||||
if (this._startTimestamp === null) {
|
||||
this._startTimestamp = timestamp;
|
||||
}
|
||||
}
|
||||
if (type === REQUEST_TYPE) {
|
||||
await this._handleRequest(content, event);
|
||||
} else if (type === START_TYPE) {
|
||||
await this._handleStart(content, event);
|
||||
async handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs) {
|
||||
// if reached phase cancelled or done, ignore anything else that comes
|
||||
if (!this.pending) {
|
||||
return;
|
||||
}
|
||||
const wasObserveOnly = this._observeOnly;
|
||||
|
||||
if (this._verifier) {
|
||||
if (type === CANCEL_TYPE || (this._verifier.events
|
||||
&& this._verifier.events.includes(type))) {
|
||||
this._verifier.handleEvent(event);
|
||||
this._adjustObserveOnly(event, isLiveEvent);
|
||||
|
||||
if (!this.observeOnly && !isRemoteEcho) {
|
||||
if (await this._cancelOnError(type, event)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (type === CANCEL_TYPE) {
|
||||
this._handleCancel();
|
||||
} else if (type === DONE_TYPE) {
|
||||
this._handleDone();
|
||||
const oldPhase = this.phase;
|
||||
this._addEvent(type, event, isSentByUs);
|
||||
|
||||
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);
|
||||
}
|
||||
try {
|
||||
// only pass events from the other side to the verifier,
|
||||
// no remote echos of our own events
|
||||
if (this._verifier && !this.observeOnly) {
|
||||
const oldEvent = this._verifier.startEvent;
|
||||
// if the verifier does not have a startEvent, it is because it's still sending and we are on the initator side
|
||||
const oldSender = oldEvent ?
|
||||
oldEvent.getSender() :
|
||||
this._client.getUserId();
|
||||
const newEventWinsRace = event.getSender() < oldSender;
|
||||
if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) {
|
||||
this._verifier.switchStartEvent(event);
|
||||
} else if (!isRemoteEcho) {
|
||||
if (type === CANCEL_TYPE || (this._verifier.events
|
||||
&& this._verifier.events.includes(type))) {
|
||||
this._verifier.handleEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newTransitions.length) {
|
||||
const lastTransition = newTransitions[newTransitions.length - 1];
|
||||
const {phase} = lastTransition;
|
||||
|
||||
this._setupTimeout(phase);
|
||||
// set phase as last thing as this emits the "change" event
|
||||
this._setPhase(phase);
|
||||
} else if (this._observeOnly !== wasObserveOnly) {
|
||||
this.emit("change");
|
||||
}
|
||||
} finally {
|
||||
// log events we processed so we can see from rageshakes what events were added to a request
|
||||
logger.log(`Verification request ${this.channel.transactionId}: ` +
|
||||
`${type} event with ${JSON.stringify(event.getContent())} ` +
|
||||
`deviceId:${this.channel.deviceId} ` +
|
||||
`sender:${event.getSender()}, isSentByUs:${isSentByUs} ` +
|
||||
`phase:${oldPhase}=>${this.phase}, ` +
|
||||
`observeOnly:${wasObserveOnly}=>${this._observeOnly}`);
|
||||
}
|
||||
}
|
||||
|
||||
async _handleRequest(content, event) {
|
||||
if (this._phase === PHASE_UNSENT) {
|
||||
const otherMethods = content.methods;
|
||||
this._commonMethods = otherMethods.
|
||||
filter(m => this._verificationMethods.has(m));
|
||||
this._requestEvent = event;
|
||||
this._initiatedByMe = this._wasSentByMe(event);
|
||||
this._setPhase(PHASE_REQUESTED);
|
||||
} else if (this._phase !== PHASE_REQUESTED) {
|
||||
logger.warn("Ignoring flagged verification request from " +
|
||||
event.getSender());
|
||||
await this.cancel(errorFromEvent(newUnexpectedMessageError()));
|
||||
_setupTimeout(phase) {
|
||||
const shouldTimeout = !this._timeoutTimer && !this.observeOnly &&
|
||||
phase === PHASE_REQUESTED && this.initiatedByMe;
|
||||
|
||||
if (shouldTimeout) {
|
||||
this._timeoutTimer = setTimeout(this._cancelOnTimeout, this.timeout);
|
||||
}
|
||||
if (this._timeoutTimer) {
|
||||
const shouldClear = phase === PHASE_STARTED ||
|
||||
phase === PHASE_READY ||
|
||||
phase === PHASE_DONE ||
|
||||
phase === PHASE_CANCELLED;
|
||||
if (shouldClear) {
|
||||
clearTimeout(this._timeoutTimer);
|
||||
this._timeoutTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_hasValidPreStartPhase() {
|
||||
return this._phase === PHASE_REQUESTED ||
|
||||
(
|
||||
this.channel.constructor.canCreateRequest(START_TYPE) &&
|
||||
this._phase === PHASE_UNSENT
|
||||
);
|
||||
}
|
||||
_cancelOnTimeout = () => {
|
||||
try {
|
||||
this.cancel({reason: "Other party didn't accept in time", code: "m.timeout"});
|
||||
} catch (err) {
|
||||
logger.error("Error while cancelling verification request", err);
|
||||
}
|
||||
};
|
||||
|
||||
async _handleStart(content, event) {
|
||||
if (this._hasValidPreStartPhase()) {
|
||||
const {method} = content;
|
||||
async _cancelOnError(type, event) {
|
||||
if (type === START_TYPE) {
|
||||
const method = event.getContent().method;
|
||||
if (!this._verificationMethods.has(method)) {
|
||||
await this.cancel(errorFromEvent(newUnknownMethodError()));
|
||||
} else {
|
||||
// if not in requested phase
|
||||
if (this.phase === PHASE_UNSENT) {
|
||||
this._initiatedByMe = this._wasSentByMe(event);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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, isSentByUs) {
|
||||
if (isSentByUs) {
|
||||
this._eventsByUs.set(type, event);
|
||||
} else {
|
||||
this._eventsByThem.set(type, event);
|
||||
}
|
||||
|
||||
// once we know the userId of the other party (from the .request event)
|
||||
// see if any event by anyone else crept into this._eventsByThem
|
||||
if (type === REQUEST_TYPE) {
|
||||
for (const [type, event] of this._eventsByThem.entries()) {
|
||||
if (event.getSender() !== this.otherUserId) {
|
||||
this._eventsByThem.delete(type);
|
||||
}
|
||||
this._verifier = this._createVerifier(method, event);
|
||||
this._setPhase(PHASE_STARTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by RequestCallbackChannel when the verifier sends an event
|
||||
* @param {string} type the "symbolic" event type
|
||||
* @param {object} content the completed or uncompleted content for the event to be sent
|
||||
*/
|
||||
handleVerifierSend(type, content) {
|
||||
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) {
|
||||
const startSentByMe = startEvent && this._wasSentByMe(startEvent);
|
||||
const {userId, deviceId} = this._getVerifierTarget(startEvent, targetDevice);
|
||||
if (!targetDevice) {
|
||||
targetDevice = this.targetDevice;
|
||||
}
|
||||
const {userId, deviceId} = targetDevice;
|
||||
|
||||
const VerifierCtor = this._verificationMethods.get(method);
|
||||
if (!VerifierCtor) {
|
||||
console.warn("could not find verifier constructor for method", method);
|
||||
logger.warn("could not find verifier constructor for method", method);
|
||||
return;
|
||||
}
|
||||
// invokes handleVerifierSend when verifier sends something
|
||||
const callbackMedium = new RequestCallbackChannel(this, this.channel);
|
||||
return new VerifierCtor(
|
||||
callbackMedium,
|
||||
this.channel,
|
||||
this._client,
|
||||
userId,
|
||||
deviceId,
|
||||
startSentByMe ? null : startEvent,
|
||||
startEvent,
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
_getVerifierTarget(startEvent, targetDevice) {
|
||||
// targetDevice should be set when creating a verifier for to_device before the .start event has been sent,
|
||||
// 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};
|
||||
}
|
||||
_wasSentByOwnUser(event) {
|
||||
return event.getSender() === this._client.getUserId();
|
||||
}
|
||||
|
||||
// only for .request and .start
|
||||
_wasSentByMe(event) {
|
||||
if (event.getSender() !== this._client.getUserId()) {
|
||||
// only for .request, .ready or .start
|
||||
_wasSentByOwnDevice(event) {
|
||||
if (!this._wasSentByOwnUser(event)) {
|
||||
return false;
|
||||
}
|
||||
const content = event.getContent();
|
||||
|
||||
+8
-8
@@ -155,7 +155,7 @@ MatrixHttpApi.prototype = {
|
||||
* data has been uploaded, with an object containing the fields `loaded`
|
||||
* (number of bytes transferred) and `total` (total size, if known).
|
||||
*
|
||||
* @return {module:client.Promise} Resolves to response object, as
|
||||
* @return {Promise} Resolves to response object, as
|
||||
* determined by this.opts.onlyData, opts.rawResponse, and
|
||||
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
|
||||
*/
|
||||
@@ -436,7 +436,7 @@ MatrixHttpApi.prototype = {
|
||||
* @param {Object=} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded). If unspecified, there will be no query params.
|
||||
*
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
* @param {Object} [data] The HTTP JSON body.
|
||||
*
|
||||
* @param {Object|Number=} opts additional options. If a number is specified,
|
||||
* this is treated as `opts.localTimeoutMs`.
|
||||
@@ -449,7 +449,7 @@ MatrixHttpApi.prototype = {
|
||||
*
|
||||
* @param {Object=} opts.headers map of additional request headers
|
||||
*
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
* @return {Promise} Resolves to <code>{data: {Object},
|
||||
* headers: {Object}, code: {Number}}</code>.
|
||||
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
||||
* object only.
|
||||
@@ -518,7 +518,7 @@ MatrixHttpApi.prototype = {
|
||||
* @param {Object=} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded). If unspecified, there will be no query params.
|
||||
*
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
* @param {Object} [data] The HTTP JSON body.
|
||||
*
|
||||
* @param {Object=} opts additional options
|
||||
*
|
||||
@@ -530,7 +530,7 @@ MatrixHttpApi.prototype = {
|
||||
*
|
||||
* @param {Object=} opts.headers map of additional request headers
|
||||
*
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
* @return {Promise} Resolves to <code>{data: {Object},
|
||||
* headers: {Object}, code: {Number}}</code>.
|
||||
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
||||
* object only.
|
||||
@@ -557,7 +557,7 @@ MatrixHttpApi.prototype = {
|
||||
* @param {Object=} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded). If unspecified, there will be no query params.
|
||||
*
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
* @param {Object} [data] The HTTP JSON body.
|
||||
*
|
||||
* @param {Object=} opts additional options
|
||||
*
|
||||
@@ -569,7 +569,7 @@ MatrixHttpApi.prototype = {
|
||||
*
|
||||
* @param {Object=} opts.headers map of additional request headers
|
||||
*
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
* @return {Promise} Resolves to <code>{data: {Object},
|
||||
* headers: {Object}, code: {Number}}</code>.
|
||||
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
||||
* object only.
|
||||
@@ -633,7 +633,7 @@ MatrixHttpApi.prototype = {
|
||||
* @param {function=} opts.bodyParser function to parse the body of the
|
||||
* response before passing it to the promise and callback.
|
||||
*
|
||||
* @return {module:client.Promise} a promise which resolves to either the
|
||||
* @return {Promise} a promise which resolves to either the
|
||||
* response object (if this.opts.onlyData is truthy), or the parsed
|
||||
* body. Rejects
|
||||
*/
|
||||
|
||||
+2
-1
@@ -16,8 +16,9 @@ limitations under the License.
|
||||
|
||||
import * as matrixcs from "./matrix";
|
||||
import * as utils from "./utils";
|
||||
import request from "request";
|
||||
|
||||
matrixcs.request(import("request"));
|
||||
matrixcs.request(request);
|
||||
utils.runPolyfills();
|
||||
|
||||
export * from "./matrix";
|
||||
|
||||
@@ -47,14 +47,14 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
|
||||
* @param {object?} opts.authData error response from the last request. If
|
||||
* null, a request will be made with no auth before starting.
|
||||
*
|
||||
* @param {function(object?): module:client.Promise} opts.doRequest
|
||||
* @param {function(object?): Promise} opts.doRequest
|
||||
* called with the new auth dict to submit the request. Also passes a
|
||||
* second deprecated arg which is a flag set to true if this request
|
||||
* is a background request. The busyChanged callback should be used
|
||||
* instead of the backfround flag. Should return a promise which resolves
|
||||
* to the successful response or rejects with a MatrixError.
|
||||
*
|
||||
* @param {function(bool): module:client.Promise} opts.busyChanged
|
||||
* @param {function(bool): Promise} opts.busyChanged
|
||||
* called whenever the interactive auth logic becomes busy submitting
|
||||
* information provided by the user or finsihes. After this has been
|
||||
* called with true the UI should indicate that a request is in progress
|
||||
@@ -132,7 +132,7 @@ InteractiveAuth.prototype = {
|
||||
/**
|
||||
* begin the authentication process.
|
||||
*
|
||||
* @return {module:client.Promise} which resolves to the response on success,
|
||||
* @return {Promise} which resolves to the response on success,
|
||||
* or rejects with the error on failure. Rejects with NoAuthFlowFoundError if
|
||||
* no suitable authentication flow can be found
|
||||
*/
|
||||
|
||||
+3
-3
@@ -103,11 +103,11 @@ export function setCryptoStoreFactory(fac) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
|
||||
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
|
||||
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
|
||||
* @param {(Object|string)} opts The configuration options for this client. If
|
||||
* this is a string, it is assumed to be the base URL. These configuration
|
||||
* options will be passed directly to {@link module:client~MatrixClient}.
|
||||
* options will be passed directly to {@link module:client.MatrixClient}.
|
||||
* @param {Object} opts.store If not set, defaults to
|
||||
* {@link module:store/memory.MemoryStore}.
|
||||
* @param {Object} opts.scheduler If not set, defaults to
|
||||
@@ -122,7 +122,7 @@ export function setCryptoStoreFactory(fac) {
|
||||
* in-memory otherwise).
|
||||
*
|
||||
* @return {MatrixClient} A new matrix client.
|
||||
* @see {@link module:client~MatrixClient} for the full list of options for
|
||||
* @see {@link module:client.MatrixClient} for the full list of options for
|
||||
* <code>opts</code>.
|
||||
*/
|
||||
export function createClient(opts) {
|
||||
|
||||
@@ -490,8 +490,9 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
|
||||
*
|
||||
* @param {MatrixEvent} event Event to be added
|
||||
* @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) {
|
||||
const events = this._filter.filterRoomTimeline([event]);
|
||||
if (!events.length) {
|
||||
@@ -529,7 +530,7 @@ EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
|
||||
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 {EventTimeline} timeline
|
||||
* @param {boolean} toStartOfTimeline
|
||||
* @param {boolean} fromCache whether the sync response came from cache
|
||||
*
|
||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||
*/
|
||||
EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
|
||||
toStartOfTimeline) {
|
||||
toStartOfTimeline, fromCache) {
|
||||
const eventId = event.getId();
|
||||
timeline.addEvent(event, toStartOfTimeline);
|
||||
this._eventIdToTimeline[eventId] = timeline;
|
||||
@@ -555,7 +557,7 @@ EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
|
||||
|
||||
const data = {
|
||||
timeline: timeline,
|
||||
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
|
||||
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline && !fromCache,
|
||||
};
|
||||
this.emit("Room.timeline", event, this.room,
|
||||
Boolean(toStartOfTimeline), false, data);
|
||||
@@ -828,7 +830,7 @@ EventTimelineSet.prototype.aggregateRelations = function(event) {
|
||||
*
|
||||
* @param {object} data more data about the event
|
||||
*
|
||||
* @param {module:event-timeline.EventTimeline} data.timeline the timeline the
|
||||
* @param {module:models/event-timeline.EventTimeline} data.timeline the timeline the
|
||||
* event was added to/removed from
|
||||
*
|
||||
* @param {boolean} data.liveEvent true if the event was a real-time event
|
||||
|
||||
+11
-1
@@ -22,7 +22,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
import * as utils from '../utils.js';
|
||||
import * as utils from '../utils';
|
||||
import {logger} from '../logger';
|
||||
|
||||
/**
|
||||
@@ -154,6 +154,12 @@ export const MatrixEvent = function(
|
||||
* attempt may succeed)
|
||||
*/
|
||||
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);
|
||||
|
||||
@@ -1054,6 +1060,10 @@ utils.extend(MatrixEvent.prototype, {
|
||||
encrypted: this.event,
|
||||
};
|
||||
},
|
||||
|
||||
setVerificationRequest: function(request) {
|
||||
this.verificationRequest = request;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
+6
-4
@@ -1067,10 +1067,11 @@ Room.prototype.removeFilteredTimelineSet = function(filter) {
|
||||
*
|
||||
* @param {MatrixEvent} event Event to be added
|
||||
* @param {string?} duplicateStrategy 'ignore' or 'replace'
|
||||
* @param {boolean} fromCache whether the sync response came from cache
|
||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||
* @private
|
||||
*/
|
||||
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
||||
Room.prototype._addLiveEvent = function(event, duplicateStrategy, fromCache) {
|
||||
if (event.isRedaction()) {
|
||||
const redactId = event.event.redacts;
|
||||
|
||||
@@ -1117,7 +1118,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
||||
|
||||
// add to our timeline sets
|
||||
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
|
||||
@@ -1427,9 +1428,10 @@ Room.prototype._revertRedactionLocalEcho = function(redactionEvent) {
|
||||
* this function will be ignored entirely, preserving the existing event in the
|
||||
* 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'.
|
||||
*/
|
||||
Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
|
||||
Room.prototype.addLiveEvents = function(events, duplicateStrategy, fromCache) {
|
||||
let i;
|
||||
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
|
||||
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++) {
|
||||
// TODO: We should have a filter to say "only add state event
|
||||
// types X Y Z to the timeline".
|
||||
this._addLiveEvent(events[i], duplicateStrategy);
|
||||
this._addLiveEvent(events[i], duplicateStrategy, fromCache);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+9
-4
@@ -388,8 +388,10 @@ SyncApi.prototype._peekPoll = function(peekRoom, token) {
|
||||
});
|
||||
|
||||
// strip out events which aren't for the given room_id (e.g presence)
|
||||
// and also ephemeral events (which we're assuming is anything without
|
||||
// and event ID because the /events API doesn't separate them).
|
||||
const events = res.chunk.filter(function(e) {
|
||||
return e.room_id === peekRoom.roomId;
|
||||
return e.room_id === peekRoom.roomId && e.event_id;
|
||||
}).map(self.client.getEventMapper());
|
||||
|
||||
peekRoom.addLiveEvents(events);
|
||||
@@ -688,6 +690,7 @@ SyncApi.prototype._syncFromCache = async function(savedSync) {
|
||||
oldSyncToken: null,
|
||||
nextSyncToken,
|
||||
catchingUp: false,
|
||||
fromCache: true,
|
||||
};
|
||||
|
||||
const data = {
|
||||
@@ -1237,7 +1240,8 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
}
|
||||
}
|
||||
|
||||
self._processRoomEvents(room, stateEvents, timelineEvents);
|
||||
self._processRoomEvents(room, stateEvents,
|
||||
timelineEvents, syncEventData.fromCache);
|
||||
|
||||
// set summary after processing events,
|
||||
// because it will trigger a name calculation
|
||||
@@ -1564,10 +1568,11 @@ SyncApi.prototype._resolveInvites = function(room) {
|
||||
* @param {MatrixEvent[]} stateEventList A list of state events. This is the state
|
||||
* at the *START* of the timeline list if it is supplied.
|
||||
* @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.
|
||||
*/
|
||||
SyncApi.prototype._processRoomEvents = function(room, stateEventList,
|
||||
timelineEventList) {
|
||||
timelineEventList, fromCache) {
|
||||
// If there are no events in the timeline yet, initialise it with
|
||||
// the given state events
|
||||
const liveTimeline = room.getLiveTimeline();
|
||||
@@ -1621,7 +1626,7 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
|
||||
// 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
|
||||
// to be decorated with sender etc.
|
||||
room.addLiveEvents(timelineEventList || []);
|
||||
room.addLiveEvents(timelineEventList || [], null, fromCache);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+61
-31
@@ -89,7 +89,7 @@ export function TimelineWindow(client, timelineSet, opts) {
|
||||
* given event
|
||||
* @param {number} [initialWindowSize = 20] Size of the initial window
|
||||
*
|
||||
* @return {module:client.Promise}
|
||||
* @return {Promise}
|
||||
*/
|
||||
TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
|
||||
const self = this;
|
||||
@@ -147,6 +147,62 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the TimelineIndex of the window in the given direction.
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to get the TimelineIndex
|
||||
* at the start of the window; EventTimeline.FORWARDS to get the TimelineIndex at
|
||||
* the end.
|
||||
*
|
||||
* @return {TimelineIndex} The requested timeline index if one exists, null
|
||||
* otherwise.
|
||||
*/
|
||||
TimelineWindow.prototype.getTimelineIndex = function(direction) {
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
return this._start;
|
||||
} else if (direction == EventTimeline.FORWARDS) {
|
||||
return this._end;
|
||||
} else {
|
||||
throw new Error("Invalid direction '" + direction + "'");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to extend the window using events that are already in the underlying
|
||||
* TimelineIndex.
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to try extending it
|
||||
* backwards; EventTimeline.FORWARDS to try extending it forwards.
|
||||
* @param {number} size number of events to try to extend by.
|
||||
*
|
||||
* @return {boolean} true if the window was extended, false otherwise.
|
||||
*/
|
||||
TimelineWindow.prototype.extend = function(direction, size) {
|
||||
const tl = this.getTimelineIndex(direction);
|
||||
|
||||
if (!tl) {
|
||||
debuglog("TimelineWindow: no timeline yet");
|
||||
return false;
|
||||
}
|
||||
|
||||
const count = (direction == EventTimeline.BACKWARDS) ?
|
||||
tl.retreat(size) : tl.advance(size);
|
||||
|
||||
if (count) {
|
||||
this._eventCount += count;
|
||||
debuglog("TimelineWindow: increased cap by " + count +
|
||||
" (now " + this._eventCount + ")");
|
||||
// remove some events from the other end, if necessary
|
||||
const excess = this._eventCount - this._windowLimit;
|
||||
if (excess > 0) {
|
||||
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if this window can be extended
|
||||
*
|
||||
@@ -161,14 +217,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
|
||||
* @return {boolean} true if we can paginate in the given direction
|
||||
*/
|
||||
TimelineWindow.prototype.canPaginate = function(direction) {
|
||||
let tl;
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
tl = this._start;
|
||||
} else if (direction == EventTimeline.FORWARDS) {
|
||||
tl = this._end;
|
||||
} else {
|
||||
throw new Error("Invalid direction '" + direction + "'");
|
||||
}
|
||||
const tl = this.getTimelineIndex(direction);
|
||||
|
||||
if (!tl) {
|
||||
debuglog("TimelineWindow: no timeline yet");
|
||||
@@ -208,7 +257,7 @@ TimelineWindow.prototype.canPaginate = function(direction) {
|
||||
* @param {number} [requestLimit = 5] limit for the number of API requests we
|
||||
* should make.
|
||||
*
|
||||
* @return {module:client.Promise} Resolves to a boolean which is true if more events
|
||||
* @return {Promise} Resolves to a boolean which is true if more events
|
||||
* were successfully retrieved.
|
||||
*/
|
||||
TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
|
||||
@@ -224,14 +273,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
|
||||
requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT;
|
||||
}
|
||||
|
||||
let tl;
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
tl = this._start;
|
||||
} else if (direction == EventTimeline.FORWARDS) {
|
||||
tl = this._end;
|
||||
} else {
|
||||
throw new Error("Invalid direction '" + direction + "'");
|
||||
}
|
||||
const tl = this.getTimelineIndex(direction);
|
||||
|
||||
if (!tl) {
|
||||
debuglog("TimelineWindow: no timeline yet");
|
||||
@@ -243,18 +285,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
|
||||
}
|
||||
|
||||
// try moving the cap
|
||||
const count = (direction == EventTimeline.BACKWARDS) ?
|
||||
tl.retreat(size) : tl.advance(size);
|
||||
|
||||
if (count) {
|
||||
this._eventCount += count;
|
||||
debuglog("TimelineWindow: increased cap by " + count +
|
||||
" (now " + this._eventCount + ")");
|
||||
// remove some events from the other end, if necessary
|
||||
const excess = this._eventCount - this._windowLimit;
|
||||
if (excess > 0) {
|
||||
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
|
||||
}
|
||||
if (this.extend(direction, size)) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
@@ -490,4 +521,3 @@ TimelineIndex.prototype.advance = function(delta) {
|
||||
TimelineIndex.prototype.retreat = function(delta) {
|
||||
return this.advance(delta * -1) * -1;
|
||||
};
|
||||
|
||||
|
||||
+55
-41
@@ -28,7 +28,7 @@ import unhomoglyph from 'unhomoglyph';
|
||||
* {"foo": "bar", "baz": "taz"}
|
||||
* @return {string} The encoded string e.g. foo=bar&baz=taz
|
||||
*/
|
||||
export function encodeParams(params) {
|
||||
export function encodeParams(params: Record<string, string>): string {
|
||||
let qs = "";
|
||||
for (const key in params) {
|
||||
if (!params.hasOwnProperty(key)) {
|
||||
@@ -48,7 +48,8 @@ export function encodeParams(params) {
|
||||
* variables with. E.g. { "$bar": "baz" }.
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
export function encodeUri(pathTemplate, variables) {
|
||||
export function encodeUri(pathTemplate: string,
|
||||
variables: Record<string, string>): string {
|
||||
for (const key in variables) {
|
||||
if (!variables.hasOwnProperty(key)) {
|
||||
continue;
|
||||
@@ -67,7 +68,7 @@ export function encodeUri(pathTemplate, variables) {
|
||||
* the array with the signature <code>fn(element){...}</code>
|
||||
* @return {Array} A new array with the results of the function.
|
||||
*/
|
||||
export function map(array, fn) {
|
||||
export function map<T, S>(array: T[], fn: (t: T) => S): S[] {
|
||||
const results = new Array(array.length);
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
results[i] = fn(array[i]);
|
||||
@@ -83,8 +84,9 @@ export function map(array, fn) {
|
||||
* looks like <code>fn(element, index, array){...}</code>.
|
||||
* @return {Array} A new array with the results of the function.
|
||||
*/
|
||||
export function filter(array, fn) {
|
||||
const results = [];
|
||||
export function filter<T>(array: T[],
|
||||
fn: (t: T, i?: number, a?: T[]) => boolean): T[] {
|
||||
const results: T[] = [];
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (fn(array[i], i, array)) {
|
||||
results.push(array[i]);
|
||||
@@ -98,15 +100,15 @@ export function filter(array, fn) {
|
||||
* @param {Object} obj The object to get the keys for.
|
||||
* @return {string[]} The keys of the object.
|
||||
*/
|
||||
export function keys(obj) {
|
||||
const keys = [];
|
||||
export function keys(obj: object): string[] {
|
||||
const result = [];
|
||||
for (const key in obj) {
|
||||
if (!obj.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
keys.push(key);
|
||||
result.push(key);
|
||||
}
|
||||
return keys;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,15 +116,15 @@ export function keys(obj) {
|
||||
* @param {Object} obj The object to get the values for.
|
||||
* @return {Array<*>} The values of the object.
|
||||
*/
|
||||
export function values(obj) {
|
||||
const values = [];
|
||||
export function values<T>(obj: Record<string, T>): T[] {
|
||||
const result = [];
|
||||
for (const key in obj) {
|
||||
if (!obj.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
values.push(obj[key]);
|
||||
result.push(obj[key]);
|
||||
}
|
||||
return values;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,7 +133,7 @@ export function values(obj) {
|
||||
* @param {Function} fn The function to invoke for each element. Has the
|
||||
* function signature <code>fn(element, index)</code>.
|
||||
*/
|
||||
export function forEach(array, fn) {
|
||||
export function forEach<T>(array: T[], fn: (t: T, i: number) => void) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
fn(array[i], i);
|
||||
}
|
||||
@@ -148,7 +150,11 @@ export function forEach(array, fn) {
|
||||
* @return {*} The first value in the array which returns <code>true</code> for
|
||||
* the given function.
|
||||
*/
|
||||
export function findElement(array, fn, reverse) {
|
||||
export function findElement<T>(
|
||||
array: T[],
|
||||
fn: (t: T, i?: number, a?: T[]) => boolean,
|
||||
reverse?: boolean
|
||||
) {
|
||||
let i;
|
||||
if (reverse) {
|
||||
for (i = array.length - 1; i >= 0; i--) {
|
||||
@@ -175,7 +181,11 @@ export function findElement(array, fn, reverse) {
|
||||
* @param {boolean} reverse True to search in reverse order.
|
||||
* @return {boolean} True if an element was removed.
|
||||
*/
|
||||
export function removeElement(array, fn, reverse) {
|
||||
export function removeElement<T>(
|
||||
array: T[],
|
||||
fn: (t: T, i?: number, a?: T[]) => boolean,
|
||||
reverse?: boolean
|
||||
) {
|
||||
let i;
|
||||
let removed;
|
||||
if (reverse) {
|
||||
@@ -203,8 +213,8 @@ export function removeElement(array, fn, reverse) {
|
||||
* @param {*} value The thing to check.
|
||||
* @return {boolean} True if it is a function.
|
||||
*/
|
||||
export function isFunction(value) {
|
||||
return Object.prototype.toString.call(value) == "[object Function]";
|
||||
export function isFunction(value: any) {
|
||||
return Object.prototype.toString.call(value) === "[object Function]";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,7 +222,7 @@ export function isFunction(value) {
|
||||
* @param {*} value The thing to check.
|
||||
* @return {boolean} True if it is an array.
|
||||
*/
|
||||
export function isArray(value) {
|
||||
export function isArray(value: any) {
|
||||
return Array.isArray ? Array.isArray(value) :
|
||||
Boolean(value && value.constructor === Array);
|
||||
}
|
||||
@@ -223,10 +233,11 @@ export function isArray(value) {
|
||||
* @param {string[]} keys The list of keys that 'obj' must have.
|
||||
* @throws If the object is missing keys.
|
||||
*/
|
||||
export function checkObjectHasKeys(obj, keys) {
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (!obj.hasOwnProperty(keys[i])) {
|
||||
throw new Error("Missing required key: " + keys[i]);
|
||||
// note using 'keys' here would shadow the 'keys' function defined above
|
||||
export function checkObjectHasKeys(obj: object, keys_: string[]) {
|
||||
for (let i = 0; i < keys_.length; i++) {
|
||||
if (!obj.hasOwnProperty(keys_[i])) {
|
||||
throw new Error("Missing required key: " + keys_[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,7 +248,7 @@ export function checkObjectHasKeys(obj, keys) {
|
||||
* @param {string[]} allowedKeys The list of allowed key names.
|
||||
* @throws If there are extra keys.
|
||||
*/
|
||||
export function checkObjectHasNoAdditionalKeys(obj, allowedKeys) {
|
||||
export function checkObjectHasNoAdditionalKeys(obj: object, allowedKeys: string[]): void {
|
||||
for (const key in obj) {
|
||||
if (!obj.hasOwnProperty(key)) {
|
||||
continue;
|
||||
@@ -254,7 +265,7 @@ export function checkObjectHasNoAdditionalKeys(obj, allowedKeys) {
|
||||
* @param {Object} obj The object to deep copy.
|
||||
* @return {Object} A copy of the object without any references to the original.
|
||||
*/
|
||||
export function deepCopy(obj) {
|
||||
export function deepCopy(obj: object): object {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
@@ -266,7 +277,7 @@ export function deepCopy(obj) {
|
||||
*
|
||||
* @return {boolean} true if the two objects are equal
|
||||
*/
|
||||
export function deepCompare(x, y) {
|
||||
export function deepCompare(x: any, y: any): boolean {
|
||||
// Inspired by
|
||||
// http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249
|
||||
|
||||
@@ -377,7 +388,7 @@ export function runPolyfills() {
|
||||
// SOURCE:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
|
||||
if (!Array.prototype.filter) {
|
||||
Array.prototype.filter = function(fun/*, thisArg*/) {
|
||||
Array.prototype.filter = function(fun: Function/*, thisArg*/) {
|
||||
if (this === void 0 || this === null) {
|
||||
throw new TypeError();
|
||||
}
|
||||
@@ -453,7 +464,7 @@ export function runPolyfills() {
|
||||
|
||||
// 8. Repeat, while k < len
|
||||
while (k < len) {
|
||||
var kValue, mappedValue;
|
||||
let kValue, mappedValue;
|
||||
|
||||
// a. Let Pk be ToString(k).
|
||||
// This is implicit for LHS operands of the in operator
|
||||
@@ -538,7 +549,7 @@ export function runPolyfills() {
|
||||
|
||||
// 7. Repeat, while k < len
|
||||
while (k < len) {
|
||||
var kValue;
|
||||
let kValue;
|
||||
|
||||
// a. Let Pk be ToString(k).
|
||||
// This is implicit for LHS operands of the in operator
|
||||
@@ -572,7 +583,7 @@ export function runPolyfills() {
|
||||
* prototype.
|
||||
* @param {function} superCtor Constructor function to inherit prototype from.
|
||||
*/
|
||||
export function inherits(ctor, superCtor) {
|
||||
export function inherits(ctor: Function, superCtor: Function) {
|
||||
// Add util.inherits from Node.js
|
||||
// Source:
|
||||
// https://github.com/joyent/node/blob/master/lib/util.js
|
||||
@@ -596,7 +607,7 @@ export function inherits(ctor, superCtor) {
|
||||
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
ctor.super_ = superCtor;
|
||||
(ctor as any).super_ = superCtor;
|
||||
ctor.prototype = Object.create(superCtor.prototype, {
|
||||
constructor: {
|
||||
value: ctor,
|
||||
@@ -617,7 +628,7 @@ export function inherits(ctor, superCtor) {
|
||||
* @param {any} SuperType The type to act as a super instance
|
||||
* @param {any} params Arguments to supply to the super type's constructor
|
||||
*/
|
||||
export function polyfillSuper(thisArg, SuperType, ...params) {
|
||||
export function polyfillSuper(thisArg: any, SuperType: any, ...params: any[]) {
|
||||
try {
|
||||
SuperType.call(thisArg, ...params);
|
||||
} catch (e) {
|
||||
@@ -633,7 +644,7 @@ export function polyfillSuper(thisArg, SuperType, ...params) {
|
||||
* @param {*} value the value to test
|
||||
* @return {boolean} whether or not value is a finite number without type-coercion
|
||||
*/
|
||||
export function isNumber(value) {
|
||||
export function isNumber(value: any): boolean {
|
||||
return typeof value === 'number' && isFinite(value);
|
||||
}
|
||||
|
||||
@@ -643,7 +654,7 @@ export function isNumber(value) {
|
||||
* @param {string} str the string to remove hidden characters from
|
||||
* @return {string} a string with the hidden characters removed
|
||||
*/
|
||||
export function removeHiddenChars(str) {
|
||||
export function removeHiddenChars(str: string): string {
|
||||
return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, ''));
|
||||
}
|
||||
|
||||
@@ -656,11 +667,11 @@ export function removeHiddenChars(str) {
|
||||
// Zero width no-break space (BOM) U+FEFF
|
||||
const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036f\uFEFF\s]/g;
|
||||
|
||||
export function escapeRegExp(string) {
|
||||
export function escapeRegExp(string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
export function globToRegexp(glob, extended) {
|
||||
export function globToRegexp(glob: string, extended: any): string {
|
||||
extended = typeof(extended) === 'boolean' ? extended : true;
|
||||
// From
|
||||
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
|
||||
@@ -679,7 +690,7 @@ export function globToRegexp(glob, extended) {
|
||||
return pat;
|
||||
}
|
||||
|
||||
export function ensureNoTrailingSlash(url) {
|
||||
export function ensureNoTrailingSlash(url: string): string {
|
||||
if (url && url.endsWith("/")) {
|
||||
return url.substr(0, url.length - 1);
|
||||
} else {
|
||||
@@ -688,13 +699,13 @@ export function ensureNoTrailingSlash(url) {
|
||||
}
|
||||
|
||||
// Returns a promise which resolves with a given value after the given number of ms
|
||||
export function sleep(ms, value) {
|
||||
export function sleep<T>(ms: number, value: T): Promise<T> {
|
||||
return new Promise((resolve => {
|
||||
setTimeout(resolve, ms, value);
|
||||
}));
|
||||
}
|
||||
|
||||
export function isNullOrUndefined(val) {
|
||||
export function isNullOrUndefined(val: any): boolean {
|
||||
return val === null || val === undefined;
|
||||
}
|
||||
|
||||
@@ -711,12 +722,15 @@ export function defer() {
|
||||
return {resolve, reject, promise};
|
||||
}
|
||||
|
||||
export async function promiseMapSeries(promises, fn) {
|
||||
export async function promiseMapSeries<T>(
|
||||
promises: Promise<T>[],
|
||||
fn: (t: T) => void
|
||||
): Promise<void> {
|
||||
for (const o of await promises) {
|
||||
await fn(await o);
|
||||
}
|
||||
}
|
||||
|
||||
export function promiseTry(fn) {
|
||||
export function promiseTry<T>(fn: () => T): Promise<T> {
|
||||
return new Promise((resolve) => resolve(fn()));
|
||||
}
|
||||
Reference in New Issue
Block a user