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";
|
2022-07-28 04:14:05 +08:00
|
|
|
import {
|
|
|
|
createClient,
|
|
|
|
createRoomWidgetClient,
|
|
|
|
MatrixClient,
|
|
|
|
} 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-06-28 05:41:07 +08:00
|
|
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
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";
|
2022-06-28 05:41:07 +08:00
|
|
|
import { WidgetApi } from "matrix-widget-api";
|
2022-07-05 03:10:13 +08:00
|
|
|
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
|
|
|
|
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-07-28 04:14:05 +08:00
|
|
|
import { getRoomParams } from "./room/useRoomParams";
|
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 defaultHomeserverHost = new URL(defaultHomeserver).host;
|
|
|
|
|
2022-07-14 20:07:30 +08:00
|
|
|
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 widget-API-based Matrix Client.
|
|
|
|
* @param widgetId The ID of the widget that the app is running inside.
|
|
|
|
* @param parentUrl The URL of the parent client.
|
|
|
|
* @returns The MatrixClient instance
|
|
|
|
*/
|
2022-06-28 05:41:07 +08:00
|
|
|
export async function initMatroskaClient(
|
2022-07-28 04:14:05 +08:00
|
|
|
widgetId: string,
|
|
|
|
parentUrl: string
|
2022-06-28 05:41:07 +08:00
|
|
|
): Promise<MatrixClient> {
|
|
|
|
// In this mode, we use a special client which routes all requests through
|
|
|
|
// the host application via the widget API
|
|
|
|
|
2022-07-28 04:14:05 +08:00
|
|
|
const { roomId, userId, deviceId } = getRoomParams();
|
|
|
|
if (!roomId) throw new Error("Room ID must be supplied");
|
|
|
|
if (!userId) throw new Error("User ID must be supplied");
|
|
|
|
if (!deviceId) throw new Error("Device ID must be supplied");
|
2022-06-28 05:41:07 +08:00
|
|
|
|
2022-08-09 21:43:12 +08:00
|
|
|
// These are all the event types the app uses
|
|
|
|
const sendState = [
|
|
|
|
{ eventType: EventType.GroupCallPrefix },
|
|
|
|
{ eventType: EventType.GroupCallMemberPrefix, stateKey: userId },
|
|
|
|
];
|
|
|
|
const receiveState = [
|
|
|
|
{ eventType: EventType.RoomMember },
|
|
|
|
{ eventType: EventType.GroupCallPrefix },
|
|
|
|
{ eventType: EventType.GroupCallMemberPrefix },
|
|
|
|
];
|
2022-07-29 04:26:14 +08:00
|
|
|
const sendRecvToDevice = [
|
|
|
|
EventType.CallInvite,
|
|
|
|
EventType.CallCandidates,
|
|
|
|
EventType.CallAnswer,
|
|
|
|
EventType.CallHangup,
|
|
|
|
EventType.CallReject,
|
|
|
|
EventType.CallSelectAnswer,
|
|
|
|
EventType.CallNegotiate,
|
|
|
|
EventType.CallSDPStreamMetadataChanged,
|
|
|
|
EventType.CallSDPStreamMetadataChangedPrefix,
|
|
|
|
EventType.CallReplaces,
|
|
|
|
"org.matrix.call_duplicate_session",
|
|
|
|
];
|
|
|
|
|
2022-06-28 05:41:07 +08:00
|
|
|
// Since all data should be coming from the host application, there's no
|
|
|
|
// need to persist anything, and therefore we can use the default stores
|
2022-07-28 04:14:05 +08:00
|
|
|
// We don't even need to set up crypto
|
2022-06-28 05:41:07 +08:00
|
|
|
const client = createRoomWidgetClient(
|
|
|
|
new WidgetApi(widgetId, new URL(parentUrl).origin),
|
|
|
|
{
|
2022-08-09 21:43:12 +08:00
|
|
|
sendState,
|
|
|
|
receiveState,
|
2022-07-29 04:26:14 +08:00
|
|
|
sendToDevice: sendRecvToDevice,
|
|
|
|
receiveToDevice: sendRecvToDevice,
|
2022-07-28 04:14:05 +08:00
|
|
|
turnServers: true,
|
2022-06-28 05:41:07 +08:00
|
|
|
},
|
|
|
|
roomId,
|
|
|
|
{
|
|
|
|
baseUrl: "",
|
2022-07-28 04:14:05 +08:00
|
|
|
userId,
|
|
|
|
deviceId,
|
2022-06-28 05:41:07 +08:00
|
|
|
timelineSupport: true,
|
2022-07-28 04:14:05 +08:00
|
|
|
}
|
2022-06-28 05:41:07 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
await client.startClient();
|
|
|
|
return client;
|
|
|
|
}
|
|
|
|
|
2022-07-14 20:07:30 +08:00
|
|
|
/**
|
2022-07-16 02:34:50 +08:00
|
|
|
* Initialises and returns a new standalone Matrix Client.
|
2022-07-14 20:07:30 +08:00
|
|
|
* 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(
|
2022-07-14 20:07:30 +08:00
|
|
|
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;
|
2022-04-27 06:20:06 +08:00
|
|
|
|
|
|
|
try {
|
|
|
|
indexedDB = window.indexedDB;
|
|
|
|
} catch (e) {}
|
|
|
|
|
2022-05-30 17:09:13 +08:00
|
|
|
const storeOpts = {} as ICreateClientOpts;
|
2022-04-27 06:20:06 +08:00
|
|
|
|
|
|
|
if (indexedDB && localStorage && !import.meta.env.DEV) {
|
2022-05-30 17:09:13 +08:00
|
|
|
storeOpts.store = new IndexedDBStore({
|
2022-04-27 06:20:06 +08:00
|
|
|
indexedDB: window.indexedDB,
|
2022-06-21 23:32:07 +08:00
|
|
|
localStorage,
|
2022-07-14 20:07:30 +08:00
|
|
|
dbName: SYNC_STORE_NAME,
|
2022-04-27 06:20:06 +08:00
|
|
|
workerFactory: () => new IndexedDBWorker(),
|
|
|
|
});
|
2022-06-21 23:32:07 +08:00
|
|
|
} else if (localStorage) {
|
|
|
|
storeOpts.store = new MemoryStore({ localStorage });
|
2022-04-27 06:20:06 +08:00
|
|
|
}
|
|
|
|
|
2022-07-14 20:07:30 +08:00
|
|
|
// 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.
|
2022-07-14 20:24:22 +08:00
|
|
|
// 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.
|
2022-07-14 20:07:30 +08:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-27 06:20:06 +08:00
|
|
|
if (indexedDB) {
|
2022-05-30 17:09:13 +08:00
|
|
|
storeOpts.cryptoStore = new IndexedDBCryptoStore(
|
2022-04-27 06:20:06 +08:00
|
|
|
indexedDB,
|
2022-07-14 20:07:30 +08:00
|
|
|
CRYPTO_STORE_NAME
|
2022-04-27 06:20:06 +08:00
|
|
|
);
|
2022-06-21 23:32:07 +08:00
|
|
|
} else if (localStorage) {
|
|
|
|
storeOpts.cryptoStore = new LocalStorageCryptoStore(localStorage);
|
|
|
|
} else {
|
|
|
|
storeOpts.cryptoStore = new MemoryCryptoStore();
|
2022-04-27 06:20:06 +08:00
|
|
|
}
|
|
|
|
|
2022-07-28 04:14:05 +08:00
|
|
|
// XXX: we read from the room params in RoomPage too:
|
2022-07-05 03:10:13 +08:00
|
|
|
// 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-07-28 04:14:05 +08:00
|
|
|
const { e2eEnabled } = getRoomParams();
|
|
|
|
if (!e2eEnabled) {
|
2022-07-05 03:10:13 +08:00
|
|
|
logger.info("Disabling E2E: group call signalling will NOT be encrypted.");
|
|
|
|
}
|
|
|
|
|
2022-05-30 17:09:13 +08:00
|
|
|
const client = createClient({
|
2022-04-27 06:20:06 +08:00
|
|
|
...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,
|
2022-07-28 04:14:05 +08:00
|
|
|
useE2eForGroupCall: e2eEnabled,
|
2022-01-07 08:51:23 +08:00
|
|
|
});
|
2022-01-06 09:19:03 +08:00
|
|
|
|
2022-04-27 06:20:06 +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 });
|
2022-04-27 06:20:06 +08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-06-01 16:29:47 +08:00
|
|
|
export function roomAliasLocalpartFromRoomName(roomName: string): string {
|
2022-01-06 09:19:03 +08:00
|
|
|
return roomName
|
|
|
|
.trim()
|
|
|
|
.replace(/\s/g, "-")
|
|
|
|
.replace(/[^\w-]/g, "")
|
|
|
|
.toLowerCase();
|
|
|
|
}
|
|
|
|
|
2022-06-01 16:29:47 +08:00
|
|
|
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("-")
|
2022-03-04 09:09:31 +08:00
|
|
|
.map((part) =>
|
|
|
|
part.length > 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part
|
|
|
|
)
|
2022-03-04 08:56:45 +08:00
|
|
|
.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
|
2022-07-20 23:01:29 +08:00
|
|
|
): 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,
|
2022-06-01 16:29:47 +08:00
|
|
|
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
|
|
|
|
);
|
|
|
|
|
2022-07-20 23:01:29 +08:00
|
|
|
return [fullAliasFromRoomName(name, client), result.room_id];
|
2022-01-06 09:19:03 +08:00
|
|
|
}
|
|
|
|
|
2022-07-28 04:14:05 +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) {
|
2022-07-28 04:14:05 +08:00
|
|
|
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 {
|
2022-07-28 04:14:05 +08:00
|
|
|
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");
|
|
|
|
}
|