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) 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", "name": "matrix-js-sdk",
"version": "12.4.0", "version": "12.5.0",
"description": "Matrix Client-Server SDK for Javascript", "description": "Matrix Client-Server SDK for Javascript",
"scripts": { "scripts": {
"prepublishOnly": "yarn build", "prepublishOnly": "yarn build",

View File

@@ -256,97 +256,145 @@ describe("MegolmDecryption", function() {
}); });
}); });
it("re-uses sessions for sequential messages", async function() { describe("session reuse and key reshares", () => {
mockCrypto.backupManager = { let megolmEncryption;
backupGroupSession: () => {}, let aliceDeviceInfo;
}; let mockRoom;
const mockStorage = new MockStorageApi(); let olmDevice;
const cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(cryptoStore); beforeEach(async () => {
olmDevice.verifySignature = jest.fn(); mockCrypto.backupManager = {
await olmDevice.init(); backupGroupSession: () => {},
};
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({ olmDevice = new OlmDevice(cryptoStore);
one_time_keys: { olmDevice.verifySignature = jest.fn();
'@alice:home.server': { await olmDevice.init();
aliceDevice: {
'signed_curve25519:flooble': { mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI', one_time_keys: {
signatures: { '@alice:home.server': {
'@alice:home.server': { aliceDevice: {
'ed25519:aliceDevice': 'totally valid', '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({ aliceDeviceInfo = {
'@alice:home.server': { deviceId: 'aliceDevice',
aliceDevice: { isBlocked: jest.fn().mockReturnValue(false),
deviceId: 'aliceDevice', isUnverified: jest.fn().mockReturnValue(false),
isBlocked: jest.fn().mockReturnValue(false), getIdentityKey: jest.fn().mockReturnValue(
isUnverified: jest.fn().mockReturnValue(false), 'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
getIdentityKey: jest.fn().mockReturnValue( ),
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE', getFingerprint: jest.fn().mockReturnValue(''),
), };
getFingerprint: jest.fn().mockReturnValue(''),
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
'@alice:home.server': {
aliceDevice: aliceDeviceInfo,
}, },
}, }));
}));
mockCrypto.checkDeviceTrust.mockReturnValue({ mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false, 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({ it("re-uses sessions for sequential messages", async function() {
userId: '@user:id', const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
crypto: mockCrypto, body: "Some text",
olmDevice: olmDevice, });
baseApis: mockBaseApis, expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
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();
// this should have claimed a key for alice as it's starting a new session // this should have claimed a key for alice as it's starting a new session
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith( expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000, [['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
); );
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith( expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false, ['@alice:home.server'], false,
); );
expect(mockBaseApis.sendToDevice).toHaveBeenCalled(); expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith( expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000, [['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
); );
mockBaseApis.claimOneTimeKeys.mockReset(); mockBaseApis.claimOneTimeKeys.mockReset();
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", { const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some more text", 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 it("re-shares keys to devices it's already sent to", async function() {
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled(); const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
// likewise they should show the same session ID mockBaseApis.sendToDevice.mockClear();
expect(ct2.session_id).toEqual(ct1.session_id); 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); 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 () => { it('should be able to return event information', async () => {
const mxcLatter = "example.org/file"; const mxcLatter = "example.org/file";
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter }; const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
@@ -151,4 +190,21 @@ describe("MSC3089Branch", () => {
httpUrl: expect.stringMatching(`.+${mxcLatter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`), 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, [targetUser]: expectedPl,
}, },
}); });
// Store new power levels so the `getPermissions()` test passes
makePowerLevels(content);
return Promise.resolve(); return Promise.resolve();
}); });
client.sendStateEvent = fn; client.sendStateEvent = fn;
await tree.setPermissions(targetUser, role); await tree.setPermissions(targetUser, role);
expect(fn.mock.calls.length).toBe(1); expect(fn.mock.calls.length).toBe(1);
const finalPermissions = tree.getPermissions(targetUser);
expect(finalPermissions).toEqual(role);
} }
it('should support setting Viewer permissions', () => { it('should support setting Viewer permissions', () => {
return evaluatePowerLevels({ return evaluatePowerLevels({
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE, ...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
users_default: 1024, users_default: 1024,
events_default: 1025,
events: {
[EventType.RoomPowerLevels]: 1026,
},
}, TreePermissions.Viewer, 1024); }, TreePermissions.Viewer, 1024);
}); });
it('should support setting Editor permissions', () => { it('should support setting Editor permissions', () => {
return evaluatePowerLevels({ return evaluatePowerLevels({
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE, ...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
events_default: 1024, users_default: 1024,
}, TreePermissions.Editor, 1024); events_default: 1025,
events: {
[EventType.RoomPowerLevels]: 1026,
},
}, TreePermissions.Editor, 1025);
}); });
it('should support setting Owner permissions', () => { it('should support setting Owner permissions', () => {
return evaluatePowerLevels({ return evaluatePowerLevels({
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE, ...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
users_default: 1024,
events_default: 1025,
events: { events: {
[EventType.RoomPowerLevels]: 1024, [EventType.RoomPowerLevels]: 1026,
}, },
}, TreePermissions.Owner, 1024); }, TreePermissions.Owner, 1026);
}); });
it('should support demoting permissions', () => { it('should support demoting permissions', () => {
return evaluatePowerLevels({ return evaluatePowerLevels({
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE, ...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
users_default: 1024, users_default: 1024,
events_default: 1025,
events: {
[EventType.RoomPowerLevels]: 1026,
},
users: { users: {
[targetUser]: 2222, [targetUser]: 2222,
}, },
@@ -270,11 +291,15 @@ describe("MSC3089TreeSpace", () => {
it('should support promoting permissions', () => { it('should support promoting permissions', () => {
return evaluatePowerLevels({ return evaluatePowerLevels({
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE, ...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
events_default: 1024, users_default: 1024,
events_default: 1025,
events: {
[EventType.RoomPowerLevels]: 1026,
},
users: { users: {
[targetUser]: 5, [targetUser]: 5,
}, },
}, TreePermissions.Editor, 1024); }, TreePermissions.Editor, 1025);
}); });
it('should support defaults: Viewer', () => { it('should support defaults: Viewer', () => {

View File

@@ -33,14 +33,9 @@ declare global {
} }
interface Window { interface Window {
electron?: Electron;
webkitAudioContext: typeof AudioContext; webkitAudioContext: typeof AudioContext;
} }
interface Electron {
getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>>;
}
interface Crypto { interface Crypto {
webkitSubtle?: Window["crypto"]["subtle"]; 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 { interface HTMLAudioElement {
// sinkId & setSinkId are experimental and typescript doesn't know about them // sinkId & setSinkId are experimental and typescript doesn't know about them
sinkId: string; sinkId: string;
@@ -108,4 +88,11 @@ declare global {
interface PromiseConstructor { interface PromiseConstructor {
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>; 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 */ /** @module auto-discovery */
import { IClientWellKnown, IWellKnownConfig } from "./client";
import { logger } from './logger'; import { logger } from './logger';
import { URL as NodeURL } from "url"; import { URL as NodeURL } from "url";
// Dev note: Auto discovery is part of the spec. // Dev note: Auto discovery is part of the spec.
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery // See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
/** export enum AutoDiscoveryAction {
* Description for what an automatically discovered client configuration SUCCESS = "SUCCESS",
* would look like. Although this is a class, it is recommended that it IGNORE = "IGNORE",
* be treated as an interface definition rather than as a class. PROMPT = "PROMPT",
* FAIL_PROMPT = "FAIL_PROMPT",
* Additional properties than those defined here may be present, and FAIL_ERROR = "FAIL_ERROR",
* 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",
};
}
} }
/** /**
@@ -102,55 +42,36 @@ export class AutoDiscovery {
// translate the meaning of the states in the spec, but also // translate the meaning of the states in the spec, but also
// support our own if needed. // support our own if needed.
static get ERROR_INVALID() { public static readonly ERROR_INVALID = "Invalid homeserver discovery response";
return "Invalid homeserver discovery response";
}
static get ERROR_GENERIC_FAILURE() { public static readonly ERROR_GENERIC_FAILURE = "Failed to get autodiscovery configuration from server";
return "Failed to get autodiscovery configuration from server";
}
static get ERROR_INVALID_HS_BASE_URL() { public static readonly ERROR_INVALID_HS_BASE_URL = "Invalid base_url for m.homeserver";
return "Invalid base_url for m.homeserver";
}
static get ERROR_INVALID_HOMESERVER() { public static readonly ERROR_INVALID_HOMESERVER = "Homeserver URL does not appear to be a valid Matrix homeserver";
return "Homeserver URL does not appear to be a valid Matrix homeserver";
}
static get ERROR_INVALID_IS_BASE_URL() { public static readonly ERROR_INVALID_IS_BASE_URL = "Invalid base_url for m.identity_server";
return "Invalid base_url for m.identity_server";
}
static get ERROR_INVALID_IDENTITY_SERVER() { // eslint-disable-next-line
return "Identity server URL does not appear to be a valid identity server"; public static readonly ERROR_INVALID_IDENTITY_SERVER = "Identity server URL does not appear to be a valid identity server";
}
static get ERROR_INVALID_IS() { public static readonly ERROR_INVALID_IS = "Invalid identity server discovery response";
return "Invalid identity server discovery response";
}
static get ERROR_MISSING_WELLKNOWN() { public static readonly ERROR_MISSING_WELLKNOWN = "No .well-known JSON file found";
return "No .well-known JSON file found";
}
static get ERROR_INVALID_JSON() { public static readonly ERROR_INVALID_JSON = "Invalid JSON";
return "Invalid JSON";
}
static get ALL_ERRORS() { public static readonly ALL_ERRORS = [
return [ AutoDiscovery.ERROR_INVALID,
AutoDiscovery.ERROR_INVALID, AutoDiscovery.ERROR_GENERIC_FAILURE,
AutoDiscovery.ERROR_GENERIC_FAILURE, AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
AutoDiscovery.ERROR_INVALID_HS_BASE_URL, AutoDiscovery.ERROR_INVALID_HOMESERVER,
AutoDiscovery.ERROR_INVALID_HOMESERVER, AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
AutoDiscovery.ERROR_INVALID_IS_BASE_URL, AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER, AutoDiscovery.ERROR_INVALID_IS,
AutoDiscovery.ERROR_INVALID_IS, AutoDiscovery.ERROR_MISSING_WELLKNOWN,
AutoDiscovery.ERROR_MISSING_WELLKNOWN, AutoDiscovery.ERROR_INVALID_JSON,
AutoDiscovery.ERROR_INVALID_JSON, ];
];
}
/** /**
* The auto discovery failed. The client is expected to communicate * The auto discovery failed. The client is expected to communicate
@@ -158,7 +79,7 @@ export class AutoDiscovery {
* @return {string} * @return {string}
* @constructor * @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 * The auto discovery failed, however the client may still recover
@@ -169,7 +90,7 @@ export class AutoDiscovery {
* @return {string} * @return {string}
* @constructor * @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 * The auto discovery didn't fail but did not find anything of
@@ -178,14 +99,14 @@ export class AutoDiscovery {
* @return {string} * @return {string}
* @constructor * @constructor
*/ */
static get PROMPT() { return "PROMPT"; } public static readonly PROMPT = AutoDiscoveryAction.PROMPT;
/** /**
* The auto discovery was successful. * The auto discovery was successful.
* @return {string} * @return {string}
* @constructor * @constructor
*/ */
static get SUCCESS() { return "SUCCESS"; } public static readonly SUCCESS = AutoDiscoveryAction.SUCCESS;
/** /**
* Validates and verifies client configuration information for purposes * 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 * and identity server URL the client would want. Additional details
* may also be included, and will be transparently brought into the * may also be included, and will be transparently brought into the
* response object unaltered. * 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. * by the .well-known auto-discovery endpoint.
* @return {Promise<DiscoveredClientConfig>} Resolves to the verified * @return {Promise<DiscoveredClientConfig>} Resolves to the verified
* configuration, which may include error states. Rejects on unexpected * configuration, which may include error states. Rejects on unexpected
* failure, not when verification fails. * 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. // 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 // 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 // Step 2: Make sure the homeserver URL is valid *looking*. We'll make
// sure it points to a homeserver in Step 3. // sure it points to a homeserver in Step 3.
const hsUrl = this._sanitizeWellKnownUrl( const hsUrl = this.sanitizeWellKnownUrl(
wellknown["m.homeserver"]["base_url"], wellknown["m.homeserver"]["base_url"],
); );
if (!hsUrl) { if (!hsUrl) {
@@ -250,7 +171,7 @@ export class AutoDiscovery {
} }
// Step 3: Make sure the homeserver URL points to a homeserver. // 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`, `${hsUrl}/_matrix/client/versions`,
); );
if (!hsVersions || !hsVersions.raw["versions"]) { if (!hsVersions || !hsVersions.raw["versions"]) {
@@ -272,7 +193,7 @@ export class AutoDiscovery {
}; };
// Step 5: Try to pull out the identity server configuration // Step 5: Try to pull out the identity server configuration
let isUrl = ""; let isUrl: string | boolean = "";
if (wellknown["m.identity_server"]) { if (wellknown["m.identity_server"]) {
// We prepare a failing identity server response to save lines later // We prepare a failing identity server response to save lines later
// in this branch. // in this branch.
@@ -287,7 +208,7 @@ export class AutoDiscovery {
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it // Step 5a: Make sure the URL is valid *looking*. We'll make sure it
// points to an identity server in Step 5b. // points to an identity server in Step 5b.
isUrl = this._sanitizeWellKnownUrl( isUrl = this.sanitizeWellKnownUrl(
wellknown["m.identity_server"]["base_url"], wellknown["m.identity_server"]["base_url"],
); );
if (!isUrl) { if (!isUrl) {
@@ -299,10 +220,10 @@ export class AutoDiscovery {
// Step 5b: Verify there is an identity server listening on the provided // Step 5b: Verify there is an identity server listening on the provided
// URL. // URL.
const isResponse = await this._fetchWellKnownObject( const isResponse = await this.fetchWellKnownObject(
`${isUrl}/_matrix/identity/api/v1`, `${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"); logger.error("Invalid /api/v1 response");
failingClientConfig["m.identity_server"].error = failingClientConfig["m.identity_server"].error =
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER; AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
@@ -317,7 +238,7 @@ export class AutoDiscovery {
// Step 6: Now that the identity server is valid, or never existed, // Step 6: Now that the identity server is valid, or never existed,
// populate the IS section. // populate the IS section.
if (isUrl && isUrl.length > 0) { if (isUrl && isUrl.toString().length > 0) {
clientConfig["m.identity_server"] = { clientConfig["m.identity_server"] = {
state: AutoDiscovery.SUCCESS, state: AutoDiscovery.SUCCESS,
error: null, error: null,
@@ -359,7 +280,7 @@ export class AutoDiscovery {
* configuration, which may include error states. Rejects on unexpected * configuration, which may include error states. Rejects on unexpected
* failure, not when discovery fails. * 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) { if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length"); 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 // Step 1: Actually request the .well-known JSON file and make sure it
// at least has a homeserver definition. // at least has a homeserver definition.
const wellknown = await this._fetchWellKnownObject( const wellknown = await this.fetchWellKnownObject(
`https://${domain}/.well-known/matrix/client`, `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"); logger.error("No response or error when parsing .well-known");
if (wellknown.reason) logger.error(wellknown.reason); if (wellknown.reason) logger.error(wellknown.reason);
if (wellknown.action === "IGNORE") { if (wellknown.action === AutoDiscoveryAction.IGNORE) {
clientConfig["m.homeserver"] = { clientConfig["m.homeserver"] = {
state: AutoDiscovery.PROMPT, state: AutoDiscovery.PROMPT,
error: null, error: null,
@@ -427,12 +348,12 @@ export class AutoDiscovery {
* @returns {Promise<object>} Resolves to the domain's client config. Can * @returns {Promise<object>} Resolves to the domain's client config. Can
* be an empty object. * be an empty object.
*/ */
static async getRawClientConfig(domain) { public static async getRawClientConfig(domain: string): Promise<IClientWellKnown> {
if (!domain || typeof(domain) !== "string" || domain.length === 0) { if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length"); 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`, `https://${domain}/.well-known/matrix/client`,
); );
if (!response) return {}; 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. * @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
* @private * @private
*/ */
static _sanitizeWellKnownUrl(url) { private static sanitizeWellKnownUrl(url: string): string | boolean {
if (!url) return false; if (!url) return false;
try { try {
@@ -495,8 +416,9 @@ export class AutoDiscovery {
* @return {Promise<object>} Resolves to the returned state. * @return {Promise<object>} Resolves to the returned state.
* @private * @private
*/ */
static async _fetchWellKnownObject(url) { private static async fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
// eslint-disable-next-line
const request = require("./matrix").getRequest(); const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available"); if (!request) throw new Error("No request library available");
request( request(
@@ -505,10 +427,10 @@ export class AutoDiscovery {
if (err || response && if (err || response &&
(response.statusCode < 200 || response.statusCode >= 300) (response.statusCode < 200 || response.statusCode >= 300)
) { ) {
let action = "FAIL_PROMPT"; let action = AutoDiscoveryAction.FAIL_PROMPT;
let reason = (err ? err.message : null) || "General failure"; let reason = (err ? err.message : null) || "General failure";
if (response && response.statusCode === 404) { if (response && response.statusCode === 404) {
action = "IGNORE"; action = AutoDiscoveryAction.IGNORE;
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN; reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
} }
resolve({ raw: {}, action: action, reason: reason, error: err }); resolve({ raw: {}, action: action, reason: reason, error: err });
@@ -516,7 +438,7 @@ export class AutoDiscovery {
} }
try { try {
resolve({ raw: JSON.parse(body), action: "SUCCESS" }); resolve({ raw: JSON.parse(body), action: AutoDiscoveryAction.SUCCESS });
} catch (e) { } catch (e) {
let reason = AutoDiscovery.ERROR_INVALID; let reason = AutoDiscovery.ERROR_INVALID;
if (e.name === "SyntaxError") { if (e.name === "SyntaxError") {
@@ -524,7 +446,7 @@ export class AutoDiscovery {
} }
resolve({ resolve({
raw: {}, raw: {},
action: "FAIL_PROMPT", action: AutoDiscoveryAction.FAIL_PROMPT,
reason: reason, reason: reason,
error: e, error: e,
}); });

View File

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

View File

@@ -101,6 +101,13 @@ interface IPayload extends Partial<IMessage> {
} }
/* eslint-enable camelcase */ /* 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 * @private
* @constructor * @constructor
@@ -115,12 +122,12 @@ interface IPayload extends Partial<IMessage> {
* *
* @property {object} sharedWithDevices * @property {object} sharedWithDevices
* devices with which we have shared the session key * devices with which we have shared the session key
* userId -> {deviceId -> msgindex} * userId -> {deviceId -> SharedWithData}
*/ */
class OutboundSessionInfo { class OutboundSessionInfo {
public useCount = 0; public useCount = 0;
public creationTime: number; public creationTime: number;
public sharedWithDevices: Record<string, Record<string, number>> = {}; public sharedWithDevices: Record<string, Record<string, SharedWithData>> = {};
public blockedDevicesNotified: Record<string, Record<string, boolean>> = {}; public blockedDevicesNotified: Record<string, Record<string, boolean>> = {};
constructor(public readonly sessionId: string, public readonly sharedHistory = false) { constructor(public readonly sessionId: string, public readonly sharedHistory = false) {
@@ -150,11 +157,11 @@ class OutboundSessionInfo {
return false; 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]) { if (!this.sharedWithDevices[userId]) {
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 { public markNotifiedBlockedDevice(userId: string, deviceId: string): void {
@@ -572,6 +579,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
payload: IPayload, payload: IPayload,
): Promise<void> { ): Promise<void> {
const contentMap = {}; const contentMap = {};
const deviceInfoByDeviceId = new Map<string, DeviceInfo>();
const promises = []; const promises = [];
for (let i = 0; i < userDeviceMap.length; i++) { for (let i = 0; i < userDeviceMap.length; i++) {
@@ -584,6 +592,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
const userId = val.userId; const userId = val.userId;
const deviceInfo = val.deviceInfo; const deviceInfo = val.deviceInfo;
const deviceId = deviceInfo.deviceId; const deviceId = deviceInfo.deviceId;
deviceInfoByDeviceId.set(deviceId, deviceInfo);
if (!contentMap[userId]) { if (!contentMap[userId]) {
contentMap[userId] = {}; contentMap[userId] = {};
@@ -636,7 +645,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
for (const userId of Object.keys(contentMap)) { for (const userId of Object.keys(contentMap)) {
for (const deviceId of Object.keys(contentMap[userId])) { for (const deviceId of Object.keys(contentMap[userId])) {
session.markSharedWithDevice( 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}`); logger.debug(`megolm session ${sessionId} never shared with user ${userId}`);
return; return;
} }
const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId]; const sessionSharedData = obSessionInfo.sharedWithDevices[userId][device.deviceId];
if (sentChainIndex === undefined) { if (sessionSharedData === undefined) {
logger.debug( logger.debug(
"megolm session ID " + sessionId + " never shared with device " + "megolm session ID " + sessionId + " never shared with device " +
userId + ":" + device.deviceId, userId + ":" + device.deviceId,
@@ -728,10 +740,18 @@ class MegolmEncryption extends EncryptionAlgorithm {
return; 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 // get the key from the inbound session: the outbound one will already
// have been ratcheted to the next chain index. // have been ratcheted to the next chain index.
const key = await this.olmDevice.getInboundGroupSessionKey( const key = await this.olmDevice.getInboundGroupSessionKey(
this.roomId, senderKey, sessionId, sentChainIndex, this.roomId, senderKey, sessionId, sessionSharedData.messageIndex,
); );
if (!key) { if (!key) {
@@ -882,7 +902,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
const deviceId = deviceInfo.deviceId; const deviceId = deviceInfo.deviceId;
session.markSharedWithDevice( 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 { export interface IRecoveryKey {
keyInfo: { keyInfo?: {
pubkey: string; pubkey: string;
passphrase?: { passphrase?: {
algorithm: string; algorithm: string;
@@ -67,7 +67,7 @@ export interface IRecoveryKey {
}; };
}; };
privateKey: Uint8Array; privateKey: Uint8Array;
encodedPrivateKey: string; encodedPrivateKey?: string;
} }
export interface ICreateSecretStorageOpts { 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, * recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed. * 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(); const decryption = new global.Olm.PkDecryption();
try { try {
const keyInfo: Partial<IRecoveryKey["keyInfo"]> = {}; const keyInfo: Partial<IRecoveryKey["keyInfo"]> = {};

View File

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

View File

@@ -160,6 +160,13 @@ export interface IGeneratedSas {
emoji?: EmojiMapping[]; emoji?: EmojiMapping[];
} }
export interface ISasEvent {
sas: IGeneratedSas;
confirm(): Promise<void>;
cancel(): void;
mismatch(): void;
}
function generateSas(sasBytes: number[], methods: string[]): IGeneratedSas { function generateSas(sasBytes: number[], methods: string[]): IGeneratedSas {
const sas: IGeneratedSas = {}; const sas: IGeneratedSas = {};
for (const method of methods) { for (const method of methods) {
@@ -232,12 +239,7 @@ export class SAS extends Base {
private waitingForAccept: boolean; private waitingForAccept: boolean;
public ourSASPubKey: string; public ourSASPubKey: string;
public theirSASPubKey: string; public theirSASPubKey: string;
public sasEvent: { public sasEvent: ISasEvent;
sas: IGeneratedSas;
confirm(): Promise<void>;
cancel(): void;
mismatch(): void;
};
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
public static get NAME(): string { 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 "./crypto/store/indexeddb-crypto-store";
export * from "./content-repo"; export * from "./content-repo";
export * as ContentHelpers from "./content-helpers"; export * as ContentHelpers from "./content-helpers";
export { export { createNewMatrixCall } from "./webrtc/call";
createNewMatrixCall,
setAudioInput as setMatrixCallAudioInput,
setVideoInput as setMatrixCallVideoInput,
CallType,
} from "./webrtc/call";
// expose the underlying request object so different environments can use // expose the underlying request object so different environments can use
// different request libs (e.g. request or browser-request) // different request libs (e.g. request or browser-request)

View File

@@ -77,6 +77,26 @@ export class MSC3089Branch {
}, this.id); }, 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. * Gets information about the file needed to download it.
* @returns {Promise<{info: IEncryptedFile, httpUrl: string}>} Information about the file. * @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; || 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 * Get the previous event content JSON. This will only return something for
* state events which exist in the timeline. * 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.name"
* @fires module:client~MatrixClient#event:"RoomMember.membership" * @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; const displayName = event.getDirectionalContent().displayname;
if (event.getType() !== "m.room.member") { if (event.getType() !== "m.room.member") {
@@ -322,7 +322,7 @@ export class RoomMember extends EventEmitter {
const MXID_PATTERN = /@.+:.+/; const MXID_PATTERN = /@.+:.+/;
const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/; 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; if (!displayName || displayName === selfUserId) return false;
// First check if the displayname is something we consider truthy // 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); this.handleRemoteEcho(event, existingEvent);
} }
} }
let thread = this.findEventById(event.parentEventId)?.getThread();
let thread = this.findEventById(event.replyEventId)?.getThread();
if (thread) { if (thread) {
thread.addEvent(event); thread.addEvent(event);
} else { } else {

View File

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

View File

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

View File

@@ -315,7 +315,19 @@ export class SyncApi {
public partitionThreadedEvents(events: MatrixEvent[]): [MatrixEvent[], MatrixEvent[]] { public partitionThreadedEvents(events: MatrixEvent[]): [MatrixEvent[], MatrixEvent[]] {
if (this.opts.experimentalThreadSupport) { if (this.opts.experimentalThreadSupport) {
return events.reduce((memo, event: MatrixEvent) => { 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; return memo;
}, [[], []]); }, [[], []]);
} else { } else {

View File

@@ -214,11 +214,6 @@ export enum CallErrorCode {
Transfered = 'transferred', Transfered = 'transferred',
} }
export enum ConstraintsType {
Audio = "audio",
Video = "video",
}
/** /**
* The version field that we set in m.call.* events * 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. */ /** The length of time a call can be ringing for. */
const CALL_TIMEOUT_MS = 60000; 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 { export class CallError extends Error {
code: string; code: string;
@@ -273,7 +253,6 @@ function genCallID(): string {
*/ */
export class MatrixCall extends EventEmitter { export class MatrixCall extends EventEmitter {
public roomId: string; public roomId: string;
public type: CallType = null;
public callId: string; public callId: string;
public invitee?: string; public invitee?: string;
public state = CallState.Fledgling; public state = CallState.Fledgling;
@@ -357,10 +336,7 @@ export class MatrixCall extends EventEmitter {
* @throws If you have not specified a listener for 'error' events. * @throws If you have not specified a listener for 'error' events.
*/ */
public async placeVoiceCall(): Promise<void> { public async placeVoiceCall(): Promise<void> {
logger.debug("placeVoiceCall"); await this.placeCall(true, false);
this.checkForErrorListener();
this.type = CallType.Voice;
await this.placeCallWithConstraints(ConstraintsType.Audio);
} }
/** /**
@@ -368,17 +344,7 @@ export class MatrixCall extends EventEmitter {
* @throws If you have not specified a listener for 'error' events. * @throws If you have not specified a listener for 'error' events.
*/ */
public async placeVideoCall(): Promise<void> { public async placeVideoCall(): Promise<void> {
logger.debug("placeVideoCall"); await this.placeCall(true, true);
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;
} }
public getOpponentMember(): RoomMember { public getOpponentMember(): RoomMember {
@@ -397,6 +363,25 @@ export class MatrixCall extends EventEmitter {
return this.remoteAssertedIdentity; 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 { public get localUsermediaFeed(): CallFeed {
return this.getLocalFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia); return this.getLocalFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia);
} }
@@ -660,8 +645,6 @@ export class MatrixCall extends EventEmitter {
return; return;
} }
this.type = remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice;
this.setState(CallState.Ringing); this.setState(CallState.Ringing);
if (event.getLocalAge()) { if (event.getLocalAge()) {
@@ -699,27 +682,17 @@ export class MatrixCall extends EventEmitter {
return; return;
} }
logger.debug(`Answering call ${this.callId} of type ${this.type}`); logger.debug(`Answering call ${this.callId}`);
if (!this.localUsermediaStream && !this.waitForLocalAVStream) { 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.setState(CallState.WaitLocalMedia);
this.waitForLocalAVStream = true; this.waitForLocalAVStream = true;
try { try {
let mediaStream: MediaStream; const mediaStream = await this.client.getMediaHandler().getUserMediaStream(
true,
if (this.type === CallType.Voice) { this.hasRemoteUserMediaVideoTrack,
mediaStream = await this.client.getLocalAudioStream(); );
} else {
mediaStream = await this.client.getLocalVideoStream();
}
this.waitForLocalAVStream = false; this.waitForLocalAVStream = false;
this.gotUserMediaForAnswer(mediaStream); this.gotUserMediaForAnswer(mediaStream);
} catch (e) { } catch (e) {
@@ -811,12 +784,11 @@ export class MatrixCall extends EventEmitter {
/** /**
* Starts/stops screensharing * Starts/stops screensharing
* @param enabled the desired screensharing state * @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 * @returns {boolean} new screensharing state
*/ */
public async setScreensharingEnabled( public async setScreensharingEnabled(
enabled: boolean, enabled: boolean, desktopCapturerSourceId?: string,
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
): Promise<boolean> { ): Promise<boolean> {
// Skip if there is nothing to do // Skip if there is nothing to do
if (enabled && this.isScreensharing()) { if (enabled && this.isScreensharing()) {
@@ -829,13 +801,13 @@ export class MatrixCall extends EventEmitter {
// Fallback to replaceTrack() // Fallback to replaceTrack()
if (!this.opponentSupportsSDPStreamMetadata()) { if (!this.opponentSupportsSDPStreamMetadata()) {
return await this.setScreensharingEnabledWithoutMetadataSupport(enabled, selectDesktopCapturerSource); return await this.setScreensharingEnabledWithoutMetadataSupport(enabled, desktopCapturerSourceId);
} }
logger.debug(`Set screensharing enabled? ${enabled}`); logger.debug(`Set screensharing enabled? ${enabled}`);
if (enabled) { if (enabled) {
try { try {
const stream = await getScreensharingStream(selectDesktopCapturerSource); const stream = await this.client.getMediaHandler().getScreensharingStream(desktopCapturerSourceId);
if (!stream) return false; if (!stream) return false;
this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare); this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare);
return true; return true;
@@ -861,17 +833,16 @@ export class MatrixCall extends EventEmitter {
* Starts/stops screensharing * Starts/stops screensharing
* Should be used ONLY if the opponent doesn't support SDPStreamMetadata * Should be used ONLY if the opponent doesn't support SDPStreamMetadata
* @param enabled the desired screensharing state * @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 * @returns {boolean} new screensharing state
*/ */
private async setScreensharingEnabledWithoutMetadataSupport( private async setScreensharingEnabledWithoutMetadataSupport(
enabled: boolean, enabled: boolean, desktopCapturerSourceId?: string,
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
): Promise<boolean> { ): Promise<boolean> {
logger.debug(`Set screensharing enabled? ${enabled} using replaceTrack()`); logger.debug(`Set screensharing enabled? ${enabled} using replaceTrack()`);
if (enabled) { if (enabled) {
try { try {
const stream = await getScreensharingStream(selectDesktopCapturerSource); const stream = await this.client.getMediaHandler().getScreensharingStream(desktopCapturerSourceId);
if (!stream) return false; if (!stream) return false;
const track = stream.getTracks().find((track) => { const track = stream.getTracks().find((track) => {
@@ -1041,7 +1012,7 @@ export class MatrixCall extends EventEmitter {
this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia); this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia);
this.setState(CallState.CreateOffer); this.setState(CallState.CreateOffer);
logger.debug("gotUserMediaForInvite -> " + this.type); logger.debug("gotUserMediaForInvite");
// Now we wait for the negotiationneeded event // 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 // XXX Find a better way to do this
this.client.callEventHandler.calls.set(this.callId, this); this.client.callEventHandler.calls.set(this.callId, this);
this.setState(CallState.WaitLocalMedia); this.setState(CallState.WaitLocalMedia);
@@ -1879,14 +1860,7 @@ export class MatrixCall extends EventEmitter {
this.peerConn = this.createPeerConnection(); this.peerConn = this.createPeerConnection();
try { try {
let mediaStream: MediaStream; const mediaStream = await this.client.getMediaHandler().getUserMediaStream(audio, video);
if (constraintsType === ConstraintsType.Audio) {
mediaStream = await this.client.getLocalAudioStream();
} else {
mediaStream = await this.client.getLocalVideoStream();
}
this.gotUserMediaForInvite(mediaStream); this.gotUserMediaForInvite(mediaStream);
} catch (e) { } catch (e) {
this.getUserMediaFailed(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 { function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean): void {
for (let i = 0; i < tracks.length; i++) { for (let i = 0; i < tracks.length; i++) {
tracks[i].enabled = enabled; 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 * DEPRECATED
* Use client.createCall() * 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