mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-04-18 20:15:39 +00:00
* Migrate to eslint-9 strictTypeChecked & typescript 5. * Update to MPS 0.23.0. Required for strict type checks. * Looks like we found a test that was complete garbage, amazing really. * FIXUP * Well, the command handler was bugged previously... The command handler used to always only return the command without the prefix due to an operator precedence bug. This meant that when we made the order of operations explicit, we were now including the prefix of the command in the copy. So when we parsed arguments the code wasn't expecting the prefix to be there. * update to MPS 0.23.1. MPS 0.23.0 was bugged because we didn't enable `noUncheckedIndexedAccess` while upgrading to typescript 5. * Make sure eslint runs on all ts files. * eslint fixes. * enable `noUncheckedIndexedAccess` & `exactOptionalPropertyTypes`. * eslint ignores is clearly not understood by me. * Update SuperCoolStream for eslint and ts5. * stricter eslint done i thinks * Whoops, added on .only somewhere. * Update MPS. * fix broken test realted things. * Well I guess that part of getMessagesByUserIn was part of the interface. * Fix redactionCommandTest. * Account for escapeHTML in tests. * Fix tests. * stuff not matching with .editorconfig fixes. * Fix appservice webAPI test. * Update for MPS 0.23.3.
198 lines
8.1 KiB
TypeScript
198 lines
8.1 KiB
TypeScript
import { HmacSHA1 } from "crypto-js";
|
|
import { getRequestFn, LogService, MatrixClient, MemoryStorageProvider, PantalaimonClient } from "matrix-bot-sdk";
|
|
import "../../src/utils"; // we need this for the patches to matrix-bot-sdk's `getRequestFn`.
|
|
import { NoticeMessageContent, RoomMessage, Value } from "matrix-protection-suite";
|
|
|
|
const REGISTRATION_ATTEMPTS = 10;
|
|
const REGISTRATION_RETRY_BASE_DELAY_MS = 100;
|
|
|
|
/**
|
|
* Register a user using the synapse admin api that requires the use of a registration secret rather than an admin user.
|
|
* This should only be used by test code and should not be included from any file in the source directory
|
|
* either by explicit imports or copy pasting.
|
|
*
|
|
* @param username The username to give the user.
|
|
* @param displayname The displayname to give the user.
|
|
* @param password The password to use.
|
|
* @param admin True to make the user an admin, false otherwise.
|
|
* @returns The response from synapse.
|
|
*/
|
|
export async function registerUser(homeserver: string, username: string, displayname: string, password: string, admin: boolean): Promise<void> {
|
|
const registerUrl = `${homeserver}/_synapse/admin/v1/register`
|
|
const nonce: string = await new Promise((resolve, reject) => {
|
|
getRequestFn()({uri: registerUrl, method: "GET", timeout: 60000}, (error: unknown, _response: unknown, resBody: unknown) => {
|
|
if (error) {
|
|
if (error instanceof Error) {
|
|
reject(error);
|
|
} else {
|
|
throw new TypeError(`Something is throwing absoloute garbage`);
|
|
}
|
|
} else if (typeof resBody === 'object' && resBody !== null && 'nonce' in resBody && typeof resBody.nonce === 'string') {
|
|
resolve(resBody.nonce)
|
|
} else {
|
|
reject(new TypeError(`Don't know what to do with response body ${JSON.stringify(resBody)}`));
|
|
}
|
|
});
|
|
});
|
|
const mac = HmacSHA1(`${nonce}\0${username}\0${password}\0${admin ? 'admin' : 'notadmin'}`, 'REGISTRATION_SHARED_SECRET');
|
|
for (let i = 1; i <= REGISTRATION_ATTEMPTS; ++i) {
|
|
try {
|
|
const params = {
|
|
uri: registerUrl,
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify({
|
|
nonce,
|
|
username,
|
|
displayname,
|
|
password,
|
|
admin,
|
|
mac: mac.toString()
|
|
}),
|
|
timeout: 60000
|
|
}
|
|
await new Promise((resolve, reject) => {
|
|
getRequestFn()(params, (error: unknown) => { error !== undefined ? error instanceof Error ? reject(error) : resolve(new TypeError(`something is throwing garbage`)) : resolve(undefined); });
|
|
}); return;
|
|
} catch (ex) {
|
|
// In case of timeout or throttling, backoff and retry.
|
|
if (ex?.code === 'ESOCKETTIMEDOUT' || ex?.code === 'ETIMEDOUT'
|
|
|| ex?.body?.errcode === 'M_LIMIT_EXCEEDED') {
|
|
await new Promise(resolve => setTimeout(resolve, REGISTRATION_RETRY_BASE_DELAY_MS * i * i));
|
|
continue;
|
|
}
|
|
throw ex;
|
|
}
|
|
}
|
|
throw new Error(`Retried registration ${REGISTRATION_ATTEMPTS} times, is Mjolnir or Synapse misconfigured?`);
|
|
}
|
|
|
|
export type RegistrationOptions = {
|
|
/**
|
|
* If specified and true, make the user an admin.
|
|
*/
|
|
isAdmin?: boolean,
|
|
/**
|
|
* If `exact`, use the account with this exact name, attempting to reuse
|
|
* an existing account if possible.
|
|
*
|
|
* If `contains` create a new account with a name that contains this
|
|
* specific string.
|
|
*/
|
|
name: { exact: string } | { contains: string },
|
|
/**
|
|
* If specified and true, throttle this user.
|
|
*/
|
|
isThrottled?: boolean
|
|
}
|
|
|
|
/**
|
|
* Register a new test user.
|
|
*
|
|
* @returns A string that is both the username and password of a new user.
|
|
*/
|
|
async function registerNewTestUser(homeserver: string, options: RegistrationOptions) {
|
|
while (true) {
|
|
let username;
|
|
if ("exact" in options.name) {
|
|
username = options.name.exact;
|
|
} else {
|
|
username = `mjolnir-test-user-${options.name.contains}${Math.floor(Math.random() * 100000)}`
|
|
}
|
|
try {
|
|
await registerUser(homeserver, username, username, username, Boolean(options.isAdmin));
|
|
return username;
|
|
} catch (e) {
|
|
if (e?.body?.errcode === 'M_USER_IN_USE') {
|
|
if ("exact" in options.name) {
|
|
LogService.debug("test/clientHelper", `${username} already registered, reusing`);
|
|
return username;
|
|
} else {
|
|
LogService.debug("test/clientHelper", `${username} already registered, trying another`);
|
|
}
|
|
} else {
|
|
console.error(`failed to register user ${e}`);
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registers a test user and returns a `MatrixClient` logged in and ready to use.
|
|
*
|
|
* @returns A new `MatrixClient` session for a unique test user.
|
|
*/
|
|
export async function newTestUser(homeserver: string, options: RegistrationOptions): Promise<MatrixClient> {
|
|
const username = await registerNewTestUser(homeserver, options);
|
|
const pantalaimon = new PantalaimonClient(homeserver, new MemoryStorageProvider());
|
|
const client = await pantalaimon.createClientWithCredentials(username, username);
|
|
if (!options.isThrottled) {
|
|
const userId = await client.getUserId();
|
|
await overrideRatelimitForUser(homeserver, userId);
|
|
}
|
|
return client;
|
|
}
|
|
|
|
let _globalAdminUser: MatrixClient | undefined;
|
|
|
|
/**
|
|
* Get a client that can perform synapse admin API actions.
|
|
* @returns A client logged in with an admin user.
|
|
*/
|
|
async function getGlobalAdminUser(homeserver: string): Promise<MatrixClient> {
|
|
// Initialize global admin user if needed.
|
|
if (_globalAdminUser === undefined) {
|
|
const USERNAME = "mjolnir-test-internal-admin-user";
|
|
try {
|
|
await registerUser(homeserver, USERNAME, USERNAME, USERNAME, true);
|
|
} catch (e) {
|
|
if (e?.body?.errcode === 'M_USER_IN_USE') {
|
|
// Then we've already registered the user in a previous run and that is ok.
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
_globalAdminUser = await new PantalaimonClient(homeserver, new MemoryStorageProvider()).createClientWithCredentials(USERNAME, USERNAME);
|
|
}
|
|
return _globalAdminUser;
|
|
}
|
|
|
|
/**
|
|
* Disable ratelimiting for this user in Synapse.
|
|
* @param userId The user to disable ratelimiting for, has to include both the server part and local part.
|
|
*/
|
|
export async function overrideRatelimitForUser(homeserver: string, userId: string) {
|
|
await (await getGlobalAdminUser(homeserver)).doRequest("POST", `/_synapse/admin/v1/users/${userId}/override_ratelimit`, null, {
|
|
"messages_per_second": 0,
|
|
"burst_count": 0
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Put back the default ratelimiting for this user in Synapse.
|
|
* @param userId The user to use default ratelimiting for, has to include both the server part and local part.
|
|
*/
|
|
export async function resetRatelimitForUser(homeserver: string, userId: string) {
|
|
await (await getGlobalAdminUser(homeserver)).doRequest("DELETE", `/_synapse/admin/v1/users/${userId}/override_ratelimit`, null);
|
|
}
|
|
|
|
|
|
/**
|
|
* Utility to create an event listener for m.notice msgtype m.room.messages.
|
|
* @param targetRoomdId The roomId to listen into.
|
|
* @param cb The callback when a m.notice event is found in targetRoomId.
|
|
* @returns The callback to pass to `MatrixClient.on('room.message', cb)`
|
|
*/
|
|
export function noticeListener(targetRoomdId: string, cb: (event: Omit<RoomMessage, 'content'> & { content: NoticeMessageContent }) => void) {
|
|
return (roomId: string, event: unknown) => {
|
|
if (roomId !== targetRoomdId) {
|
|
return;
|
|
}
|
|
if (Value.Check(RoomMessage, event) && Value.Check(NoticeMessageContent, event.content)) {
|
|
cb(event as Omit<RoomMessage, 'content'> & { content: NoticeMessageContent });
|
|
return;
|
|
}
|
|
}
|
|
}
|