You've already forked matrix-js-sdk
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:
19
CHANGELOG.md
19
CHANGELOG.md
@@ -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)
|
||||
===================================================================================================
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
27
src/@types/global.d.ts
vendored
27
src/@types/global.d.ts
vendored
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"]> = {};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
14
src/sync.ts
14
src/sync.ts
@@ -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 {
|
||||
|
||||
@@ -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
115
src/webrtc/mediaHandler.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user