1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00

Merge branch 'develop' into robertlong/group-call

This commit is contained in:
Robert Long
2021-09-15 11:27:59 -07:00
24 changed files with 1108 additions and 999 deletions

View File

@@ -1,3 +1,22 @@
Changes in [12.5.0](https://github.com/vector-im/element-desktop/releases/tag/v12.5.0) (2021-09-14)
===================================================================================================
## ✨ Features
* [Release] Exclude opt-in Element performance metrics from encryption ([\#1901](https://github.com/matrix-org/matrix-js-sdk/pull/1901)).
* Give `MatrixCall` the capability to emit `LengthChanged` events ([\#1873](https://github.com/matrix-org/matrix-js-sdk/pull/1873)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Improve browser example ([\#1875](https://github.com/matrix-org/matrix-js-sdk/pull/1875)). Contributed by [psrpinto](https://github.com/psrpinto).
* Give `CallFeed` the capability to emit on volume changes ([\#1865](https://github.com/matrix-org/matrix-js-sdk/pull/1865)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## 🐛 Bug Fixes
* Fix verification request cancellation ([\#1871](https://github.com/matrix-org/matrix-js-sdk/pull/1871)).
Changes in [12.4.1](https://github.com/vector-im/element-desktop/releases/tag/v12.4.1) (2021-09-13)
===================================================================================================
## 🔒 SECURITY FIXES
* Fix a security issue with message key sharing. See https://matrix.org/blog/2021/09/13/vulnerability-disclosure-key-sharing
for details.
Changes in [12.4.0](https://github.com/vector-im/element-desktop/releases/tag/v12.4.0) (2021-08-31)
===================================================================================================

View File

@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "12.4.0",
"version": "12.5.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",

View File

@@ -256,97 +256,145 @@ describe("MegolmDecryption", function() {
});
});
it("re-uses sessions for sequential messages", async function() {
mockCrypto.backupManager = {
backupGroupSession: () => {},
};
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
describe("session reuse and key reshares", () => {
let megolmEncryption;
let aliceDeviceInfo;
let mockRoom;
let olmDevice;
const olmDevice = new OlmDevice(cryptoStore);
olmDevice.verifySignature = jest.fn();
await olmDevice.init();
beforeEach(async () => {
mockCrypto.backupManager = {
backupGroupSession: () => {},
};
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:flooble': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally valid',
olmDevice = new OlmDevice(cryptoStore);
olmDevice.verifySignature = jest.fn();
await olmDevice.init();
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:flooble': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally valid',
},
},
},
},
},
},
},
}));
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
}));
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
'@alice:home.server': {
aliceDevice: {
deviceId: 'aliceDevice',
isBlocked: jest.fn().mockReturnValue(false),
isUnverified: jest.fn().mockReturnValue(false),
getIdentityKey: jest.fn().mockReturnValue(
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
),
getFingerprint: jest.fn().mockReturnValue(''),
aliceDeviceInfo = {
deviceId: 'aliceDevice',
isBlocked: jest.fn().mockReturnValue(false),
isUnverified: jest.fn().mockReturnValue(false),
getIdentityKey: jest.fn().mockReturnValue(
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
),
getFingerprint: jest.fn().mockReturnValue(''),
};
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
'@alice:home.server': {
aliceDevice: aliceDeviceInfo,
},
},
}));
}));
mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false,
mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false,
});
megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
config: {
rotation_period_ms: 9999999999999,
},
});
mockRoom = {
getEncryptionTargetMembers: jest.fn().mockReturnValue(
[{ userId: "@alice:home.server" }],
),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
};
});
const megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
config: {
rotation_period_ms: 9999999999999,
},
});
const mockRoom = {
getEncryptionTargetMembers: jest.fn().mockReturnValue(
[{ userId: "@alice:home.server" }],
),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
};
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
it("re-uses sessions for sequential messages", async function() {
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
// this should have claimed a key for alice as it's starting a new session
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
// this should have claimed a key for alice as it's starting a new session
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
mockBaseApis.claimOneTimeKeys.mockReset();
mockBaseApis.claimOneTimeKeys.mockReset();
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some more text",
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some more text",
});
// this should *not* have claimed a key as it should be using the same session
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
// likewise they should show the same session ID
expect(ct2.session_id).toEqual(ct1.session_id);
});
// this should *not* have claimed a key as it should be using the same session
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
it("re-shares keys to devices it's already sent to", async function() {
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
// likewise they should show the same session ID
expect(ct2.session_id).toEqual(ct1.session_id);
mockBaseApis.sendToDevice.mockClear();
await megolmEncryption.reshareKeyWithDevice(
olmDevice.deviceCurve25519Key,
ct1.session_id,
'@alice:home.server',
aliceDeviceInfo,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
});
it("does not re-share keys to devices whose keys have changed", async function() {
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
aliceDeviceInfo.getIdentityKey = jest.fn().mockReturnValue(
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWI',
);
mockBaseApis.sendToDevice.mockClear();
await megolmEncryption.reshareKeyWithDevice(
olmDevice.deviceCurve25519Key,
ct1.session_id,
'@alice:home.server',
aliceDeviceInfo,
);
expect(mockBaseApis.sendToDevice).not.toHaveBeenCalled();
});
});
});

View File

@@ -127,6 +127,45 @@ describe("MSC3089Branch", () => {
expect(stateFn).toHaveBeenCalledTimes(1);
});
it('should be unlocked by default', async () => {
indexEvent.getContent = () => ({ active: true });
const res = branch.isLocked();
expect(res).toEqual(false);
});
it('should use lock status from index event', async () => {
indexEvent.getContent = () => ({ active: true, locked: true });
const res = branch.isLocked();
expect(res).toEqual(true);
});
it('should be able to change its locked status', async () => {
const locked = true;
indexEvent.getContent = () => ({ active: true, retained: true });
const stateFn = jest.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
expect(content).toMatchObject({
retained: true, // canary for copying state
active: true,
locked: locked,
});
expect(stateKey).toEqual(fileEventId);
return Promise.resolve(); // return value not used
});
client.sendStateEvent = stateFn;
await branch.setLocked(locked);
expect(stateFn).toHaveBeenCalledTimes(1);
});
it('should be able to return event information', async () => {
const mxcLatter = "example.org/file";
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
@@ -151,4 +190,21 @@ describe("MSC3089Branch", () => {
httpUrl: expect.stringMatching(`.+${mxcLatter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`),
});
});
it('should be able to return the event object', async () => {
const mxcLatter = "example.org/file";
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
const eventsArr = [
{ getId: () => "$not-file", getContent: () => ({}) },
{ getId: () => fileEventId, getContent: () => ({ file: fileContent }) },
];
client.getEventTimeline = () => Promise.resolve({
getEvents: () => eventsArr,
}) as any as Promise<EventTimeline>; // partial
client.decryptEventIfNeeded = () => Promise.resolve();
const res = await branch.getFileEvent();
expect(res).toBeDefined();
expect(res).toBe(eventsArr[1]);
});
});

View File

