mirror of
https://github.com/matrix-org/mjolnir.git
synced 2024-10-01 01:36:06 -04:00
Retry requests in case of throttling (#178)
* Retry requests in case of throttling Co-authored-by: gnuxie <gnuxie@element.io>
This commit is contained in:
parent
baa9129fa5
commit
c7a96a3afe
@ -21,11 +21,9 @@ homeserver:
|
|||||||
# Make manual testing easier
|
# Make manual testing easier
|
||||||
enable_registration: true
|
enable_registration: true
|
||||||
|
|
||||||
# Getting rid of throttling.
|
# We remove rc_message so we can test rate limiting,
|
||||||
rc_message:
|
# but we keep the others because of https://github.com/matrix-org/synapse/issues/11785
|
||||||
per_second: 10000
|
# and we don't want to slow integration tests down.
|
||||||
burst_count: 10000
|
|
||||||
|
|
||||||
rc_registration:
|
rc_registration:
|
||||||
per_second: 10000
|
per_second: 10000
|
||||||
burst_count: 10000
|
burst_count: 10000
|
||||||
|
69
src/utils.ts
69
src/utils.ts
@ -313,6 +313,74 @@ function patchMatrixClientForConciseExceptions() {
|
|||||||
isMatrixClientPatchedForConciseExceptions = true;
|
isMatrixClientPatchedForConciseExceptions = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_REQUEST_ATTEMPTS = 15;
|
||||||
|
const REQUEST_RETRY_BASE_DURATION_MS = 100;
|
||||||
|
|
||||||
|
const TRACE_CONCURRENT_REQUESTS = false;
|
||||||
|
let numberOfConcurrentRequests = 0;
|
||||||
|
let isMatrixClientPatchedForRetryWhenThrottled = false;
|
||||||
|
/**
|
||||||
|
* Patch instances of MatrixClient to make sure that it retries requests
|
||||||
|
* in case of throttling.
|
||||||
|
*
|
||||||
|
* Note: As of this writing, we do not re-attempt requests that timeout,
|
||||||
|
* only request that are throttled by the server. The rationale is that,
|
||||||
|
* in case of DoS, we do not wish to make the situation even worse.
|
||||||
|
*/
|
||||||
|
function patchMatrixClientForRetry() {
|
||||||
|
if (isMatrixClientPatchedForRetryWhenThrottled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let originalRequestFn = getRequestFn();
|
||||||
|
setRequestFn(async (params, cb) => {
|
||||||
|
let attempt = 1;
|
||||||
|
numberOfConcurrentRequests += 1;
|
||||||
|
if (TRACE_CONCURRENT_REQUESTS) {
|
||||||
|
console.trace("Current number of concurrent requests", numberOfConcurrentRequests);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
let result: any[] = await new Promise((resolve, reject) => {
|
||||||
|
originalRequestFn(params, function requestFnWithRetry(err, response, resBody) {
|
||||||
|
// Note: There is no data race on `attempt` as we `await` before continuing
|
||||||
|
// to the next iteration of the loop.
|
||||||
|
if (attempt < MAX_REQUEST_ATTEMPTS && err?.body?.errcode === 'M_LIMIT_EXCEEDED') {
|
||||||
|
// We need to retry.
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
// No need-to-retry error? Lucky us!
|
||||||
|
// Note that this may very well be an error, just not
|
||||||
|
// one we need to retry.
|
||||||
|
resolve([err, response, resBody]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// This is our final result.
|
||||||
|
// Pass result, whether success or error.
|
||||||
|
return cb(...result);
|
||||||
|
} catch (err) {
|
||||||
|
// Need to retry.
|
||||||
|
let retryAfterMs = attempt * attempt * REQUEST_RETRY_BASE_DURATION_MS;
|
||||||
|
if ("retry_after_ms" in err) {
|
||||||
|
try {
|
||||||
|
retryAfterMs = Number.parseInt(err.retry_after_ms, 10);
|
||||||
|
} catch (ex) {
|
||||||
|
// Use default value.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogService.debug("Mjolnir.client", `Waiting ${retryAfterMs}ms before retrying ${params.method} ${params.uri}`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, retryAfterMs));
|
||||||
|
attempt += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
numberOfConcurrentRequests -= 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
isMatrixClientPatchedForRetryWhenThrottled = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform any patching deemed necessary to MatrixClient.
|
* Perform any patching deemed necessary to MatrixClient.
|
||||||
*/
|
*/
|
||||||
@ -324,4 +392,5 @@ export function patchMatrixClient() {
|
|||||||
// - `patchMatrixClientForRetry` expects that all errors are returned as
|
// - `patchMatrixClientForRetry` expects that all errors are returned as
|
||||||
// errors.
|
// errors.
|
||||||
patchMatrixClientForConciseExceptions();
|
patchMatrixClientForConciseExceptions();
|
||||||
|
patchMatrixClientForRetry();
|
||||||
}
|
}
|
||||||
|
@ -32,8 +32,8 @@ describe("Test: Reporting abuse", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create a few users and a room.
|
// Create a few users and a room.
|
||||||
let goodUser = await newTestUser(false, "reporting-abuse-good-user");
|
let goodUser = await newTestUser({ name: { contains: "reporting-abuse-good-user" }});
|
||||||
let badUser = await newTestUser(false, "reporting-abuse-bad-user");
|
let badUser = await newTestUser({ name: { contains: "reporting-abuse-bad-user" }});
|
||||||
let goodUserId = await goodUser.getUserId();
|
let goodUserId = await goodUser.getUserId();
|
||||||
let badUserId = await badUser.getUserId();
|
let badUserId = await badUser.getUserId();
|
||||||
|
|
||||||
@ -227,13 +227,13 @@ describe("Test: Reporting abuse", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create a moderator.
|
// Create a moderator.
|
||||||
let moderatorUser = await newTestUser(false, "reacting-abuse-moderator-user");
|
let moderatorUser = await newTestUser({ name: { contains: "reporting-abuse-moderator-user" }});
|
||||||
matrixClient().inviteUser(await moderatorUser.getUserId(), this.mjolnir.managementRoomId);
|
matrixClient().inviteUser(await moderatorUser.getUserId(), this.mjolnir.managementRoomId);
|
||||||
await moderatorUser.joinRoom(this.mjolnir.managementRoomId);
|
await moderatorUser.joinRoom(this.mjolnir.managementRoomId);
|
||||||
|
|
||||||
// Create a few users and a room.
|
// Create a few users and a room.
|
||||||
let goodUser = await newTestUser(false, "reacting-abuse-good-user");
|
let goodUser = await newTestUser({ name: { contains: "reacting-abuse-good-user" }});
|
||||||
let badUser = await newTestUser(false, "reacting-abuse-bad-user");
|
let badUser = await newTestUser({ name: { contains: "reacting-abuse-bad-user" }});
|
||||||
let goodUserId = await goodUser.getUserId();
|
let goodUserId = await goodUser.getUserId();
|
||||||
let badUserId = await badUser.getUserId();
|
let badUserId = await badUser.getUserId();
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ describe("Test: Updating the BanList", function () {
|
|||||||
it("Calculates what has changed correctly.", async function () {
|
it("Calculates what has changed correctly.", async function () {
|
||||||
this.timeout(10000);
|
this.timeout(10000);
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir = config.RUNTIME.client!
|
||||||
const moderator = await newTestUser(false, "moderator");
|
const moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||||
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]});
|
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]});
|
||||||
const banList = new BanList(banListId, banListId, mjolnir);
|
const banList = new BanList(banListId, banListId, mjolnir);
|
||||||
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
|
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
|
||||||
@ -117,7 +117,7 @@ describe("Test: Updating the BanList", function () {
|
|||||||
it("Will remove rules with old types when they are 'soft redacted' with a different but more recent event type.", async function () {
|
it("Will remove rules with old types when they are 'soft redacted' with a different but more recent event type.", async function () {
|
||||||
this.timeout(3000);
|
this.timeout(3000);
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir = config.RUNTIME.client!
|
||||||
const moderator = await newTestUser(false, "moderator");
|
const moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||||
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]});
|
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]});
|
||||||
const banList = new BanList(banListId, banListId, mjolnir);
|
const banList = new BanList(banListId, banListId, mjolnir);
|
||||||
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
|
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
|
||||||
@ -139,7 +139,7 @@ describe("Test: Updating the BanList", function () {
|
|||||||
it("A rule of the most recent type won't be deleted when an old rule is deleted for the same entity.", async function () {
|
it("A rule of the most recent type won't be deleted when an old rule is deleted for the same entity.", async function () {
|
||||||
this.timeout(3000);
|
this.timeout(3000);
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir = config.RUNTIME.client!
|
||||||
const moderator = await newTestUser(false, "moderator");
|
const moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||||
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]});
|
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]});
|
||||||
const banList = new BanList(banListId, banListId, mjolnir);
|
const banList = new BanList(banListId, banListId, mjolnir);
|
||||||
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
|
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
|
||||||
|
@ -18,12 +18,11 @@ const REGISTRATION_RETRY_BASE_DELAY_MS = 100;
|
|||||||
* @returns The response from synapse.
|
* @returns The response from synapse.
|
||||||
*/
|
*/
|
||||||
export async function registerUser(username: string, displayname: string, password: string, admin: boolean) {
|
export async function registerUser(username: string, displayname: string, password: string, admin: boolean) {
|
||||||
let registerUrl = `${config.homeserverUrl}/_synapse/admin/v1/register`
|
const registerUrl = `${config.homeserverUrl}/_synapse/admin/v1/register`;
|
||||||
let { data } = await axios.get(registerUrl);
|
|
||||||
let nonce = data.nonce!;
|
|
||||||
let mac = HmacSHA1(`${nonce}\0${username}\0${password}\0${admin ? 'admin' : 'notadmin'}`, 'REGISTRATION_SHARED_SECRET');
|
|
||||||
for (let i = 1; i <= REGISTRATION_ATTEMPTS; ++i) {
|
for (let i = 1; i <= REGISTRATION_ATTEMPTS; ++i) {
|
||||||
try {
|
try {
|
||||||
|
const { data: { nonce } } = await axios.get(registerUrl);
|
||||||
|
const mac = HmacSHA1(`${nonce}\0${username}\0${password}\0${admin ? 'admin' : 'notadmin'}`, 'REGISTRATION_SHARED_SECRET');
|
||||||
return await axios.post(registerUrl, {
|
return await axios.post(registerUrl, {
|
||||||
nonce,
|
nonce,
|
||||||
username,
|
username,
|
||||||
@ -45,47 +44,117 @@ export async function registerUser(username: string, displayname: string, passwo
|
|||||||
throw new Error(`Retried registration ${REGISTRATION_ATTEMPTS} times, is Mjolnir or Synapse misconfigured?`);
|
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 with a unique username.
|
* Register a new test user.
|
||||||
*
|
*
|
||||||
* @param isAdmin Whether to make the new user an admin.
|
* @returns A string that is both the username and password of a new user.
|
||||||
* @param label If specified, a string to place somewhere within the username.
|
|
||||||
* @returns A string that is the username and password of a new user.
|
|
||||||
*/
|
*/
|
||||||
export async function registerNewTestUser(isAdmin: boolean, label: string = "") {
|
async function registerNewTestUser(options: RegistrationOptions) {
|
||||||
let isUserValid = false;
|
|
||||||
let username;
|
|
||||||
if (label != "") {
|
|
||||||
label += "-";
|
|
||||||
}
|
|
||||||
do {
|
do {
|
||||||
username = `mjolnir-test-user-${label}${Math.floor(Math.random() * 100000)}`
|
let username;
|
||||||
await registerUser(username, username, username, isAdmin).then(_ => isUserValid = true).catch(e => {
|
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(username, username, username, options.isAdmin);
|
||||||
|
return username;
|
||||||
|
} catch (e) {
|
||||||
if (e.isAxiosError && e?.response?.data?.errcode === 'M_USER_IN_USE') {
|
if (e.isAxiosError && e?.response?.data?.errcode === 'M_USER_IN_USE') {
|
||||||
LogService.debug("test/clientHelper", `${username} already registered, trying another`);
|
if ("exact" in options.name) {
|
||||||
false // continue and try again
|
LogService.debug("test/clientHelper", `${username} already registered, reusing`);
|
||||||
|
return username;
|
||||||
|
} else {
|
||||||
|
LogService.debug("test/clientHelper", `${username} already registered, trying another`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(`failed to register user ${e}`);
|
console.error(`failed to register user ${e}`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
} while (!isUserValid);
|
} while (true);
|
||||||
return username;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a unique test user and returns a `MatrixClient` logged in and ready to use.
|
* Registers a test user and returns a `MatrixClient` logged in and ready to use.
|
||||||
*
|
*
|
||||||
* @param isAdmin Whether to make the user an admin.
|
|
||||||
* @param label If specified, a string to place somewhere within the username.
|
|
||||||
* @returns A new `MatrixClient` session for a unique test user.
|
* @returns A new `MatrixClient` session for a unique test user.
|
||||||
*/
|
*/
|
||||||
export async function newTestUser(isAdmin: boolean = false, label: string = ""): Promise<MatrixClient> {
|
export async function newTestUser(options: RegistrationOptions): Promise<MatrixClient> {
|
||||||
const username = await registerNewTestUser(isAdmin, label);
|
const username = await registerNewTestUser(options);
|
||||||
const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider());
|
const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider());
|
||||||
return await pantalaimon.createClientWithCredentials(username, username);
|
const client = await pantalaimon.createClientWithCredentials(username, username);
|
||||||
|
if (!options.isThrottled) {
|
||||||
|
let userId = await client.getUserId();
|
||||||
|
await overrideRatelimitForUser(userId);
|
||||||
|
}
|
||||||
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _globalAdminUser: MatrixClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a client that can perform synapse admin API actions.
|
||||||
|
* @returns A client logged in with an admin user.
|
||||||
|
*/
|
||||||
|
async function getGlobalAdminUser(): Promise<MatrixClient> {
|
||||||
|
// Initialize global admin user if needed.
|
||||||
|
if (!_globalAdminUser) {
|
||||||
|
const USERNAME = "mjolnir-test-internal-admin-user";
|
||||||
|
try {
|
||||||
|
await registerUser(USERNAME, USERNAME, USERNAME, true);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.isAxiosError && e?.response?.data?.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(config.homeserverUrl, 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(userId: string) {
|
||||||
|
await (await getGlobalAdminUser()).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(userId: string) {
|
||||||
|
await (await getGlobalAdminUser()).doRequest("DELETE", `/_synapse/admin/v1/users/${userId}/override_ratelimit`, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to create an event listener for m.notice msgtype m.room.messages.
|
* Utility to create an event listener for m.notice msgtype m.room.messages.
|
||||||
* @param targetRoomdId The roomId to listen into.
|
* @param targetRoomdId The roomId to listen into.
|
||||||
|
@ -10,11 +10,11 @@ import { getFirstReaction } from "./commandUtils";
|
|||||||
it('Mjölnir redacts all of the events sent by a spammer when instructed to by giving their id and a room id.', async function() {
|
it('Mjölnir redacts all of the events sent by a spammer when instructed to by giving their id and a room id.', async function() {
|
||||||
this.timeout(60000);
|
this.timeout(60000);
|
||||||
// Create a few users and a room.
|
// Create a few users and a room.
|
||||||
let badUser = await newTestUser(false, "spammer-needs-redacting");
|
let badUser = await newTestUser({ name: { contains: "spammer-needs-redacting" } });
|
||||||
let badUserId = await badUser.getUserId();
|
let badUserId = await badUser.getUserId();
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir = config.RUNTIME.client!
|
||||||
let mjolnirUserId = await mjolnir.getUserId();
|
let mjolnirUserId = await mjolnir.getUserId();
|
||||||
let moderator = await newTestUser(false, "moderator");
|
let moderator = await newTestUser({ name: { contains: "moderator" } });
|
||||||
this.moderator = moderator;
|
this.moderator = moderator;
|
||||||
await moderator.joinRoom(config.managementRoom);
|
await moderator.joinRoom(config.managementRoom);
|
||||||
let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]});
|
let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]});
|
||||||
@ -54,11 +54,11 @@ import { getFirstReaction } from "./commandUtils";
|
|||||||
it('Mjölnir redacts all of the events sent by a spammer when instructed to by giving their id in multiple rooms.', async function() {
|
it('Mjölnir redacts all of the events sent by a spammer when instructed to by giving their id in multiple rooms.', async function() {
|
||||||
this.timeout(60000);
|
this.timeout(60000);
|
||||||
// Create a few users and a room.
|
// Create a few users and a room.
|
||||||
let badUser = await newTestUser(false, "spammer-needs-redacting");
|
let badUser = await newTestUser({ name: { contains: "spammer-needs-redacting" } });
|
||||||
let badUserId = await badUser.getUserId();
|
let badUserId = await badUser.getUserId();
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir = config.RUNTIME.client!
|
||||||
let mjolnirUserId = await mjolnir.getUserId();
|
let mjolnirUserId = await mjolnir.getUserId();
|
||||||
let moderator = await newTestUser(false, "moderator");
|
let moderator = await newTestUser({ name: { contains: "moderator" } });
|
||||||
this.moderator = moderator;
|
this.moderator = moderator;
|
||||||
await moderator.joinRoom(config.managementRoom);
|
await moderator.joinRoom(config.managementRoom);
|
||||||
let targetRooms: string[] = [];
|
let targetRooms: string[] = [];
|
||||||
@ -103,10 +103,10 @@ import { getFirstReaction } from "./commandUtils";
|
|||||||
it("Redacts a single event when instructed to.", async function () {
|
it("Redacts a single event when instructed to.", async function () {
|
||||||
this.timeout(60000);
|
this.timeout(60000);
|
||||||
// Create a few users and a room.
|
// Create a few users and a room.
|
||||||
let badUser = await newTestUser(false, "spammer-needs-redacting");
|
let badUser = await newTestUser({ name: { contains: "spammer-needs-redacting" } });
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir = config.RUNTIME.client!
|
||||||
let mjolnirUserId = await mjolnir.getUserId();
|
let mjolnirUserId = await mjolnir.getUserId();
|
||||||
let moderator = await newTestUser(false, "moderator");
|
let moderator = await newTestUser({ name: { contains: "moderator" } });
|
||||||
this.moderator = moderator;
|
this.moderator = moderator;
|
||||||
await moderator.joinRoom(config.managementRoom);
|
await moderator.joinRoom(config.managementRoom);
|
||||||
let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]});
|
let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]});
|
||||||
|
@ -4,7 +4,7 @@ import { newTestUser, noticeListener } from "./clientHelper"
|
|||||||
describe("Test: !help command", function() {
|
describe("Test: !help command", function() {
|
||||||
let client;
|
let client;
|
||||||
this.beforeEach(async function () {
|
this.beforeEach(async function () {
|
||||||
client = await newTestUser(true);
|
client = await newTestUser({ name: { contains: "-" }});;
|
||||||
await client.start();
|
await client.start();
|
||||||
})
|
})
|
||||||
this.afterEach(async function () {
|
this.afterEach(async function () {
|
||||||
|
@ -23,7 +23,7 @@ import {
|
|||||||
} from "matrix-bot-sdk";
|
} from "matrix-bot-sdk";
|
||||||
import { Mjolnir} from '../../src/Mjolnir';
|
import { Mjolnir} from '../../src/Mjolnir';
|
||||||
import config from "../../src/config";
|
import config from "../../src/config";
|
||||||
import { registerUser } from "./clientHelper";
|
import { overrideRatelimitForUser, registerUser } from "./clientHelper";
|
||||||
import { patchMatrixClient } from "../../src/utils";
|
import { patchMatrixClient } from "../../src/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -82,6 +82,7 @@ export async function makeMjolnir(): Promise<Mjolnir> {
|
|||||||
LogService.info("test/mjolnirSetupUtils", "Starting bot...");
|
LogService.info("test/mjolnirSetupUtils", "Starting bot...");
|
||||||
const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider());
|
const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider());
|
||||||
const client = await pantalaimon.createClientWithCredentials(config.pantalaimon.username, config.pantalaimon.password);
|
const client = await pantalaimon.createClientWithCredentials(config.pantalaimon.username, config.pantalaimon.password);
|
||||||
|
await overrideRatelimitForUser(await client.getUserId());
|
||||||
patchMatrixClient();
|
patchMatrixClient();
|
||||||
await ensureAliasedRoomExists(client, config.managementRoom);
|
await ensureAliasedRoomExists(client, config.managementRoom);
|
||||||
let mjolnir = await Mjolnir.setupMjolnirFromConfig(client);
|
let mjolnir = await Mjolnir.setupMjolnirFromConfig(client);
|
||||||
|
74
test/integration/throttleTest.ts
Normal file
74
test/integration/throttleTest.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { strict as assert } from "assert";
|
||||||
|
import { newTestUser, overrideRatelimitForUser, resetRatelimitForUser } from "./clientHelper";
|
||||||
|
import { getMessagesByUserIn } from "../../src/utils";
|
||||||
|
import { getFirstReaction } from "./commands/commandUtils";
|
||||||
|
|
||||||
|
describe("Test: throttled users can function with Mjolnir.", function () {
|
||||||
|
it('throttled users survive being throttled by synapse', async function() {
|
||||||
|
this.timeout(60000);
|
||||||
|
let throttledUser = await newTestUser({ name: { contains: "throttled" }, isThrottled: true });
|
||||||
|
let throttledUserId = await throttledUser.getUserId();
|
||||||
|
let targetRoom = await throttledUser.createRoom();
|
||||||
|
// send enough messages to hit the rate limit.
|
||||||
|
await Promise.all([...Array(150).keys()].map((i) => throttledUser.sendMessage(targetRoom, {msgtype: 'm.text.', body: `Message #${i}`})));
|
||||||
|
let messageCount = 0;
|
||||||
|
await getMessagesByUserIn(throttledUser, throttledUserId, targetRoom, 150, (events) => {
|
||||||
|
messageCount += events.length;
|
||||||
|
});
|
||||||
|
assert.equal(messageCount, 150, "There should have been 150 messages in this room");
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Test: Mjolnir can still sync and respond to commands while throttled", function () {
|
||||||
|
beforeEach(async function() {
|
||||||
|
await resetRatelimitForUser(await this.mjolnir.client.getUserId())
|
||||||
|
})
|
||||||
|
afterEach(async function() {
|
||||||
|
await overrideRatelimitForUser(await this.mjolnir.client.getUserId());
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can still perform and respond to a redaction command', async function () {
|
||||||
|
this.timeout(60000);
|
||||||
|
// Create a few users and a room.
|
||||||
|
let badUser = await newTestUser({ name: { contains: "spammer-needs-redacting" } });
|
||||||
|
let badUserId = await badUser.getUserId();
|
||||||
|
const mjolnir = this.mjolnir.client;
|
||||||
|
let mjolnirUserId = await mjolnir.getUserId();
|
||||||
|
let moderator = await newTestUser({ name: { contains: "moderator" } });
|
||||||
|
this.moderator = moderator;
|
||||||
|
await moderator.joinRoom(this.mjolnir.managementRoomId);
|
||||||
|
let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]});
|
||||||
|
await moderator.setUserPowerLevel(mjolnirUserId, targetRoom, 100);
|
||||||
|
await badUser.joinRoom(targetRoom);
|
||||||
|
|
||||||
|
// Give Mjolnir some work to do and some messages to sync through.
|
||||||
|
await Promise.all([...Array(100).keys()].map((i) => moderator.sendMessage(this.mjolnir.managementRoomId, {msgtype: 'm.text.', body: `Irrelevant Message #${i}`})));
|
||||||
|
await Promise.all([...Array(50).keys()].map(_ => moderator.sendMessage(this.mjolnir.managementRoomId, {msgtype: 'm.text', body: '!mjolnir status'})));
|
||||||
|
|
||||||
|
await moderator.sendMessage(this.mjolnir.managementRoomId, {msgtype: 'm.text', body: `!mjolnir rooms add ${targetRoom}`});
|
||||||
|
|
||||||
|
await Promise.all([...Array(50).keys()].map((i) => badUser.sendMessage(targetRoom, {msgtype: 'm.text.', body: `Bad Message #${i}`})));
|
||||||
|
|
||||||
|
try {
|
||||||
|
moderator.start();
|
||||||
|
await getFirstReaction(moderator, this.mjolnir.managementRoomId, '✅', async () => {
|
||||||
|
return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir redact ${badUserId} ${targetRoom}` });
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
moderator.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
await getMessagesByUserIn(moderator, badUserId, targetRoom, 1000, function(events) {
|
||||||
|
count += events.length
|
||||||
|
events.map(e => {
|
||||||
|
if (e.type === 'm.room.member') {
|
||||||
|
assert.equal(Object.keys(e.content).length, 1, "Only membership should be left on the membership event when it has been redacted.")
|
||||||
|
} else if (Object.keys(e.content).length !== 0) {
|
||||||
|
throw new Error(`This event should have been redacted: ${JSON.stringify(e, null, 2)}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
assert.equal(count, 51, "There should be exactly 51 events from the spammer in this room.");
|
||||||
|
})
|
||||||
|
})
|
@ -10,9 +10,9 @@ describe("Test: timeline pagination", function () {
|
|||||||
it('does not paginate across the entire room history while backfilling.', async function() {
|
it('does not paginate across the entire room history while backfilling.', async function() {
|
||||||
this.timeout(60000);
|
this.timeout(60000);
|
||||||
// Create a few users and a room.
|
// Create a few users and a room.
|
||||||
let badUser = await newTestUser(false, "spammer");
|
let badUser = await newTestUser({ name: { contains: "spammer" }});
|
||||||
let badUserId = await badUser.getUserId();
|
let badUserId = await badUser.getUserId();
|
||||||
let moderator = await newTestUser(false, "moderator");
|
let moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||||
let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId()]});
|
let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId()]});
|
||||||
await badUser.joinRoom(targetRoom);
|
await badUser.joinRoom(targetRoom);
|
||||||
|
|
||||||
@ -39,9 +39,9 @@ describe("Test: timeline pagination", function () {
|
|||||||
})
|
})
|
||||||
it('does not call the callback with an empty array when there are no relevant events', async function() {
|
it('does not call the callback with an empty array when there are no relevant events', async function() {
|
||||||
this.timeout(60000);
|
this.timeout(60000);
|
||||||
let badUser = await newTestUser(false, "spammer");
|
let badUser = await newTestUser({ name: { contains: "spammer" }});
|
||||||
let badUserId = await badUser.getUserId();
|
let badUserId = await badUser.getUserId();
|
||||||
let moderator = await newTestUser(false, "moderator");
|
let moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||||
let targetRoom = await moderator.createRoom();
|
let targetRoom = await moderator.createRoom();
|
||||||
// send some irrelevant messages
|
// send some irrelevant messages
|
||||||
await Promise.all([...Array(200).keys()].map((i) => moderator.sendMessage(targetRoom, {msgtype: 'm.text.', body: `Irrelevant Message #${i}`})));
|
await Promise.all([...Array(200).keys()].map((i) => moderator.sendMessage(targetRoom, {msgtype: 'm.text.', body: `Irrelevant Message #${i}`})));
|
||||||
@ -54,9 +54,9 @@ describe("Test: timeline pagination", function () {
|
|||||||
})
|
})
|
||||||
it("The limit provided is respected", async function() {
|
it("The limit provided is respected", async function() {
|
||||||
this.timeout(60000);
|
this.timeout(60000);
|
||||||
let badUser = await newTestUser(false, "spammer");
|
let badUser = await newTestUser({ name: { contains: "spammer" }});
|
||||||
let badUserId = await badUser.getUserId();
|
let badUserId = await badUser.getUserId();
|
||||||
let moderator = await newTestUser(false, "moderator");
|
let moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||||
let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId()]});
|
let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId()]});
|
||||||
await badUser.joinRoom(targetRoom);
|
await badUser.joinRoom(targetRoom);
|
||||||
// send some bad person messages
|
// send some bad person messages
|
||||||
@ -83,7 +83,7 @@ describe("Test: timeline pagination", function () {
|
|||||||
});
|
});
|
||||||
it("Gives the events to the callback ordered by youngest first (even more important when the limit is reached halfway through a chunk).", async function() {
|
it("Gives the events to the callback ordered by youngest first (even more important when the limit is reached halfway through a chunk).", async function() {
|
||||||
this.timeout(60000);
|
this.timeout(60000);
|
||||||
let moderator = await newTestUser(false, "moderator");
|
let moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||||
let moderatorId = await moderator.getUserId();
|
let moderatorId = await moderator.getUserId();
|
||||||
let targetRoom = await moderator.createRoom();
|
let targetRoom = await moderator.createRoom();
|
||||||
for (let i = 0; i < 20; i++) {
|
for (let i = 0; i < 20; i++) {
|
||||||
|
Loading…
Reference in New Issue
Block a user