mirror of
https://github.com/Novaloop-AG/healthchat-web.git
synced 2026-11-04 10:32:44 +00:00
Absorb the matrix-react-sdk repository (#28192)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> Co-authored-by: github-merge-queue <github-merge-queue@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Florian Duros <florian.duros@ormaz.fr> Co-authored-by: Kim Brose <kim.brose@nordeck.net> Co-authored-by: Florian Duros <florianduros@element.io> Co-authored-by: R Midhun Suresh <hi@midhun.dev> Co-authored-by: dbkr <986903+dbkr@users.noreply.github.com> Co-authored-by: ElementRobot <releases@riot.im> Co-authored-by: dbkr <dbkr@users.noreply.github.com> Co-authored-by: David Baker <dbkr@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: David Langley <davidl@element.io> Co-authored-by: Michael Weimann <michaelw@matrix.org> Co-authored-by: Timshel <Timshel@users.noreply.github.com> Co-authored-by: Sahil Silare <32628578+sahil9001@users.noreply.github.com> Co-authored-by: Will Hunt <will@half-shot.uk> Co-authored-by: Hubert Chathi <hubert@uhoreg.ca> Co-authored-by: Andrew Ferrazzutti <andrewf@element.io> Co-authored-by: Robin <robin@robin.town> Co-authored-by: Tulir Asokan <tulir@maunium.net>
This commit is contained in:
committed by
GitHub
parent
2b99496025
commit
c05c429803
@@ -0,0 +1,504 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { JSHandle, Page } from "@playwright/test";
|
||||
import { PageFunctionOn } from "playwright-core/types/structs";
|
||||
|
||||
import { Network } from "./network";
|
||||
import type {
|
||||
IContent,
|
||||
ICreateRoomOpts,
|
||||
ISendEventResponse,
|
||||
MatrixClient,
|
||||
Room,
|
||||
MatrixEvent,
|
||||
ReceiptType,
|
||||
IRoomDirectoryOptions,
|
||||
KnockRoomOpts,
|
||||
Visibility,
|
||||
UploadOpts,
|
||||
Upload,
|
||||
StateEvents,
|
||||
TimelineEvents,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import type { RoomMessageEventContent } from "matrix-js-sdk/src/types";
|
||||
import { Credentials } from "../plugins/homeserver";
|
||||
|
||||
export class Client {
|
||||
public network: Network;
|
||||
protected client: JSHandle<MatrixClient>;
|
||||
|
||||
protected getClientHandle(): Promise<JSHandle<MatrixClient>> {
|
||||
return this.page.evaluateHandle(() => window.mxMatrixClientPeg.get());
|
||||
}
|
||||
|
||||
public async prepareClient(): Promise<JSHandle<MatrixClient>> {
|
||||
if (!this.client) {
|
||||
this.client = await this.getClientHandle();
|
||||
}
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public constructor(protected readonly page: Page) {
|
||||
page.on("framenavigated", async () => {
|
||||
this.client = null;
|
||||
});
|
||||
this.network = new Network(page, this);
|
||||
}
|
||||
|
||||
public evaluate<R, Arg, O extends MatrixClient = MatrixClient>(
|
||||
pageFunction: PageFunctionOn<O, Arg, R>,
|
||||
arg: Arg,
|
||||
): Promise<R>;
|
||||
public evaluate<R, O extends MatrixClient = MatrixClient>(
|
||||
pageFunction: PageFunctionOn<O, void, R>,
|
||||
arg?: any,
|
||||
): Promise<R>;
|
||||
public async evaluate<T>(fn: (client: MatrixClient) => T, arg?: any): Promise<T> {
|
||||
await this.prepareClient();
|
||||
return this.client.evaluate(fn, arg);
|
||||
}
|
||||
|
||||
public evaluateHandle<R, Arg, O extends MatrixClient = MatrixClient>(
|
||||
pageFunction: PageFunctionOn<O, Arg, R>,
|
||||
arg: Arg,
|
||||
): Promise<JSHandle<R>>;
|
||||
public evaluateHandle<R, O extends MatrixClient = MatrixClient>(
|
||||
pageFunction: PageFunctionOn<O, void, R>,
|
||||
arg?: any,
|
||||
): Promise<JSHandle<R>>;
|
||||
public async evaluateHandle<T>(fn: (client: MatrixClient) => T, arg?: any): Promise<JSHandle<T>> {
|
||||
await this.prepareClient();
|
||||
return this.client.evaluateHandle(fn, arg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param roomId ID of the room to send the event into
|
||||
* @param threadId ID of the thread to send into or null for main timeline
|
||||
* @param eventType type of event to send
|
||||
* @param content the event content to send
|
||||
*/
|
||||
public async sendEvent(
|
||||
roomId: string,
|
||||
threadId: string | null,
|
||||
eventType: string,
|
||||
content: IContent,
|
||||
): Promise<ISendEventResponse> {
|
||||
const client = await this.prepareClient();
|
||||
return client.evaluate(
|
||||
async (client, { roomId, threadId, eventType, content }) => {
|
||||
return client.sendEvent(
|
||||
roomId,
|
||||
threadId,
|
||||
eventType as keyof TimelineEvents,
|
||||
content as TimelineEvents[keyof TimelineEvents],
|
||||
);
|
||||
},
|
||||
{ roomId, threadId, eventType, content },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message into a room
|
||||
* @param roomId ID of the room to send the message into
|
||||
* @param content the event content to send
|
||||
* @param threadId optional thread id
|
||||
*/
|
||||
public async sendMessage(
|
||||
roomId: string,
|
||||
content: IContent | string,
|
||||
threadId: string | null = null,
|
||||
): Promise<ISendEventResponse> {
|
||||
if (typeof content === "string") {
|
||||
content = {
|
||||
msgtype: "m.text",
|
||||
body: content,
|
||||
};
|
||||
}
|
||||
|
||||
const client = await this.prepareClient();
|
||||
return client.evaluate(
|
||||
(client, { roomId, content, threadId }) => {
|
||||
return client.sendMessage(roomId, threadId, content as RoomMessageEventContent);
|
||||
},
|
||||
{
|
||||
roomId,
|
||||
content,
|
||||
threadId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async redactEvent(roomId: string, eventId: string, reason?: string): Promise<ISendEventResponse> {
|
||||
return this.evaluate(
|
||||
async (client, { roomId, eventId, reason }) => {
|
||||
return client.redactEvent(roomId, eventId, reason);
|
||||
},
|
||||
{ roomId, eventId, reason },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a room with given options.
|
||||
* @param options the options to apply when creating the room
|
||||
* @return the ID of the newly created room
|
||||
*/
|
||||
public async createRoom(options: ICreateRoomOpts): Promise<string> {
|
||||
const client = await this.prepareClient();
|
||||
return await client.evaluate(async (cli, options) => {
|
||||
const resp = await cli.createRoom(options);
|
||||
const roomId = resp.room_id;
|
||||
if (!cli.getRoom(roomId)) {
|
||||
await new Promise<void>((resolve) => {
|
||||
const onRoom = (room: Room) => {
|
||||
if (room.roomId === roomId) {
|
||||
cli.off(window.matrixcs.ClientEvent.Room, onRoom);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
cli.on(window.matrixcs.ClientEvent.Room, onRoom);
|
||||
});
|
||||
}
|
||||
return roomId;
|
||||
}, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a space with given options.
|
||||
* @param options the options to apply when creating the space
|
||||
* @return the ID of the newly created space (room)
|
||||
*/
|
||||
public async createSpace(options: ICreateRoomOpts): Promise<string> {
|
||||
return this.createRoom({
|
||||
...options,
|
||||
creation_content: {
|
||||
...options.creation_content,
|
||||
type: "m.space",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the given room by alias or ID
|
||||
* @param roomIdOrAlias the id or alias of the room to join
|
||||
*/
|
||||
public async joinRoom(roomIdOrAlias: string): Promise<void> {
|
||||
const client = await this.prepareClient();
|
||||
await client.evaluate(async (client, roomIdOrAlias) => {
|
||||
return await client.joinRoom(roomIdOrAlias);
|
||||
}, roomIdOrAlias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make this bot join a room by name
|
||||
* @param roomName Name of the room to join
|
||||
*/
|
||||
public async joinRoomByName(roomName: string): Promise<string> {
|
||||
const client = await this.prepareClient();
|
||||
return client.evaluate(
|
||||
async (client, { roomName }) => {
|
||||
const room = client.getRooms().find((r) => r.getDefaultRoomName(client.getUserId()) === roomName);
|
||||
if (room) {
|
||||
await client.joinRoom(room.roomId);
|
||||
return room.roomId;
|
||||
}
|
||||
throw new Error(`Bot room join failed. Cannot find room '${roomName}'`);
|
||||
},
|
||||
{
|
||||
roomName,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until next sync from this client
|
||||
*/
|
||||
public async waitForNextSync(): Promise<void> {
|
||||
await this.page.waitForResponse(async (response) => {
|
||||
const accessToken = await this.evaluate((client) => client.getAccessToken());
|
||||
const authHeader = await response.request().headerValue("authorization");
|
||||
return response.url().includes("/sync") && authHeader.includes(accessToken);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invites the given user to the given room.
|
||||
* @param roomId the id of the room to invite to
|
||||
* @param userId the id of the user to invite
|
||||
*/
|
||||
public async inviteUser(roomId: string, userId: string): Promise<void> {
|
||||
const client = await this.prepareClient();
|
||||
await client.evaluate((client, { roomId, userId }) => client.invite(roomId, userId), {
|
||||
roomId,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Knocks the given room.
|
||||
* @param roomId the id of the room to knock
|
||||
* @param opts the options to use when knocking
|
||||
*/
|
||||
public async knockRoom(roomId: string, opts?: KnockRoomOpts): Promise<void> {
|
||||
const client = await this.prepareClient();
|
||||
await client.evaluate((client, { roomId, opts }) => client.knockRoom(roomId, opts), { roomId, opts });
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks the given user from the given room.
|
||||
* @param roomId the id of the room to kick from
|
||||
* @param userId the id of the user to kick
|
||||
* @param reason the reason for the kick
|
||||
*/
|
||||
public async kick(roomId: string, userId: string, reason?: string): Promise<void> {
|
||||
const client = await this.prepareClient();
|
||||
await client.evaluate((client, { roomId, userId, reason }) => client.kick(roomId, userId, reason), {
|
||||
roomId,
|
||||
userId,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bans the given user from the given room.
|
||||
* @param roomId the id of the room to ban from
|
||||
* @param userId the id of the user to ban
|
||||
* @param reason the reason for the ban
|
||||
*/
|
||||
public async ban(roomId: string, userId: string, reason?: string): Promise<void> {
|
||||
const client = await this.prepareClient();
|
||||
await client.evaluate((client, { roomId, userId, reason }) => client.ban(roomId, userId, reason), {
|
||||
roomId,
|
||||
userId,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unban the given user from the given room.
|
||||
* @param roomId the id of the room to unban from
|
||||
* @param userId the id of the user to unban
|
||||
*/
|
||||
public async unban(roomId: string, userId: string): Promise<void> {
|
||||
const client = await this.prepareClient();
|
||||
await client.evaluate((client, { roomId, userId }) => client.unban(roomId, userId), { roomId, userId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the client to have specific membership of a given room
|
||||
*
|
||||
* This is often useful after joining a room, when we need to wait for the sync loop to catch up.
|
||||
*
|
||||
* Times out with an error after 1 second.
|
||||
*
|
||||
* @param roomId - ID of the room to check
|
||||
* @param membership - required membership.
|
||||
*/
|
||||
public async awaitRoomMembership(roomId: string, membership: string = "join") {
|
||||
await this.evaluate(
|
||||
(cli: MatrixClient, { roomId, membership }) => {
|
||||
const isReady = () => {
|
||||
// Fetch the room on each check, because we get a different instance before and after the join arrives.
|
||||
const room = cli.getRoom(roomId);
|
||||
const myMembership = room?.getMyMembership();
|
||||
// @ts-ignore access to private field "logger"
|
||||
cli.logger.info(`waiting for room ${roomId}: membership now ${myMembership}`);
|
||||
return myMembership === membership;
|
||||
};
|
||||
if (isReady()) return;
|
||||
|
||||
const timeoutPromise = new Promise((resolve) => setTimeout(resolve, 1000)).then(() => {
|
||||
const room = cli.getRoom(roomId);
|
||||
const myMembership = room?.getMyMembership();
|
||||
throw new Error(
|
||||
`Timeout waiting for room ${roomId} membership (now '${myMembership}', wanted '${membership}')`,
|
||||
);
|
||||
});
|
||||
|
||||
const readyPromise = new Promise<void>((resolve) => {
|
||||
async function onEvent() {
|
||||
if (isReady()) {
|
||||
cli.removeListener(window.matrixcs.ClientEvent.Event, onEvent);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
cli.on(window.matrixcs.ClientEvent.Event, onEvent);
|
||||
});
|
||||
|
||||
return Promise.race([timeoutPromise, readyPromise]);
|
||||
},
|
||||
{ roomId, membership },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MatrixEvent} event
|
||||
* @param {ReceiptType} receiptType
|
||||
* @param {boolean} unthreaded
|
||||
*/
|
||||
public async sendReadReceipt(
|
||||
event: JSHandle<MatrixEvent>,
|
||||
receiptType?: ReceiptType,
|
||||
unthreaded?: boolean,
|
||||
): Promise<{}> {
|
||||
const client = await this.prepareClient();
|
||||
return client.evaluate(
|
||||
(client, { event, receiptType, unthreaded }) => {
|
||||
return client.sendReadReceipt(event, receiptType, unthreaded);
|
||||
},
|
||||
{ event, receiptType, unthreaded },
|
||||
);
|
||||
}
|
||||
|
||||
public async publicRooms(options?: IRoomDirectoryOptions): ReturnType<MatrixClient["publicRooms"]> {
|
||||
const client = await this.prepareClient();
|
||||
return client.evaluate((client, options) => {
|
||||
return client.publicRooms(options);
|
||||
}, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {Promise} Resolves: {} an empty object.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
public async setDisplayName(name: string): Promise<{}> {
|
||||
const client = await this.prepareClient();
|
||||
return client.evaluate(async (cli: MatrixClient, name) => cli.setDisplayName(name), name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {Promise} Resolves: {} an empty object.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
public async setAvatarUrl(url: string): Promise<{}> {
|
||||
const client = await this.prepareClient();
|
||||
return client.evaluate(async (cli: MatrixClient, url) => cli.setAvatarUrl(url), url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to the media repository on the homeserver.
|
||||
*
|
||||
* @param {object} file The object to upload. On a browser, something that
|
||||
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
|
||||
* a Buffer, String or ReadStream.
|
||||
*/
|
||||
public async uploadContent(file: Buffer, opts?: UploadOpts): Promise<Awaited<Upload["promise"]>> {
|
||||
const client = await this.prepareClient();
|
||||
return client.evaluate(
|
||||
async (cli: MatrixClient, { file, opts }) => cli.uploadContent(new Uint8Array(file), opts),
|
||||
{
|
||||
file: [...file],
|
||||
opts,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstraps cross-signing.
|
||||
*/
|
||||
public async bootstrapCrossSigning(credentials: Credentials): Promise<void> {
|
||||
const client = await this.prepareClient();
|
||||
return bootstrapCrossSigningForClient(client, credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets account data for the user.
|
||||
* @param type The type of account data to set
|
||||
* @param content The content to set
|
||||
*/
|
||||
public async setAccountData(type: string, content: IContent): Promise<void> {
|
||||
const client = await this.prepareClient();
|
||||
return client.evaluate(
|
||||
async (client, { type, content }) => {
|
||||
await client.setAccountData(type, content);
|
||||
},
|
||||
{ type, content },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a state event into the room.
|
||||
* @param roomId ID of the room to send the event into
|
||||
* @param eventType type of event to send
|
||||
* @param content the event content to send
|
||||
* @param stateKey the state key to use
|
||||
*/
|
||||
public async sendStateEvent(
|
||||
roomId: string,
|
||||
eventType: string,
|
||||
content: IContent,
|
||||
stateKey?: string,
|
||||
): Promise<ISendEventResponse> {
|
||||
const client = await this.prepareClient();
|
||||
return client.evaluate(
|
||||
async (client, { roomId, eventType, content, stateKey }) => {
|
||||
return client.sendStateEvent(roomId, eventType as keyof StateEvents, content, stateKey);
|
||||
},
|
||||
{ roomId, eventType, content, stateKey },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Leaves the given room.
|
||||
* @param roomId ID of the room to leave
|
||||
*/
|
||||
public async leave(roomId: string): Promise<void> {
|
||||
const client = await this.prepareClient();
|
||||
return client.evaluate(async (client, roomId) => {
|
||||
await client.leave(roomId);
|
||||
}, roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the directory visibility for a room.
|
||||
* @param roomId ID of the room to set the directory visibility for
|
||||
* @param visibility The new visibility for the room
|
||||
*/
|
||||
public async setRoomDirectoryVisibility(roomId: string, visibility: Visibility): Promise<void> {
|
||||
const client = await this.prepareClient();
|
||||
return client.evaluate(
|
||||
async (client, { roomId, visibility }) => {
|
||||
await client.setRoomDirectoryVisibility(roomId, visibility);
|
||||
},
|
||||
{ roomId, visibility },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Call `CryptoApi.bootstrapCrossSigning` on the given Matrix client, using the given credentials to authenticate
|
||||
* the UIA request.
|
||||
*/
|
||||
export function bootstrapCrossSigningForClient(
|
||||
client: JSHandle<MatrixClient>,
|
||||
credentials: Credentials,
|
||||
resetKeys: boolean = false,
|
||||
) {
|
||||
return client.evaluate(
|
||||
async (client, { credentials, resetKeys }) => {
|
||||
await client.getCrypto().bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (func) => {
|
||||
await func({
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: credentials.userId,
|
||||
},
|
||||
password: credentials.password,
|
||||
});
|
||||
},
|
||||
setupNewCrossSigning: resetKeys,
|
||||
});
|
||||
},
|
||||
{ credentials, resetKeys },
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user