@@ -227,40 +227,61 @@ describe("MSC3089TreeSpace", () => {
[targetUser]: expectedPl,
},
});
// Store new power levels so the `getPermissions()` test passes
makePowerLevels(content);
return Promise.resolve();
});
client.sendStateEvent = fn;
await tree.setPermissions(targetUser, role);
expect(fn.mock.calls.length).toBe(1);
const finalPermissions = tree.getPermissions(targetUser);
expect(finalPermissions).toEqual(role);
}
it('should support setting Viewer permissions', () => {
return evaluatePowerLevels({
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
users_default: 1024,
events_default: 1025,
events: {
[EventType.RoomPowerLevels]: 1026,
},
}, TreePermissions.Viewer, 1024);
});
it('should support setting Editor permissions', () => {
return evaluatePowerLevels({
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
events_default: 1024,
}, TreePermissions.Editor, 1024);
users_default: 1024,
events_default: 1025,
events: {
[EventType.RoomPowerLevels]: 1026,
},
}, TreePermissions.Editor, 1025);
});
it('should support setting Owner permissions', () => {
return evaluatePowerLevels({
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
users_default: 1024,
events_default: 1025,
events: {
[EventType.RoomPowerLevels]: 1024,
[EventType.RoomPowerLevels]: 1026,
},
}, TreePermissions.Owner, 1024);
}, TreePermissions.Owner, 1026);
});
it('should support demoting permissions', () => {
return evaluatePowerLevels({
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
users_default: 1024,
events_default: 1025,
events: {
[EventType.RoomPowerLevels]: 1026,
},
users: {
[targetUser]: 2222,
},
@@ -270,11 +291,15 @@ describe("MSC3089TreeSpace", () => {
it('should support promoting permissions', () => {
return evaluatePowerLevels({
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
events_default: 1024,
users_default: 1024,
events_default: 1025,
events: {
[EventType.RoomPowerLevels]: 1026,
},
users: {
[targetUser]: 5,
},
}, TreePermissions.Editor, 1024);
}, TreePermissions.Editor, 1025);
});
it('should support defaults: Viewer', () => {

View File

@@ -33,14 +33,9 @@ declare global {
}
interface Window {
electron?: Electron;
webkitAudioContext: typeof AudioContext;
}
interface Electron {
getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>>;
}
interface Crypto {
webkitSubtle?: Window["crypto"]["subtle"];
}
@@ -67,21 +62,6 @@ declare global {
};
}
interface DesktopCapturerSource {
id: string;
name: string;
thumbnailURL: string;
}
interface GetSourcesOptions {
types: Array<string>;
thumbnailSize?: {
height: number;
width: number;
};
fetchWindowIcons?: boolean;
}
interface HTMLAudioElement {
// sinkId & setSinkId are experimental and typescript doesn't know about them
sinkId: string;
@@ -108,4 +88,11 @@ declare global {
interface PromiseConstructor {
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
}
interface RTCRtpTransceiver {
// This has been removed from TS
// (https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029),
// but we still need this for MatrixCall::getRidOfRTXCodecs()
setCodecPreferences(codecs: RTCRtpCodecCapability[]): void;
}
}

View File

@@ -17,79 +17,19 @@ limitations under the License.
/** @module auto-discovery */
import { IClientWellKnown, IWellKnownConfig } from "./client";
import { logger } from './logger';
import { URL as NodeURL } from "url";
// Dev note: Auto discovery is part of the spec.
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
/**
* Description for what an automatically discovered client configuration
* would look like. Although this is a class, it is recommended that it
* be treated as an interface definition rather than as a class.
*
* Additional properties than those defined here may be present, and
* should follow the Java package naming convention.
*/
class DiscoveredClientConfig { // eslint-disable-line no-unused-vars
// Dev note: this is basically a copy/paste of the .well-known response
// object as defined in the spec. It does have additional information,
// however. Overall, this exists to serve as a place for documentation
// and not functionality.
// See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-well-known-matrix-client
constructor() {
/**
* The homeserver configuration the client should use. This will
* always be present on the object.
* @type {{state: string, base_url: string}} The configuration.
*/
this["m.homeserver"] = {
/**
* The lookup result state. If this is anything other than
* AutoDiscovery.SUCCESS then base_url may be falsey. Additionally,
* if this is not AutoDiscovery.SUCCESS then the client should
* assume the other properties in the client config (such as
* the identity server configuration) are not valid.
*/
state: AutoDiscovery.PROMPT,
/**
* If the state is AutoDiscovery.FAIL_ERROR or .FAIL_PROMPT
* then this will contain a human-readable (English) message
* for what went wrong. If the state is none of those previously
* mentioned, this will be falsey.
*/
error: "Something went wrong",
/**
* The base URL clients should use to talk to the homeserver,
* particularly for the login process. May be falsey if the
* state is not AutoDiscovery.SUCCESS.
*/
base_url: "https://matrix.org",
};
/**
* The identity server configuration the client should use. This
* will always be present on teh object.
* @type {{state: string, base_url: string}} The configuration.
*/
this["m.identity_server"] = {
/**
* The lookup result state. If this is anything other than
* AutoDiscovery.SUCCESS then base_url may be falsey.
*/
state: AutoDiscovery.PROMPT,
/**
* The base URL clients should use for interacting with the
* identity server. May be falsey if the state is not
* AutoDiscovery.SUCCESS.
*/
base_url: "https://vector.im",
};
}
export enum AutoDiscoveryAction {
SUCCESS = "SUCCESS",
IGNORE = "IGNORE",
PROMPT = "PROMPT",
FAIL_PROMPT = "FAIL_PROMPT",
FAIL_ERROR = "FAIL_ERROR",
}
/**
@@ -102,55 +42,36 @@ export class AutoDiscovery {
// translate the meaning of the states in the spec, but also
// support our own if needed.
static get ERROR_INVALID() {
return "Invalid homeserver discovery response";
}
public static readonly ERROR_INVALID = "Invalid homeserver discovery response";
static get ERROR_GENERIC_FAILURE() {
return "Failed to get autodiscovery configuration from server";
}
public static readonly ERROR_GENERIC_FAILURE = "Failed to get autodiscovery configuration from server";
static get ERROR_INVALID_HS_BASE_URL() {
return "Invalid base_url for m.homeserver";
}
public static readonly ERROR_INVALID_HS_BASE_URL = "Invalid base_url for m.homeserver";
static get ERROR_INVALID_HOMESERVER() {
return "Homeserver URL does not appear to be a valid Matrix homeserver";
}
public static readonly ERROR_INVALID_HOMESERVER = "Homeserver URL does not appear to be a valid Matrix homeserver";
static get ERROR_INVALID_IS_BASE_URL() {
return "Invalid base_url for m.identity_server";
}
public static readonly ERROR_INVALID_IS_BASE_URL = "Invalid base_url for m.identity_server";
static get ERROR_INVALID_IDENTITY_SERVER() {
return "Identity server URL does not appear to be a valid identity server";
}
// eslint-disable-next-line
public static readonly ERROR_INVALID_IDENTITY_SERVER = "Identity server URL does not appear to be a valid identity server";
static get ERROR_INVALID_IS() {
return "Invalid identity server discovery response";
}
public static readonly ERROR_INVALID_IS = "Invalid identity server discovery response";
static get ERROR_MISSING_WELLKNOWN() {
return "No .well-known JSON file found";
}
public static readonly ERROR_MISSING_WELLKNOWN = "No .well-known JSON file found";
static get ERROR_INVALID_JSON() {
return "Invalid JSON";
}
public static readonly ERROR_INVALID_JSON = "Invalid JSON";
static get ALL_ERRORS() {
return [
AutoDiscovery.ERROR_INVALID,
AutoDiscovery.ERROR_GENERIC_FAILURE,
AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
AutoDiscovery.ERROR_INVALID_HOMESERVER,
AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
AutoDiscovery.ERROR_INVALID_IS,
AutoDiscovery.ERROR_MISSING_WELLKNOWN,
AutoDiscovery.ERROR_INVALID_JSON,
];
}
public static readonly ALL_ERRORS = [
AutoDiscovery.ERROR_INVALID,
AutoDiscovery.ERROR_GENERIC_FAILURE,
AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
AutoDiscovery.ERROR_INVALID_HOMESERVER,
AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
AutoDiscovery.ERROR_INVALID_IS,
AutoDiscovery.ERROR_MISSING_WELLKNOWN,
AutoDiscovery.ERROR_INVALID_JSON,
];
/**
* The auto discovery failed. The client is expected to communicate
@@ -158,7 +79,7 @@ export class AutoDiscovery {
* @return {string}
* @constructor
*/
static get FAIL_ERROR() { return "FAIL_ERROR"; }
public static readonly FAIL_ERROR = AutoDiscoveryAction.FAIL_ERROR;
/**
* The auto discovery failed, however the client may still recover
@@ -169,7 +90,7 @@ export class AutoDiscovery {
* @return {string}
* @constructor
*/
static get FAIL_PROMPT() { return "FAIL_PROMPT"; }
public static readonly FAIL_PROMPT = AutoDiscoveryAction.FAIL_PROMPT;
/**
* The auto discovery didn't fail but did not find anything of
@@ -178,14 +99,14 @@ export class AutoDiscovery {
* @return {string}
* @constructor
*/
static get PROMPT() { return "PROMPT"; }
public static readonly PROMPT = AutoDiscoveryAction.PROMPT;
/**
* The auto discovery was successful.
* @return {string}
* @constructor
*/
static get SUCCESS() { return "SUCCESS"; }
public static readonly SUCCESS = AutoDiscoveryAction.SUCCESS;
/**
* Validates and verifies client configuration information for purposes
@@ -193,13 +114,13 @@ export class AutoDiscovery {
* and identity server URL the client would want. Additional details
* may also be included, and will be transparently brought into the
* response object unaltered.
* @param {string} wellknown The configuration object itself, as returned
* @param {object} wellknown The configuration object itself, as returned
* by the .well-known auto-discovery endpoint.
* @return {Promise<DiscoveredClientConfig>} Resolves to the verified
* configuration, which may include error states. Rejects on unexpected
* failure, not when verification fails.
*/
static async fromDiscoveryConfig(wellknown) {
public static async fromDiscoveryConfig(wellknown: any): Promise<IClientWellKnown> {
// Step 1 is to get the config, which is provided to us here.
// We default to an error state to make the first few checks easier to
@@ -240,7 +161,7 @@ export class AutoDiscovery {
// Step 2: Make sure the homeserver URL is valid *looking*. We'll make
// sure it points to a homeserver in Step 3.
const hsUrl = this._sanitizeWellKnownUrl(
const hsUrl = this.sanitizeWellKnownUrl(
wellknown["m.homeserver"]["base_url"],
);
if (!hsUrl) {
@@ -250,7 +171,7 @@ export class AutoDiscovery {
}
// Step 3: Make sure the homeserver URL points to a homeserver.
const hsVersions = await this._fetchWellKnownObject(
const hsVersions = await this.fetchWellKnownObject(
`${hsUrl}/_matrix/client/versions`,
);
if (!hsVersions || !hsVersions.raw["versions"]) {
@@ -272,7 +193,7 @@ export class AutoDiscovery {
};
// Step 5: Try to pull out the identity server configuration
let isUrl = "";
let isUrl: string | boolean = "";
if (wellknown["m.identity_server"]) {
// We prepare a failing identity server response to save lines later
// in this branch.
@@ -287,7 +208,7 @@ export class AutoDiscovery {
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it
// points to an identity server in Step 5b.
isUrl = this._sanitizeWellKnownUrl(
isUrl = this.sanitizeWellKnownUrl(
wellknown["m.identity_server"]["base_url"],
);
if (!isUrl) {
@@ -299,10 +220,10 @@ export class AutoDiscovery {
// Step 5b: Verify there is an identity server listening on the provided
// URL.
const isResponse = await this._fetchWellKnownObject(
const isResponse = await this.fetchWellKnownObject(
`${isUrl}/_matrix/identity/api/v1`,
);
if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") {
if (!isResponse || !isResponse.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) {
logger.error("Invalid /api/v1 response");
failingClientConfig["m.identity_server"].error =
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
@@ -317,7 +238,7 @@ export class AutoDiscovery {
// Step 6: Now that the identity server is valid, or never existed,
// populate the IS section.
if (isUrl && isUrl.length > 0) {
if (isUrl && isUrl.toString().length > 0) {
clientConfig["m.identity_server"] = {
state: AutoDiscovery.SUCCESS,
error: null,
@@ -359,7 +280,7 @@ export class AutoDiscovery {
* configuration, which may include error states. Rejects on unexpected
* failure, not when discovery fails.
*/
static async findClientConfig(domain) {
public static async findClientConfig(domain: string): Promise<IClientWellKnown> {
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
@@ -395,13 +316,13 @@ export class AutoDiscovery {
// Step 1: Actually request the .well-known JSON file and make sure it
// at least has a homeserver definition.
const wellknown = await this._fetchWellKnownObject(
const wellknown = await this.fetchWellKnownObject(
`https://${domain}/.well-known/matrix/client`,
);
if (!wellknown || wellknown.action !== "SUCCESS") {
if (!wellknown || wellknown.action !== AutoDiscoveryAction.SUCCESS) {
logger.error("No response or error when parsing .well-known");
if (wellknown.reason) logger.error(wellknown.reason);
if (wellknown.action === "IGNORE") {
if (wellknown.action === AutoDiscoveryAction.IGNORE) {
clientConfig["m.homeserver"] = {
state: AutoDiscovery.PROMPT,
error: null,
@@ -427,12 +348,12 @@ export class AutoDiscovery {
* @returns {Promise<object>} Resolves to the domain's client config. Can
* be an empty object.
*/
static async getRawClientConfig(domain) {
public static async getRawClientConfig(domain: string): Promise<IClientWellKnown> {
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
const response = await this._fetchWellKnownObject(
const response = await this.fetchWellKnownObject(
`https://${domain}/.well-known/matrix/client`,
);
if (!response) return {};
@@ -447,7 +368,7 @@ export class AutoDiscovery {
* @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
* @private
*/
static _sanitizeWellKnownUrl(url) {
private static sanitizeWellKnownUrl(url: string): string | boolean {
if (!url) return false;
try {
@@ -495,8 +416,9 @@ export class AutoDiscovery {
* @return {Promise<object>} Resolves to the returned state.
* @private
*/
static async _fetchWellKnownObject(url) {
private static async fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
return new Promise(function(resolve, reject) {
// eslint-disable-next-line
const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available");
request(
@@ -505,10 +427,10 @@ export class AutoDiscovery {
if (err || response &&
(response.statusCode < 200 || response.statusCode >= 300)
) {
let action = "FAIL_PROMPT";
let action = AutoDiscoveryAction.FAIL_PROMPT;
let reason = (err ? err.message : null) || "General failure";
if (response && response.statusCode === 404) {
action = "IGNORE";
action = AutoDiscoveryAction.IGNORE;
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
}
resolve({ raw: {}, action: action, reason: reason, error: err });
@@ -516,7 +438,7 @@ export class AutoDiscovery {
}
try {
resolve({ raw: JSON.parse(body), action: "SUCCESS" });
resolve({ raw: JSON.parse(body), action: AutoDiscoveryAction.SUCCESS });
} catch (e) {
let reason = AutoDiscovery.ERROR_INVALID;
if (e.name === "SyntaxError") {
@@ -524,7 +446,7 @@ export class AutoDiscovery {
}
resolve({
raw: {},
action: "FAIL_PROMPT",
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: reason,
error: e,
});

View File

@@ -23,7 +23,7 @@ import { EventEmitter } from "events";
import { ISyncStateData, SyncApi } from "./sync";
import { EventStatus, IContent, IDecryptOptions, IEvent, MatrixEvent } from "./models/event";
import { StubStore } from "./store/stub";
import { createNewMatrixCall, MatrixCall, ConstraintsType, getUserMediaContraints, CallType } from "./webrtc/call";
import { createNewMatrixCall, MatrixCall, CallType } from "./webrtc/call";
import { Filter, IFilterDefinition } from "./filter";
import { CallEventHandler } from './webrtc/callEventHandler';
import * as utils from './utils';
@@ -31,7 +31,7 @@ import { sleep } from './utils';
import { Group } from "./models/group";
import { Direction, EventTimeline } from "./models/event-timeline";
import { IActionsObject, PushProcessor } from "./pushprocessor";
import { AutoDiscovery } from "./autodiscovery";
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
import * as olmlib from "./crypto/olmlib";
import { decodeBase64, encodeBase64 } from "./crypto/olmlib";
import { IExportedDevice as IOlmDevice } from "./crypto/OlmDevice";
@@ -145,6 +145,7 @@ import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, Rule
import { IThreepid } from "./@types/threepids";
import { CryptoStore } from "./crypto/store/base";
import { GroupCall } from "./webrtc/groupCall";
import { MediaHandler } from "./webrtc/mediaHandler";
export type Store = IStore;
export type SessionStore = WebStorageSessionStore;
@@ -476,14 +477,19 @@ interface IServerVersions {
unstable_features: Record<string, boolean>;
}
interface IClientWellKnown {
export interface IClientWellKnown {
[key: string]: any;
"m.homeserver": {
base_url: string;
};
"m.identity_server"?: {
base_url: string;
};
"m.homeserver"?: IWellKnownConfig;
"m.identity_server"?: IWellKnownConfig;
}
export interface IWellKnownConfig {
raw?: any; // todo typings
action?: AutoDiscoveryAction;
reason?: string;
error?: Error | string;
// eslint-disable-next-line
base_url?: string | null;
}
interface IKeyBackupPath {
@@ -695,8 +701,6 @@ export class MatrixClient extends EventEmitter {
public iceCandidatePoolSize = 0; // XXX: Intended private, used in code.
public idBaseUrl: string;
public baseUrl: string;
private localAVStreamType: ConstraintsType;
private localAVStream: MediaStream;
// Note: these are all `protected` to let downstream consumers make mistakes if they want to.
// We don't technically support this usage, but have reasons to do this.
@@ -736,6 +740,7 @@ export class MatrixClient extends EventEmitter {
protected checkTurnServersIntervalID: number;
protected exportedOlmDeviceToImport: IOlmDevice;
protected txnCtr = 0;
protected mediaHandler = new MediaHandler();
constructor(opts: IMatrixClientCreateOpts) {
super();
@@ -1243,6 +1248,13 @@ export class MatrixClient extends EventEmitter {
return this.canSupportVoip;
}
/**
* @returns {MediaHandler}
*/
public getMediaHandler(): MediaHandler {
return this.mediaHandler;
}
/**
* Set whether VoIP calls are forced to use only TURN
* candidates. This is the same as the forceTURN option
@@ -1261,43 +1273,6 @@ export class MatrixClient extends EventEmitter {
this.supportsCallTransfer = support;
}
public async getLocalVideoStream() {
if (this.localAVStreamType === ConstraintsType.Video) {
return this.localAVStream.clone();
}
const constraints = getUserMediaContraints(ConstraintsType.Video);
logger.log("Getting user media with constraints", constraints);
const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
this.localAVStreamType = ConstraintsType.Video;
this.localAVStream = mediaStream;
return mediaStream;
}
public async getLocalAudioStream() {
if (this.localAVStreamType === ConstraintsType.Audio) {
return this.localAVStream.clone();
}
const constraints = getUserMediaContraints(ConstraintsType.Audio);
logger.log("Getting user media with constraints", constraints);
const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
this.localAVStreamType = ConstraintsType.Audio;
this.localAVStream = mediaStream;
return mediaStream;
}
public stopLocalMediaStream() {
if (this.localAVStream) {
for (const track of this.localAVStream.getTracks()) {
track.stop();
}
this.localAVStreamType = null;
this.localAVStream = null;
}
}
/**
* Creates a new call.
* The place*Call methods on the returned call can be used to actually place a call
@@ -2083,7 +2058,7 @@ export class MatrixClient extends EventEmitter {
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*/
public createRecoveryKeyFromPassphrase(password: string): Promise<IRecoveryKey> {
public createRecoveryKeyFromPassphrase(password?: string): Promise<IRecoveryKey> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
@@ -2526,7 +2501,7 @@ export class MatrixClient extends EventEmitter {
*/
// TODO: Verify types
public async prepareKeyBackupVersion(
password: string,
password?: string,
opts: IKeyBackupPrepareOpts = { secureSecretStorage: false },
): Promise<Pick<IPreparedKeyBackupVersion, "algorithm" | "auth_data" | "recovery_key">> {
if (!this.crypto) {
@@ -6163,17 +6138,17 @@ export class MatrixClient extends EventEmitter {
public register(
username: string,
password: string,
sessionId: string,
auth: any,
bindThreepids: any,
guestAccessToken: string,
inhibitLogin: boolean,
sessionId: string | null,
auth: { session?: string, type: string },
bindThreepids?: boolean | null | { email?: boolean, msisdn?: boolean },
guestAccessToken?: string,
inhibitLogin?: boolean,
callback?: Callback,
): Promise<any> { // TODO: Types (many)
// backwards compat
if (bindThreepids === true) {
bindThreepids = { email: true };
} else if (bindThreepids === null || bindThreepids === undefined) {
} else if (bindThreepids === null || bindThreepids === undefined || bindThreepids === false) {
bindThreepids = {};
}
if (typeof inhibitLogin === 'function') {
@@ -7507,7 +7482,7 @@ export class MatrixClient extends EventEmitter {
return this.http.authedRequest(undefined, "GET", path, qps, undefined);
}
public uploadDeviceSigningKeys(auth: any, keys: CrossSigningKeys): Promise<{}> { // TODO: types
public uploadDeviceSigningKeys(auth: any, keys?: CrossSigningKeys): Promise<{}> { // TODO: types
const data = Object.assign({}, keys);
if (auth) Object.assign(data, { auth });
return this.http.authedRequest(

View File

@@ -101,6 +101,13 @@ interface IPayload extends Partial<IMessage> {
}
/* eslint-enable camelcase */
interface SharedWithData {
// The identity key of the device we shared with
deviceKey: string;
// The message index of the ratchet we shared with that device
messageIndex: number;
}
/**
* @private
* @constructor
@@ -115,12 +122,12 @@ interface IPayload extends Partial<IMessage> {
*
* @property {object} sharedWithDevices
* devices with which we have shared the session key
* userId -> {deviceId -> msgindex}
* userId -> {deviceId -> SharedWithData}
*/
class OutboundSessionInfo {
public useCount = 0;
public creationTime: number;
public sharedWithDevices: Record<string, Record<string, number>> = {};
public sharedWithDevices: Record<string, Record<string, SharedWithData>> = {};
public blockedDevicesNotified: Record<string, Record<string, boolean>> = {};
constructor(public readonly sessionId: string, public readonly sharedHistory = false) {
@@ -150,11 +157,11 @@ class OutboundSessionInfo {
return false;
}
public markSharedWithDevice(userId: string, deviceId: string, chainIndex: number): void {
public markSharedWithDevice(userId: string, deviceId: string, deviceKey: string, chainIndex: number): void {
if (!this.sharedWithDevices[userId]) {
this.sharedWithDevices[userId] = {};
}
this.sharedWithDevices[userId][deviceId] = chainIndex;
this.sharedWithDevices[userId][deviceId] = { deviceKey, messageIndex: chainIndex };
}
public markNotifiedBlockedDevice(userId: string, deviceId: string): void {
@@ -572,6 +579,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
payload: IPayload,
): Promise<void> {
const contentMap = {};
const deviceInfoByDeviceId = new Map<string, DeviceInfo>();
const promises = [];
for (let i = 0; i < userDeviceMap.length; i++) {
@@ -584,6 +592,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
const userId = val.userId;
const deviceInfo = val.deviceInfo;
const deviceId = deviceInfo.deviceId;
deviceInfoByDeviceId.set(deviceId, deviceInfo);
if (!contentMap[userId]) {
contentMap[userId] = {};
@@ -636,7 +645,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
for (const userId of Object.keys(contentMap)) {
for (const deviceId of Object.keys(contentMap[userId])) {
session.markSharedWithDevice(
userId, deviceId, chainIndex,
userId,
deviceId,
deviceInfoByDeviceId.get(deviceId).getIdentityKey(),
chainIndex,
);
}
}
@@ -719,8 +731,8 @@ class MegolmEncryption extends EncryptionAlgorithm {
logger.debug(`megolm session ${sessionId} never shared with user ${userId}`);
return;
}
const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId];
if (sentChainIndex === undefined) {
const sessionSharedData = obSessionInfo.sharedWithDevices[userId][device.deviceId];
if (sessionSharedData === undefined) {
logger.debug(
"megolm session ID " + sessionId + " never shared with device " +
userId + ":" + device.deviceId,
@@ -728,10 +740,18 @@ class MegolmEncryption extends EncryptionAlgorithm {
return;
}
if (sessionSharedData.deviceKey !== device.getIdentityKey()) {
logger.warn(
`Session has been shared with device ${device.deviceId} but with identity ` +
`key ${sessionSharedData.deviceKey}. Key is now ${device.getIdentityKey()}!`,
);
return;
}
// get the key from the inbound session: the outbound one will already
// have been ratcheted to the next chain index.
const key = await this.olmDevice.getInboundGroupSessionKey(
this.roomId, senderKey, sessionId, sentChainIndex,
this.roomId, senderKey, sessionId, sessionSharedData.messageIndex,
);
if (!key) {
@@ -882,7 +902,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
const deviceId = deviceInfo.deviceId;
session.markSharedWithDevice(
userId, deviceId, key.chain_index,
userId, deviceId, deviceInfo.getIdentityKey(), key.chain_index,
);
}

View File

@@ -58,7 +58,7 @@ export interface IEncryptedEventInfo {
}
export interface IRecoveryKey {
keyInfo: {
keyInfo?: {
pubkey: string;
passphrase?: {
algorithm: string;
@@ -67,7 +67,7 @@ export interface IRecoveryKey {
};
};
privateKey: Uint8Array;
encodedPrivateKey: string;
encodedPrivateKey?: string;
}
export interface ICreateSecretStorageOpts {

View File

@@ -506,7 +506,7 @@ export class Crypto extends EventEmitter {
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*/
public async createRecoveryKeyFromPassphrase(password: string): Promise<IRecoveryKey> {
public async createRecoveryKeyFromPassphrase(password?: string): Promise<IRecoveryKey> {
const decryption = new global.Olm.PkDecryption();
try {
const keyInfo: Partial<IRecoveryKey["keyInfo"]> = {};

View File

@@ -103,6 +103,10 @@ export class VerificationBase extends EventEmitter {
content.from_device === this.baseApis.getDeviceId();
}
public get hasBeenCancelled(): boolean {
return this.cancelled;
}
private resetTimer(): void {
logger.info("Refreshing/starting the verification transaction timeout timer");
if (this.transactionTimeoutTimer !== null) {

View File

@@ -160,6 +160,13 @@ export interface IGeneratedSas {
emoji?: EmojiMapping[];
}
export interface ISasEvent {
sas: IGeneratedSas;
confirm(): Promise<void>;
cancel(): void;
mismatch(): void;
}
function generateSas(sasBytes: number[], methods: string[]): IGeneratedSas {
const sas: IGeneratedSas = {};
for (const method of methods) {
@@ -232,12 +239,7 @@ export class SAS extends Base {
private waitingForAccept: boolean;
public ourSASPubKey: string;
public theirSASPubKey: string;
public sasEvent: {
sas: IGeneratedSas;
confirm(): Promise<void>;
cancel(): void;
mismatch(): void;
};
public sasEvent: ISasEvent;
// eslint-disable-next-line @typescript-eslint/naming-convention
public static get NAME(): string {

View File

@@ -47,12 +47,7 @@ export * from "./crypto/store/memory-crypto-store";
export * from "./crypto/store/indexeddb-crypto-store";
export * from "./content-repo";
export * as ContentHelpers from "./content-helpers";
export {
createNewMatrixCall,
setAudioInput as setMatrixCallAudioInput,
setVideoInput as setMatrixCallVideoInput,
CallType,
} from "./webrtc/call";
export { createNewMatrixCall } from "./webrtc/call";
// expose the underlying request object so different environments can use
// different request libs (e.g. request or browser-request)

View File

@@ -77,6 +77,26 @@ export class MSC3089Branch {
}, this.id);
}
/**
* Gets whether or not a file is locked.
* @returns {boolean} True if locked, false otherwise.
*/
public isLocked(): boolean {
return this.indexEvent.getContent()['locked'] || false;
}
/**
* Sets a file as locked or unlocked.
* @param {boolean} locked True to lock the file, false otherwise.
* @returns {Promise<void>} Resolves when complete.
*/
public async setLocked(locked: boolean): Promise<void> {
await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {
...this.indexEvent.getContent(),
locked: locked,
}, this.id);
}
/**
* Gets information about the file needed to download it.
* @returns {Promise<{info: IEncryptedFile, httpUrl: string}>} Information about the file.

View File

@@ -420,6 +420,11 @@ export class MatrixEvent extends EventEmitter {
|| this.thread instanceof Thread;
}
public get parentEventId(): string {
return this.replyEventId
|| this.getWireContent()["m.relates_to"]?.event_id;
}
/**
* Get the previous event content JSON. This will only return something for
* state events which exist in the timeline.

View File

@@ -103,7 +103,7 @@ export class RoomMember extends EventEmitter {
* @fires module:client~MatrixClient#event:"RoomMember.name"
* @fires module:client~MatrixClient#event:"RoomMember.membership"
*/
public setMembershipEvent(event: MatrixEvent, roomState: RoomState): void {
public setMembershipEvent(event: MatrixEvent, roomState?: RoomState): void {
const displayName = event.getDirectionalContent().displayname;
if (event.getType() !== "m.room.member") {
@@ -322,7 +322,7 @@ export class RoomMember extends EventEmitter {
const MXID_PATTERN = /@.+:.+/;
const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/;
function shouldDisambiguate(selfUserId: string, displayName: string, roomState: RoomState): boolean {
function shouldDisambiguate(selfUserId: string, displayName: string, roomState?: RoomState): boolean {
if (!displayName || displayName === selfUserId) return false;
// First check if the displayname is something we consider truthy

View File

@@ -1305,8 +1305,7 @@ export class Room extends EventEmitter {
this.handleRemoteEcho(event, existingEvent);
}
}
let thread = this.findEventById(event.replyEventId)?.getThread();
let thread = this.findEventById(event.parentEventId)?.getThread();
if (thread) {
thread.addEvent(event);
} else {

View File

@@ -149,6 +149,10 @@ export class Thread extends EventEmitter {
return this.findEventById(this.root);
}
public get roomId(): string {
return this.rootEvent.getRoomId();
}
/**
* The number of messages in the thread
*/

View File

@@ -31,17 +31,22 @@ import { logger } from './logger';
const TIMER_CHECK_PERIOD_MS = 1000;
// counter, for making up ids to return from setTimeout
let _count = 0;
let count = 0;
// the key for our callback with the real global.setTimeout
let _realCallbackKey;
let realCallbackKey: NodeJS.Timeout | number;
// a sorted list of the callbacks to be run.
// each is an object with keys [runAt, func, params, key].
const _callbackList = [];
const callbackList: {
runAt: number;
func: (...params: any[]) => void;
params: any[];
key: number;
}[] = [];
// var debuglog = logger.log.bind(logger);
const debuglog = function() {};
const debuglog = function(...params: any[]) {};
/**
* Replace the function used by this module to get the current time.
@@ -52,10 +57,10 @@ const debuglog = function() {};
*
* @internal
*/
export function setNow(f) {
_now = f || Date.now;
export function setNow(f: () => number): void {
now = f || Date.now;
}
let _now = Date.now;
let now = Date.now;
/**
* reimplementation of window.setTimeout, which will call the callback if
@@ -67,17 +72,16 @@ let _now = Date.now;
* @return {Number} an identifier for this callback, which may be passed into
* clearTimeout later.
*/
export function setTimeout(func, delayMs) {
export function setTimeout(func: (...params: any[]) => void, delayMs: number, ...params: any[]): number {
delayMs = delayMs || 0;
if (delayMs < 0) {
delayMs = 0;
}
const params = Array.prototype.slice.call(arguments, 2);
const runAt = _now() + delayMs;
const key = _count++;
const runAt = now() + delayMs;
const key = count++;
debuglog("setTimeout: scheduling cb", key, "at", runAt,
"(delay", delayMs, ")");
"(delay", delayMs, ")");
const data = {
runAt: runAt,
func: func,
@@ -87,13 +91,13 @@ export function setTimeout(func, delayMs) {
// figure out where it goes in the list
const idx = binarySearch(
_callbackList, function(el) {
callbackList, function(el) {
return el.runAt - runAt;
},
);
_callbackList.splice(idx, 0, data);
_scheduleRealCallback();
callbackList.splice(idx, 0, data);
scheduleRealCallback();
return key;
}
@@ -103,68 +107,69 @@ export function setTimeout(func, delayMs) {
*
* @param {Number} key result from an earlier setTimeout call
*/
export function clearTimeout(key) {
if (_callbackList.length === 0) {
export function clearTimeout(key: number): void {
if (callbackList.length === 0) {
return;
}
// remove the element from the list
let i;
for (i = 0; i < _callbackList.length; i++) {
const cb = _callbackList[i];
for (i = 0; i < callbackList.length; i++) {
const cb = callbackList[i];
if (cb.key == key) {
_callbackList.splice(i, 1);
callbackList.splice(i, 1);
break;
}
}
// iff it was the first one in the list, reschedule our callback.
if (i === 0) {
_scheduleRealCallback();
scheduleRealCallback();
}
}
// use the real global.setTimeout to schedule a callback to _runCallbacks.
function _scheduleRealCallback() {
if (_realCallbackKey) {
global.clearTimeout(_realCallbackKey);
// use the real global.setTimeout to schedule a callback to runCallbacks.
function scheduleRealCallback(): void {
if (realCallbackKey) {
global.clearTimeout(realCallbackKey as NodeJS.Timeout);
}
const first = _callbackList[0];
const first = callbackList[0];
if (!first) {
debuglog("_scheduleRealCallback: no more callbacks, not rescheduling");
debuglog("scheduleRealCallback: no more callbacks, not rescheduling");
return;
}
const now = _now();
const delayMs = Math.min(first.runAt - now, TIMER_CHECK_PERIOD_MS);
const timestamp = now();
const delayMs = Math.min(first.runAt - timestamp, TIMER_CHECK_PERIOD_MS);
debuglog("_scheduleRealCallback: now:", now, "delay:", delayMs);
_realCallbackKey = global.setTimeout(_runCallbacks, delayMs);
debuglog("scheduleRealCallback: now:", timestamp, "delay:", delayMs);
realCallbackKey = global.setTimeout(runCallbacks, delayMs);
}
function _runCallbacks() {
function runCallbacks(): void {
let cb;
const now = _now();
debuglog("_runCallbacks: now:", now);
const timestamp = now();
debuglog("runCallbacks: now:", timestamp);
// get the list of things to call
const callbacksToRun = [];
// eslint-disable-next-line
while (true) {
const first = _callbackList[0];
if (!first || first.runAt > now) {
const first = callbackList[0];
if (!first || first.runAt > timestamp) {
break;
}
cb = _callbackList.shift();
debuglog("_runCallbacks: popping", cb.key);
cb = callbackList.shift();
debuglog("runCallbacks: popping", cb.key);
callbacksToRun.push(cb);
}
// reschedule the real callback before running our functions, to
// keep the codepaths the same whether or not our functions
// register their own setTimeouts.
_scheduleRealCallback();
scheduleRealCallback();
for (let i = 0; i < callbacksToRun.length; i++) {
cb = callbacksToRun[i];
@@ -172,7 +177,7 @@ function _runCallbacks() {
cb.func.apply(global, cb.params);
} catch (e) {
logger.error("Uncaught exception in callback function",
e.stack || e);
e.stack || e);
}
}
}
@@ -182,7 +187,7 @@ function _runCallbacks() {
* returns the index of the last element for which func returns
* greater than zero, or array.length if no such element exists.
*/
function binarySearch(array, func) {
function binarySearch<T>(array: T[], func: (v: T) => number): number {
// min is inclusive, max exclusive.
let min = 0;
let max = array.length;

View File

@@ -315,7 +315,19 @@ export class SyncApi {
public partitionThreadedEvents(events: MatrixEvent[]): [MatrixEvent[], MatrixEvent[]] {
if (this.opts.experimentalThreadSupport) {
return events.reduce((memo, event: MatrixEvent) => {
memo[event.replyInThread ? 1 : 0].push(event);
const room = this.client.getRoom(event.getRoomId());
// An event should live in the thread timeline if
// - It's a reply in thread event
// - It's related to a reply in thread event
let shouldLiveInThreadTimeline = event.replyInThread;
if (!shouldLiveInThreadTimeline) {
const parentEventId = event.getWireContent()["m.relates_to"]?.event_id;
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => {
return mxEv.getId() === parentEventId;
});
shouldLiveInThreadTimeline = parentEvent?.replyInThread;
}
memo[shouldLiveInThreadTimeline ? 1 : 0].push(event);
return memo;
}, [[], []]);
} else {

View File

@@ -214,11 +214,6 @@ export enum CallErrorCode {
Transfered = 'transferred',
}
export enum ConstraintsType {
Audio = "audio",
Video = "video",
}
/**
* The version field that we set in m.call.* events
*/
@@ -230,21 +225,6 @@ const FALLBACK_ICE_SERVER = 'stun:turn.matrix.org';
/** The length of time a call can be ringing for. */
const CALL_TIMEOUT_MS = 60000;
/** Retrieves sources from desktopCapturer */
export function getDesktopCapturerSources(): Promise<Array<DesktopCapturerSource>> {
const options: GetSourcesOptions = {
thumbnailSize: {
height: 176,
width: 312,
},
types: [
"screen",
"window",
],
};
return window.electron.getDesktopCapturerSources(options);
}
export class CallError extends Error {
code: string;
@@ -273,7 +253,6 @@ function genCallID(): string {
*/
export class MatrixCall extends EventEmitter {
public roomId: string;
public type: CallType = null;
public callId: string;
public invitee?: string;
public state = CallState.Fledgling;
@@ -357,10 +336,7 @@ export class MatrixCall extends EventEmitter {
* @throws If you have not specified a listener for 'error' events.
*/
public async placeVoiceCall(): Promise<void> {
logger.debug("placeVoiceCall");
this.checkForErrorListener();
this.type = CallType.Voice;
await this.placeCallWithConstraints(ConstraintsType.Audio);
await this.placeCall(true, false);
}
/**
@@ -368,17 +344,7 @@ export class MatrixCall extends EventEmitter {
* @throws If you have not specified a listener for 'error' events.
*/
public async placeVideoCall(): Promise<void> {
logger.debug("placeVideoCall");
this.checkForErrorListener();
this.type = CallType.Video;
await this.placeCallWithConstraints(ConstraintsType.Video);
}
public createDataChannel(label: string, options: RTCDataChannelInit) {
logger.debug("createDataChannel");
const dataChannel = this.peerConn.createDataChannel(label, options);
this.emit(CallEvent.Datachannel, dataChannel);
return dataChannel;
await this.placeCall(true, true);
}
public getOpponentMember(): RoomMember {
@@ -397,6 +363,25 @@ export class MatrixCall extends EventEmitter {
return this.remoteAssertedIdentity;
}
public get type(): CallType {
return (this.hasLocalUserMediaVideoTrack || this.hasRemoteUserMediaVideoTrack)
? CallType.Video
: CallType.Voice;
}
public get hasLocalUserMediaVideoTrack(): boolean {
return this.localUsermediaStream?.getVideoTracks().length > 0;
}
public get hasRemoteUserMediaVideoTrack(): boolean {
return this.getRemoteFeeds().some((feed) => {
return (
feed.purpose === SDPStreamMetadataPurpose.Usermedia &&
feed.stream.getVideoTracks().length > 0
);
});
}
public get localUsermediaFeed(): CallFeed {
return this.getLocalFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia);
}
@@ -660,8 +645,6 @@ export class MatrixCall extends EventEmitter {
return;
}
this.type = remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice;
this.setState(CallState.Ringing);
if (event.getLocalAge()) {
@@ -699,27 +682,17 @@ export class MatrixCall extends EventEmitter {
return;
}
logger.debug(`Answering call ${this.callId} of type ${this.type}`);
logger.debug(`Answering call ${this.callId}`);
if (!this.localUsermediaStream && !this.waitForLocalAVStream) {
const constraints = getUserMediaContraints(
this.type == CallType.Video ?
ConstraintsType.Video:
ConstraintsType.Audio,
);
logger.log("Getting user media with constraints", constraints);
this.setState(CallState.WaitLocalMedia);
this.waitForLocalAVStream = true;
try {
let mediaStream: MediaStream;
if (this.type === CallType.Voice) {
mediaStream = await this.client.getLocalAudioStream();
} else {
mediaStream = await this.client.getLocalVideoStream();
}
const mediaStream = await this.client.getMediaHandler().getUserMediaStream(
true,
this.hasRemoteUserMediaVideoTrack,
);
this.waitForLocalAVStream = false;
this.gotUserMediaForAnswer(mediaStream);
} catch (e) {
@@ -811,12 +784,11 @@ export class MatrixCall extends EventEmitter {
/**
* Starts/stops screensharing
* @param enabled the desired screensharing state
* @param selectDesktopCapturerSource callBack to select a screensharing stream on desktop
* @param {string} desktopCapturerSourceId optional id of the desktop capturer source to use
* @returns {boolean} new screensharing state
*/
public async setScreensharingEnabled(
enabled: boolean,
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
enabled: boolean, desktopCapturerSourceId?: string,
): Promise<boolean> {
// Skip if there is nothing to do
if (enabled && this.isScreensharing()) {
@@ -829,13 +801,13 @@ export class MatrixCall extends EventEmitter {
// Fallback to replaceTrack()
if (!this.opponentSupportsSDPStreamMetadata()) {
return await this.setScreensharingEnabledWithoutMetadataSupport(enabled, selectDesktopCapturerSource);
return await this.setScreensharingEnabledWithoutMetadataSupport(enabled, desktopCapturerSourceId);
}
logger.debug(`Set screensharing enabled? ${enabled}`);
if (enabled) {
try {
const stream = await getScreensharingStream(selectDesktopCapturerSource);
const stream = await this.client.getMediaHandler().getScreensharingStream(desktopCapturerSourceId);
if (!stream) return false;
this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare);
return true;
@@ -861,17 +833,16 @@ export class MatrixCall extends EventEmitter {
* Starts/stops screensharing
* Should be used ONLY if the opponent doesn't support SDPStreamMetadata
* @param enabled the desired screensharing state
* @param selectDesktopCapturerSource callBack to select a screensharing stream on desktop
* @param {string} desktopCapturerSourceId optional id of the desktop capturer source to use
* @returns {boolean} new screensharing state
*/
private async setScreensharingEnabledWithoutMetadataSupport(
enabled: boolean,
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
enabled: boolean, desktopCapturerSourceId?: string,
): Promise<boolean> {
logger.debug(`Set screensharing enabled? ${enabled} using replaceTrack()`);
if (enabled) {
try {
const stream = await getScreensharingStream(selectDesktopCapturerSource);
const stream = await this.client.getMediaHandler().getScreensharingStream(desktopCapturerSourceId);
if (!stream) return false;
const track = stream.getTracks().find((track) => {
@@ -1041,7 +1012,7 @@ export class MatrixCall extends EventEmitter {
this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia);
this.setState(CallState.CreateOffer);
logger.debug("gotUserMediaForInvite -> " + this.type);
logger.debug("gotUserMediaForInvite");
// Now we wait for the negotiationneeded event
};
@@ -1861,7 +1832,17 @@ export class MatrixCall extends EventEmitter {
}
}
private async placeCallWithConstraints(constraintsType: ConstraintsType): Promise<void> {
/**
* Place a call to this room.
* @throws if you have not specified a listener for 'error' events.
* @throws if have passed audio=false.
*/
public async placeCall(audio: boolean, video: boolean): Promise<void> {
logger.debug(`placeCall audio=${audio} video=${video}`);
if (!audio) {
throw new Error("You CANNOT start a call without audio");
}
this.checkForErrorListener();
// XXX Find a better way to do this
this.client.callEventHandler.calls.set(this.callId, this);
this.setState(CallState.WaitLocalMedia);
@@ -1879,14 +1860,7 @@ export class MatrixCall extends EventEmitter {
this.peerConn = this.createPeerConnection();
try {
let mediaStream: MediaStream;
if (constraintsType === ConstraintsType.Audio) {
mediaStream = await this.client.getLocalAudioStream();
} else {
mediaStream = await this.client.getLocalVideoStream();
}
const mediaStream = await this.client.getMediaHandler().getUserMediaStream(audio, video);
this.gotUserMediaForInvite(mediaStream);
} catch (e) {
this.getUserMediaFailed(e);
@@ -1981,105 +1955,12 @@ export class MatrixCall extends EventEmitter {
}
}
async function getScreensharingStream(
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
): Promise<MediaStream> {
const screenshareConstraints = await getScreenshareContraints(selectDesktopCapturerSource);
if (!screenshareConstraints) return null;
if (window.electron?.getDesktopCapturerSources) {
// We are using Electron
logger.debug("Getting screen stream using getUserMedia()...");
return await navigator.mediaDevices.getUserMedia(screenshareConstraints);
} else {
// We are not using Electron
logger.debug("Getting screen stream using getDisplayMedia()...");
return await navigator.mediaDevices.getDisplayMedia(screenshareConstraints);
}
}
function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean): void {
for (let i = 0; i < tracks.length; i++) {
tracks[i].enabled = enabled;
}
}
export function getUserMediaContraints(type: ConstraintsType): MediaStreamConstraints {
const isWebkit = !!navigator.webkitGetUserMedia;
switch (type) {
case ConstraintsType.Audio: {
return {
audio: {
deviceId: audioInput ? { ideal: audioInput } : undefined,
},
video: false,
};
}
case ConstraintsType.Video: {
return {
audio: {
deviceId: audioInput ? { ideal: audioInput } : undefined,
}, video: {
deviceId: videoInput ? { ideal: videoInput } : undefined,
/* We want 640x360. Chrome will give it only if we ask exactly,
FF refuses entirely if we ask exactly, so have to ask for ideal
instead
XXX: Is this still true?
*/
width: isWebkit ? { exact: 640 } : { ideal: 640 },
height: isWebkit ? { exact: 360 } : { ideal: 360 },
},
};
}
}
}
async function getScreenshareContraints(
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
): Promise<DesktopCapturerConstraints> {
if (window.electron?.getDesktopCapturerSources && selectDesktopCapturerSource) {
// We have access to getDesktopCapturerSources()
logger.debug("Electron getDesktopCapturerSources() is available...");
const selectedSource = await selectDesktopCapturerSource();
if (!selectedSource) return null;
return {
audio: false,
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: selectedSource.id,
},
},
};
} else {
// We do not have access to the Electron desktop capturer,
// therefore we can assume we are on the web
logger.debug("Electron desktopCapturer is not available...");
return {
audio: false,
video: true,
};
}
}
let audioInput: string;
let videoInput: string;
/**
* Set an audio input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
export function setAudioInput(deviceId: string): void { audioInput = deviceId; }
/**
* Set a video input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
export function setVideoInput(deviceId: string): void { videoInput = deviceId; }
/**
* DEPRECATED
* Use client.createCall()

115
src/webrtc/mediaHandler.ts Normal file
View File

@@ -0,0 +1,115 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
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 { logger } from "../logger";
export class MediaHandler {
private audioInput: string;
private videoInput: string;
/**
* Set an audio input device to use for MatrixCalls
* @param {string} deviceId the identifier for the device
* undefined treated as unset
*/
public setAudioInput(deviceId: string): void {
this.audioInput = deviceId;
}
/**
* Set a video input device to use for MatrixCalls
* @param {string} deviceId the identifier for the device
* undefined treated as unset
*/
public setVideoInput(deviceId: string): void {
this.videoInput = deviceId;
}
/**
* @returns {MediaStream} based on passed parameters
*/
public async getUserMediaStream(audio: boolean, video: boolean): Promise<MediaStream> {
const constraints = this.getUserMediaContraints(audio, video);
logger.log("Getting user media with constraints", constraints);
return await navigator.mediaDevices.getUserMedia(constraints);
}
/**
* @returns {MediaStream} based on passed parameters
*/
public async getScreensharingStream(desktopCapturerSourceId: string): Promise<MediaStream> {
const screenshareConstraints = this.getScreenshareContraints(desktopCapturerSourceId);
if (!screenshareConstraints) return null;
if (desktopCapturerSourceId) {
// We are using Electron
logger.debug("Getting screen stream using getUserMedia()...");
return await navigator.mediaDevices.getUserMedia(screenshareConstraints);
} else {
// We are not using Electron
logger.debug("Getting screen stream using getDisplayMedia()...");
return await navigator.mediaDevices.getDisplayMedia(screenshareConstraints);
}
}
private getUserMediaContraints(audio: boolean, video: boolean): MediaStreamConstraints {
const isWebkit = !!navigator.webkitGetUserMedia;
return {
audio: audio
? {
deviceId: this.audioInput ? { ideal: this.audioInput } : undefined,
}
: false,
video: video
? {
deviceId: this.videoInput ? { ideal: this.videoInput } : undefined,
/* We want 640x360. Chrome will give it only if we ask exactly,
FF refuses entirely if we ask exactly, so have to ask for ideal
instead
XXX: Is this still true?
*/
width: isWebkit ? { exact: 640 } : { ideal: 640 },
height: isWebkit ? { exact: 360 } : { ideal: 360 },
}
: false,
};
}
private getScreenshareContraints(desktopCapturerSourceId?: string): DesktopCapturerConstraints {
if (desktopCapturerSourceId) {
logger.debug("Using desktop capturer source", desktopCapturerSourceId);
return {
audio: false,
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: desktopCapturerSourceId,
},
},
};
} else {
logger.debug("Not using desktop capturer source");
return {
audio: false,
video: true,
};
}
}
}

965
yarn.lock

File diff suppressed because it is too large Load Diff