element-call-Github/src/matrix-utils.ts

318 lines
9.8 KiB
TypeScript
Raw Normal View History

2022-04-28 04:38:16 +08:00
import Olm from "@matrix-org/olm";
import olmWasmPath from "@matrix-org/olm/olm.wasm?url";
2022-05-30 17:09:13 +08:00
import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
import { MemoryStore } from "matrix-js-sdk/src/store/memory";
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
2022-06-21 23:32:07 +08:00
import { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-crypto-store";
import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto-store";
import { createClient } from "matrix-js-sdk/src/matrix";
2022-05-30 17:09:13 +08:00
import { ICreateClientOpts } from "matrix-js-sdk/src/matrix";
2022-05-30 18:28:16 +08:00
import { ClientEvent } from "matrix-js-sdk/src/client";
2022-05-30 18:41:59 +08:00
import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials";
2022-05-30 18:28:16 +08:00
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
import { logger } from "matrix-js-sdk/src/logger";
2022-08-19 06:30:51 +08:00
import {
GroupCallIntent,
GroupCallType,
} from "matrix-js-sdk/src/webrtc/groupCall";
2022-05-30 17:09:13 +08:00
import type { MatrixClient } from "matrix-js-sdk/src/client";
2022-08-19 06:30:51 +08:00
import type { Room } from "matrix-js-sdk/src/models/room";
2022-05-30 17:09:13 +08:00
import IndexedDBWorker from "./IndexedDBWorker?worker";
2022-10-10 21:19:10 +08:00
import { getUrlParams } from "./UrlParams";
2022-01-06 09:19:03 +08:00
export const defaultHomeserver =
2022-05-30 17:09:13 +08:00
(import.meta.env.VITE_DEFAULT_HOMESERVER as string) ??
2022-01-06 09:19:03 +08:00
`${window.location.protocol}//${window.location.host}`;
export const fallbackICEServerAllowed =
import.meta.env.VITE_FALLBACK_ICE_ALLOWED === "true";
2022-01-06 09:19:03 +08:00
export const defaultHomeserverHost = new URL(defaultHomeserver).host;
export class CryptoStoreIntegrityError extends Error {
constructor() {
super("Crypto store data was expected, but none was found");
}
}
const SYNC_STORE_NAME = "element-call-sync";
// Note that the crypto store name has changed from previous versions
// deliberately in order to force a logout for all users due to
// https://github.com/vector-im/element-call/issues/464
// (It's a good opportunity to make the database names consistent.)
const CRYPTO_STORE_NAME = "element-call-crypto";
2022-05-30 18:28:16 +08:00
function waitForSync(client: MatrixClient) {
2022-05-30 17:09:13 +08:00
return new Promise<void>((resolve, reject) => {
2022-05-30 18:28:16 +08:00
const onSync = (
state: SyncState,
_old: SyncState,
data: ISyncStateData
) => {
2022-01-06 09:19:03 +08:00
if (state === "PREPARED") {
resolve();
2022-05-30 18:28:16 +08:00
client.removeListener(ClientEvent.Sync, onSync);
2022-01-06 09:19:03 +08:00
} else if (state === "ERROR") {
reject(data?.error);
2022-05-30 18:28:16 +08:00
client.removeListener(ClientEvent.Sync, onSync);
2022-01-06 09:19:03 +08:00
}
};
2022-05-30 18:28:16 +08:00
client.on(ClientEvent.Sync, onSync);
2022-01-06 09:19:03 +08:00
});
}
/**
2022-07-16 02:34:50 +08:00
* Initialises and returns a new standalone Matrix Client.
* If true is passed for the 'restore' parameter, a check will be made
* to ensure that corresponding crypto data is stored and recovered.
* If the check fails, CryptoStoreIntegrityError will be thrown.
* @param clientOptions Object of options passed through to the client
* @param restore Whether the session is being restored from storage
* @returns The MatrixClient instance
*/
2022-05-30 18:28:16 +08:00
export async function initClient(
clientOptions: ICreateClientOpts,
restore: boolean
2022-05-30 18:28:16 +08:00
): Promise<MatrixClient> {
2022-04-28 04:51:08 +08:00
// TODO: https://gitlab.matrix.org/matrix-org/olm/-/issues/10
2022-04-28 04:38:16 +08:00
window.OLM_OPTIONS = {};
await Olm.init({ locateFile: () => olmWasmPath });
2022-04-27 07:28:21 +08:00
2022-06-01 16:07:00 +08:00
let indexedDB: IDBFactory;
try {
indexedDB = window.indexedDB;
} catch (e) {}
2022-05-30 17:09:13 +08:00
const storeOpts = {} as ICreateClientOpts;
if (indexedDB && localStorage && !import.meta.env.DEV) {
2022-05-30 17:09:13 +08:00
storeOpts.store = new IndexedDBStore({
indexedDB: window.indexedDB,
2022-06-21 23:32:07 +08:00
localStorage,
dbName: SYNC_STORE_NAME,
workerFactory: () => new IndexedDBWorker(),
});
2022-06-21 23:32:07 +08:00
} else if (localStorage) {
storeOpts.store = new MemoryStore({ localStorage });
}
// Check whether we have crypto data store. If we are restoring a session
// from storage then we will have started the crypto store and therefore
// have generated keys for that device, so if we can't recover those keys,
// we must not continue or we'll generate new keys and anyone who saw our
// previous keys will not accept our new key.
// It's worth mentioning here that if support for indexeddb or localstorage
// appears or disappears between sessions (it happens) then the failure mode
// here will be that we'll try a different store, not find crypto data and
// fail to restore the session. An alternative would be to continue using
// whatever we were using before, but that could be confusing since you could
// enable indexeddb and but the app would still not be using it.
if (restore) {
if (indexedDB) {
const cryptoStoreExists = await IndexedDBCryptoStore.exists(
indexedDB,
CRYPTO_STORE_NAME
);
if (!cryptoStoreExists) throw new CryptoStoreIntegrityError();
} else if (localStorage) {
if (!LocalStorageCryptoStore.exists(localStorage))
throw new CryptoStoreIntegrityError();
} else {
// if we get here then we're using the memory store, which cannot
// possibly have remembered a session, so it's an error.
throw new CryptoStoreIntegrityError();
}
}
if (indexedDB) {
2022-05-30 17:09:13 +08:00
storeOpts.cryptoStore = new IndexedDBCryptoStore(
indexedDB,
CRYPTO_STORE_NAME
);
2022-06-21 23:32:07 +08:00
} else if (localStorage) {
storeOpts.cryptoStore = new LocalStorageCryptoStore(localStorage);
} else {
storeOpts.cryptoStore = new MemoryCryptoStore();
}
2022-10-10 21:19:10 +08: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 21:19:10 +08:00
const { e2eEnabled } = getUrlParams();
if (!e2eEnabled) {
logger.info("Disabling E2E: group call signalling will NOT be encrypted.");
}
2022-05-30 17:09:13 +08:00
const client = createClient({
...storeOpts,
2022-01-07 08:51:23 +08:00
...clientOptions,
useAuthorizationHeader: true,
2022-06-28 05:41:07 +08:00
// Use a relatively low timeout for API calls: this is a realtime app
2022-05-30 18:46:27 +08:00
// so we don't want API calls taking ages, we'd rather they just fail.
localTimeoutMs: 5000,
useE2eForGroupCall: e2eEnabled,
fallbackICEServerAllowed: fallbackICEServerAllowed,
2022-01-07 08:51:23 +08:00
});
2022-01-06 09:19:03 +08:00
try {
await client.store.startup();
} catch (error) {
console.error(
"Error starting matrix client store. Falling back to memory store.",
error
);
2022-05-30 17:09:13 +08:00
client.store = new MemoryStore({ localStorage });
await client.store.startup();
}
if (client.initCrypto) {
await client.initCrypto();
}
2022-01-06 09:19:03 +08:00
await client.startClient({
// dirty hack to reduce chance of gappy syncs
// should be fixed by spotting gaps and backpaginating
initialSyncLimit: 50,
});
await waitForSync(client);
return client;
}
export function roomAliasLocalpartFromRoomName(roomName: string): string {
2022-01-06 09:19:03 +08:00
return roomName
.trim()
.replace(/\s/g, "-")
.replace(/[^\w-]/g, "")
.toLowerCase();
}
export function fullAliasFromRoomName(
roomName: string,
client: MatrixClient
): string {
return `#${roomAliasLocalpartFromRoomName(roomName)}:${client.getDomain()}`;
}
2022-05-30 18:28:16 +08:00
export function roomNameFromRoomId(roomId: string): string {
2022-02-15 05:53:19 +08:00
return roomId
.match(/([^:]+):.*$/)[1]
.substring(1)
.split("-")
.map((part) =>
part.length > 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part
)
.join(" ")
.toLowerCase();
2022-02-15 05:53:19 +08:00
}
2022-05-30 18:28:16 +08:00
export function isLocalRoomId(roomId: string): boolean {
2022-02-15 05:53:19 +08:00
if (!roomId) {
return false;
}
const parts = roomId.match(/[^:]+:(.*)$/);
if (parts.length < 2) {
return false;
}
return parts[1] === defaultHomeserverHost;
}
2022-05-30 18:28:16 +08:00
export async function createRoom(
client: MatrixClient,
2022-08-19 06:30:51 +08:00
name: string,
ptt: boolean
): Promise<[string, string]> {
2022-08-19 06:30:51 +08:00
const createPromise = client.createRoom({
2022-05-30 17:09:13 +08:00
visibility: Visibility.Private,
preset: Preset.PublicChat,
2022-01-06 09:19:03 +08:00
name,
room_alias_name: roomAliasLocalpartFromRoomName(name),
2022-01-06 09: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: {
[client.getUserId()]: 100,
},
},
});
2022-08-19 06:30:51 +08:00
// Wait for the room to arrive
await new Promise<void>((resolve, reject) => {
const onRoom = async (room: Room) => {
if (room.roomId === (await createPromise).room_id) {
resolve();
cleanUp();
}
};
createPromise.catch((e) => {
reject(e);
cleanUp();
});
const cleanUp = () => {
client.off(ClientEvent.Room, onRoom);
};
client.on(ClientEvent.Room, onRoom);
});
const result = await createPromise;
console.log(`Creating ${ptt ? "PTT" : "video"} group call room`);
await client.createGroupCall(
result.room_id,
ptt ? GroupCallType.Voice : GroupCallType.Video,
ptt,
GroupCallIntent.Room
);
return [fullAliasFromRoomName(name, client), result.room_id];
2022-01-06 09:19:03 +08:00
}
export function getRoomUrl(roomIdOrAlias: string): string {
if (roomIdOrAlias.startsWith("#")) {
const [localPart, host] = roomIdOrAlias.replace("#", "").split(":");
2022-01-06 09:19:03 +08:00
if (host !== defaultHomeserverHost) {
return `${window.location.protocol}//${window.location.host}/room/${roomIdOrAlias}`;
2022-01-06 09:19:03 +08:00
} else {
2022-02-17 02:52:07 +08:00
return `${window.location.protocol}//${window.location.host}/${localPart}`;
2022-01-06 09:19:03 +08:00
}
} else {
return `${window.location.protocol}//${window.location.host}/room/#?roomId=${roomIdOrAlias}`;
2022-01-06 09:19:03 +08:00
}
}
2022-05-30 18:28:16 +08:00
export function getAvatarUrl(
client: MatrixClient,
mxcUrl: string,
avatarSize = 96
): string {
2022-01-06 09:19:03 +08:00
const width = Math.floor(avatarSize * window.devicePixelRatio);
const height = Math.floor(avatarSize * window.devicePixelRatio);
return mxcUrl && client.mxcUrlToHttp(mxcUrl, width, height, "crop");
}