Files
element-call/src/utils/matrix.ts
T

348 lines
11 KiB
TypeScript
Raw Normal View History

2023-01-03 16:55:26 +00:00
/*
Copyright 2022-2024 New Vector Ltd.
2023-01-03 16:55:26 +00:00
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
2023-01-03 16:55:26 +00:00
*/
2022-05-30 10:09:13 +01:00
import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
import { MemoryStore } from "matrix-js-sdk/src/store/memory";
2024-05-02 17:28:36 +02:00
import {
createClient,
ICreateClientOpts,
Preset,
Visibility,
} from "matrix-js-sdk/src/matrix";
2022-05-30 11:28:16 +01:00
import { ClientEvent } from "matrix-js-sdk/src/client";
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
import { logger } from "matrix-js-sdk/src/logger";
2023-10-30 16:55:16 +00:00
import { secureRandomBase64Url } from "matrix-js-sdk/src/randomstring";
2022-05-30 10:09:13 +01:00
import type { MatrixClient } from "matrix-js-sdk/src/client";
2022-08-18 18:30:51 -04:00
import type { Room } from "matrix-js-sdk/src/models/room";
import IndexedDBWorker from "../IndexedDBWorker?worker";
import { generateUrlSearchParams, getUrlParams } from "../UrlParams";
import { Config } from "../config/Config";
import { E2eeType } from "../e2ee/e2eeType";
import { EncryptionSystem, saveKeyForRoom } from "../e2ee/sharedKeyManagement";
2022-01-05 17:19:03 -08:00
2022-10-26 13:58:41 +02:00
export const fallbackICEServerAllowed =
2022-10-26 14:27:41 +02:00
import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true";
2022-01-05 17:19:03 -08:00
const SYNC_STORE_NAME = "element-call-sync";
2024-05-23 01:00:17 +09:00
async function waitForSync(client: MatrixClient): Promise<void> {
// If there is a saved sync, the client will fire an additional sync event
// for restoring it before it runs the first network sync.
// However, the sync we want to wait for is the network sync,
// as the saved sync may be missing some state.
// Thus, don't resolve on the first sync when we know it's for the saved sync.
let waitForSavedSync = !!(await client.store.getSavedSyncToken());
2022-05-30 10:09:13 +01:00
return new Promise<void>((resolve, reject) => {
2022-05-30 11:28:16 +01:00
const onSync = (
state: SyncState,
2023-06-30 16:43:28 +01:00
_old: SyncState | null,
2023-10-11 10:42:04 -04:00
data?: ISyncStateData,
2023-09-22 18:05:13 -04:00
): void => {
2022-01-05 17:19:03 -08:00
if (state === "PREPARED") {
2024-05-23 01:00:17 +09:00
if (waitForSavedSync) {
waitForSavedSync = false;
} else {
client.removeListener(ClientEvent.Sync, onSync);
resolve();
}
2022-01-05 17:19:03 -08:00
} else if (state === "ERROR") {
2022-05-30 11:28:16 +01:00
client.removeListener(ClientEvent.Sync, onSync);
2023-06-09 19:18:30 +02:00
reject(data?.error);
2022-01-05 17:19:03 -08:00
}
};
2022-05-30 11:28:16 +01:00
client.on(ClientEvent.Sync, onSync);
2022-01-05 17:19:03 -08:00
});
}
/**
2022-07-15 14:34:50 -04:00
* Initialises and returns a new standalone Matrix Client.
* This can only be called safely if no other client is running
* otherwise rust crypto will throw since it is not ready to initialize a new session.
* If another client is running make sure `.logout()` is called before executing this function.
* @param clientOptions Object of options passed through to the client
* @param restore If the rust crypto should be reset before the cient initialization or
* if the initialization should try to restore the crypto state from the indexDB.
* @returns The MatrixClient instance
*/
2022-05-30 11:28:16 +01:00
export async function initClient(
clientOptions: ICreateClientOpts,
2023-10-11 10:42:04 -04:00
restore: boolean,
2022-05-30 11:28:16 +01:00
): Promise<MatrixClient> {
2023-06-30 16:43:28 +01:00
let indexedDB: IDBFactory | undefined;
2022-04-26 15:20:06 -07:00
try {
indexedDB = window.indexedDB;
} catch (e) {
logger.warn("Could not get indexDB from window.", e);
}
2022-04-26 15:20:06 -07:00
2022-12-21 18:01:58 +00:00
// options we always pass to the client (stuff that we need in order to work)
const baseOpts = {
fallbackICEServerAllowed: fallbackICEServerAllowed,
isVoipWithNoMediaAllowed:
Config.get().features?.feature_group_calls_without_video_and_audio,
2022-12-21 18:01:58 +00:00
} as ICreateClientOpts;
2022-04-26 15:20:06 -07:00
if (indexedDB && localStorage) {
2022-12-21 18:01:58 +00:00
baseOpts.store = new IndexedDBStore({
2022-04-26 15:20:06 -07:00
indexedDB: window.indexedDB,
2022-06-21 11:32:07 -04:00
localStorage,
dbName: SYNC_STORE_NAME,
// We can't use the worker in dev mode because Vite simply doesn't bundle workers
// in dev mode: it expects them to use native modules. Ours don't, and even then only
// Chrome supports it. (It bundles them fine in production mode.)
workerFactory: import.meta.env.DEV
? undefined
2023-09-22 18:05:13 -04:00
: (): Worker => new IndexedDBWorker(),
2022-04-26 15:20:06 -07:00
});
2022-06-21 11:32:07 -04:00
} else if (localStorage) {
2022-12-21 18:01:58 +00:00
baseOpts.store = new MemoryStore({ localStorage });
2022-04-26 15:20:06 -07:00
}
2022-10-10 09:19:10 -04:00
// XXX: we read from the URL params in RoomPage too:
// it would be much better to read them in one place and pass
// the values around, but we initialise the matrix client in
// many different places so we'd have to pass it into all of
// them.
2022-10-10 09:19:10 -04:00
const { e2eEnabled } = getUrlParams();
2022-07-27 16:14:05 -04:00
if (!e2eEnabled) {
logger.info("Disabling E2E: group call signalling will NOT be encrypted.");
}
2022-05-30 10:09:13 +01:00
const client = createClient({
2022-12-21 18:01:58 +00:00
...baseOpts,
2022-01-06 16:51:23 -08:00
...clientOptions,
useAuthorizationHeader: true,
2022-06-27 17:41:07 -04:00
// Use a relatively low timeout for API calls: this is a realtime app
2022-05-30 11:46:27 +01:00
// so we don't want API calls taking ages, we'd rather they just fail.
localTimeoutMs: 5000,
2022-07-27 16:14:05 -04:00
useE2eForGroupCall: e2eEnabled,
2022-10-26 13:58:41 +02:00
fallbackICEServerAllowed: fallbackICEServerAllowed,
2022-01-06 16:51:23 -08:00
});
2022-01-05 17:19:03 -08:00
// In case of logging in a new matrix account but there is still crypto local store. This is needed for:
// - We lost the auth tokens and cannot restore the client resulting in registering a new user.
// - We start the sign in flow but are registered with a guest user. (It should additionally log out the guest before)
// - A new account is created because of missing LocalStorage: "matrix-auth-store", but the crypto IndexDB is still available.
if (!restore) {
await client.clearStores();
2022-04-26 15:20:06 -07:00
}
// Start client store.
// Note: The `client.store` is used to store things like sync results. It's independent of
// the cryptostore, and uses a separate indexeddb database.
try {
await client.store.startup();
} catch (error) {
logger.error(
"Error starting matrix client indexDB store. Falling back to memory store.",
error,
);
client.store = new MemoryStore({ localStorage });
await client.store.startup();
}
// Also creates and starts any crypto related stores.
try {
await client.initRustCrypto();
} catch (err) {
logger.warn(
err,
"Make sure to clear client stores before initializing the rust crypto.",
);
}
2024-02-21 15:41:59 +01:00
client.setGlobalErrorOnUnknownDevices(false);
2024-05-23 01:00:17 +09:00
// Once startClient is called, syncs are run asynchronously.
// Also, sync completion is communicated only via events.
// So, apply the event listener *before* starting the client.
// Otherwise, a sync may complete before the listener gets applied,
// and we will miss it.
const syncPromise = waitForSync(client);
await client.startClient({ clientWellKnownPollPeriod: 60 * 10 });
2024-05-23 01:00:17 +09:00
await syncPromise;
2022-01-05 17:19:03 -08:00
return client;
}
2022-06-01 09:29:47 +01:00
export function roomAliasLocalpartFromRoomName(roomName: string): string {
2022-01-05 17:19:03 -08:00
return roomName
.trim()
.replace(/\s/g, "-")
.replace(/[^\w-]/g, "")
.toLowerCase();
}
function fullAliasFromRoomName(roomName: string, client: MatrixClient): string {
2022-06-01 09:29:47 +01:00
return `#${roomAliasLocalpartFromRoomName(roomName)}:${client.getDomain()}`;
}
/**
2023-02-28 14:09:52 +00:00
* Applies some basic sanitisation to a room name that the user
* has given us
* @param input The room name from the user
* @param client A matrix client object
*/
export function sanitiseRoomNameInput(input: string): string {
// check to see if the user has entered a fully qualified room
// alias. If so, turn it into just the localpart because that's what
// we use
const parts = input.split(":", 2);
if (parts.length === 2 && parts[0][0] === "#") {
// looks like a room alias
if (parts[1] === Config.defaultServerName()) {
// it's local to our own homeserver
return parts[0];
} else {
throw new Error("Unsupported remote room alias");
}
}
// that's all we do here right now
return input;
}
2023-10-05 16:44:31 +01:00
interface CreateRoomResult {
roomId: string;
alias?: string;
password?: string;
}
2023-10-23 12:10:25 +01:00
/**
* Create a new room ready for calls
*
* @param client Matrix client to use
* @param name The name of the room
* @param e2ee The type of e2ee call to create. Note that we would currently never
* create a room for per-participant e2ee calls: since it's used in
* embedded mode, we use the existing room.
* @returns Object holding information about the new room
*/
2022-05-30 11:28:16 +01:00
export async function createRoom(
client: MatrixClient,
2022-08-18 18:30:51 -04:00
name: string,
2023-10-23 12:10:25 +01:00
e2ee: E2eeType,
2023-10-05 16:44:31 +01:00
): Promise<CreateRoomResult> {
2022-11-10 21:43:40 +00:00
logger.log(`Creating room for group call`);
2022-08-18 18:30:51 -04:00
const createPromise = client.createRoom({
2022-05-30 10:09:13 +01:00
visibility: Visibility.Private,
preset: Preset.PublicChat,
2022-01-05 17:19:03 -08:00
name,
2023-08-11 13:16:35 +02:00
room_alias_name: e2ee ? undefined : roomAliasLocalpartFromRoomName(name),
2022-01-05 17:19:03 -08:00
power_level_content_override: {
invite: 100,
kick: 100,
ban: 100,
redact: 50,
state_default: 0,
events_default: 0,
users_default: 0,
events: {
"m.room.power_levels": 100,
"m.room.history_visibility": 100,
"m.room.tombstone": 100,
"m.room.encryption": 100,
"m.room.name": 50,
"m.room.message": 0,
"m.room.encrypted": 50,
"m.sticker": 50,
"org.matrix.msc3401.call.member": 0,
},
users: {
2023-06-30 16:43:28 +01:00
[client.getUserId()!]: 100,
2022-01-05 17:19:03 -08:00
},
},
});
2022-08-18 18:30:51 -04:00
// Wait for the room to arrive
const roomId = await new Promise<string>((resolve, reject) => {
2022-08-18 18:30:51 -04:00
createPromise.catch((e) => {
reject(e);
cleanUp();
});
const onRoom = (room: Room): void => {
createPromise.then(
(result) => {
if (room.roomId === result.room_id) {
resolve(room.roomId);
cleanUp();
}
},
(e) => {
logger.error("Failed to wait for the room to arrive", e);
},
);
};
2023-09-22 18:05:13 -04:00
const cleanUp = (): void => {
2022-08-18 18:30:51 -04:00
client.off(ClientEvent.Room, onRoom);
};
client.on(ClientEvent.Room, onRoom);
});
let password: string | undefined;
2023-10-23 12:10:25 +01:00
if (e2ee == E2eeType.SHARED_KEY) {
2023-10-30 16:55:16 +00:00
password = secureRandomBase64Url(16);
saveKeyForRoom(roomId, password);
2023-10-05 16:44:31 +01:00
}
return {
roomId,
2023-10-05 16:44:31 +01:00
alias: e2ee ? undefined : fullAliasFromRoomName(name, client),
password,
};
2022-01-05 17:19:03 -08:00
}
2023-08-11 13:16:35 +02:00
/**
* Returns an absolute URL to that will load Element Call with the given room
* @param roomId ID of the room
* @param roomName Name of the room
2024-04-23 15:15:13 +02:00
* @param encryptionSystem what encryption (or EncryptionSystem.Unencrypted) the room uses
*/
export function getAbsoluteRoomUrl(
roomId: string,
2024-04-23 15:15:13 +02:00
encryptionSystem: EncryptionSystem,
roomName?: string,
2024-04-23 15:15:13 +02:00
viaServers?: string[],
): string {
return `${window.location.protocol}//${
window.location.host
2024-04-23 15:15:13 +02:00
}${getRelativeRoomUrl(roomId, encryptionSystem, roomName, viaServers)}`;
}
/**
* Returns a relative URL to that will load Element Call with the given room
* @param roomId ID of the room
* @param roomName Name of the room
2024-04-23 15:15:13 +02:00
* @param encryptionSystem what encryption (or EncryptionSystem.Unencrypted) the room uses
2023-08-11 13:16:35 +02:00
*/
export function getRelativeRoomUrl(
2023-09-19 18:23:44 +01:00
roomId: string,
2024-04-23 15:15:13 +02:00
encryptionSystem: EncryptionSystem,
2023-09-19 18:23:44 +01:00
roomName?: string,
2024-04-23 15:15:13 +02:00
viaServers?: string[],
2023-09-19 18:23:44 +01:00
): string {
2024-04-23 15:15:13 +02:00
const roomPart = roomName
? "/" + roomAliasLocalpartFromRoomName(roomName)
: "";
return `/room/#${roomPart}?${generateUrlSearchParams(roomId, encryptionSystem, viaServers).toString()}`;
2022-01-05 17:19:03 -08:00
}
2022-05-30 11:28:16 +01:00
export function getAvatarUrl(
client: MatrixClient,
mxcUrl: string,
2023-10-11 10:42:04 -04:00
avatarSize = 96,
2022-05-30 11:28:16 +01:00
): string {
2022-01-05 17:19:03 -08:00
const width = Math.floor(avatarSize * window.devicePixelRatio);
const height = Math.floor(avatarSize * window.devicePixelRatio);
2022-11-02 23:12:43 -04:00
// scale is more suitable for larger sizes
const resizeMethod = avatarSize <= 96 ? "crop" : "scale";
return mxcUrl && client.mxcUrlToHttp(mxcUrl, width, height, resizeMethod)!;
2022-01-05 17:19:03 -08:00
}