mirror of
https://github.com/matrix-org/mjolnir.git
synced 2024-10-01 01:36:06 -04:00
Merge branch 'main' into gnuxie/why-are-they-banned
This commit is contained in:
commit
c581d0e2ff
32
.github/workflows/mjolnir.yml
vendored
32
.github/workflows/mjolnir.yml
vendored
@ -11,10 +11,38 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Integration tests
|
name: Build & Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Specifically use node 16 like in the readme.
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
- run: yarn install
|
||||||
|
- run: yarn build
|
||||||
|
- run: yarn lint
|
||||||
|
unit:
|
||||||
|
name: Unit tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Specifically use node 16 like in the readme.
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
- run: yarn install
|
||||||
|
- run: yarn test
|
||||||
|
integration:
|
||||||
|
name: Integration tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
- name: install mx-tester
|
- name: install mx-tester
|
||||||
run: cargo install mx-tester
|
run: cargo install mx-tester
|
||||||
- name: Setup image
|
- name: Setup image
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM node:14-alpine
|
FROM node:16-alpine
|
||||||
COPY . /tmp/src
|
COPY . /tmp/src
|
||||||
RUN cd /tmp/src \
|
RUN cd /tmp/src \
|
||||||
&& yarn install \
|
&& yarn install \
|
||||||
|
@ -235,3 +235,8 @@ web:
|
|||||||
abuseReporting:
|
abuseReporting:
|
||||||
# Whether to enable this feature.
|
# Whether to enable this feature.
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
# Whether or not to actively poll synapse for abuse reports, to be used
|
||||||
|
# instead of intercepting client calls to synapse's abuse endpoint, when that
|
||||||
|
# isn't possible/practical.
|
||||||
|
pollReports: false
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
To build mjolnir, you have to have installed `yarn` 1.x and Node 14.
|
To build mjolnir, you have to have installed `yarn` 1.x and Node 16.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/matrix-org/mjolnir.git
|
git clone https://github.com/matrix-org/mjolnir.git
|
||||||
@ -12,4 +12,4 @@ cp config/default.yaml config/development.yaml
|
|||||||
nano config/development.yaml
|
nano config/development.yaml
|
||||||
|
|
||||||
node lib/index.js
|
node lib/index.js
|
||||||
```
|
```
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
"lint": "tslint --project ./tsconfig.json -t stylish",
|
"lint": "tslint --project ./tsconfig.json -t stylish",
|
||||||
"start:dev": "yarn build && node --async-stack-traces lib/index.js",
|
"start:dev": "yarn build && node --async-stack-traces lib/index.js",
|
||||||
"test": "ts-mocha --project ./tsconfig.json test/commands/**/*.ts",
|
"test": "ts-mocha --project ./tsconfig.json test/commands/**/*.ts",
|
||||||
"test:integration": "NODE_ENV=harness ts-mocha --async-stack-traces --require test/integration/fixtures.ts --project ./tsconfig.json \"test/integration/**/*Test.ts\"",
|
"test:integration": "NODE_ENV=harness ts-mocha --async-stack-traces --require test/integration/fixtures.ts --timeout 300000 --project ./tsconfig.json \"test/integration/**/*Test.ts\"",
|
||||||
"test:manual": "NODE_ENV=harness ts-node test/integration/manualLaunchScript.ts",
|
"test:manual": "NODE_ENV=harness ts-node test/integration/manualLaunchScript.ts",
|
||||||
"version": "sed -i '/# version automated/s/[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*/'$npm_package_version'/' synapse_antispam/setup.py && git add synapse_antispam/setup.py && cat synapse_antispam/setup.py"
|
"version": "sed -i '/# version automated/s/[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*/'$npm_package_version'/' synapse_antispam/setup.py && git add synapse_antispam/setup.py && cat synapse_antispam/setup.py"
|
||||||
},
|
},
|
||||||
@ -48,6 +48,6 @@
|
|||||||
"shell-quote": "^1.7.3"
|
"shell-quote": "^1.7.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,7 @@ import { Healthz } from "./health/healthz";
|
|||||||
import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue";
|
import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue";
|
||||||
import { htmlEscape } from "./utils";
|
import { htmlEscape } from "./utils";
|
||||||
import { ReportManager } from "./report/ReportManager";
|
import { ReportManager } from "./report/ReportManager";
|
||||||
|
import { ReportPoller } from "./report/ReportPoller";
|
||||||
import { WebAPIs } from "./webapis/WebAPIs";
|
import { WebAPIs } from "./webapis/WebAPIs";
|
||||||
import { replaceRoomIdsWithPills } from "./utils";
|
import { replaceRoomIdsWithPills } from "./utils";
|
||||||
import RuleServer from "./models/RuleServer";
|
import RuleServer from "./models/RuleServer";
|
||||||
@ -67,6 +68,11 @@ const ENABLED_PROTECTIONS_EVENT_TYPE = "org.matrix.mjolnir.enabled_protections";
|
|||||||
const PROTECTED_ROOMS_EVENT_TYPE = "org.matrix.mjolnir.protected_rooms";
|
const PROTECTED_ROOMS_EVENT_TYPE = "org.matrix.mjolnir.protected_rooms";
|
||||||
const WARN_UNPROTECTED_ROOM_EVENT_PREFIX = "org.matrix.mjolnir.unprotected_room_warning.for.";
|
const WARN_UNPROTECTED_ROOM_EVENT_PREFIX = "org.matrix.mjolnir.unprotected_room_warning.for.";
|
||||||
const CONSEQUENCE_EVENT_DATA = "org.matrix.mjolnir.consequence";
|
const CONSEQUENCE_EVENT_DATA = "org.matrix.mjolnir.consequence";
|
||||||
|
/**
|
||||||
|
* Synapse will tell us where we last got to on polling reports, so we need
|
||||||
|
* to store that for pagination on further polls
|
||||||
|
*/
|
||||||
|
export const REPORT_POLL_EVENT_TYPE = "org.matrix.mjolnir.report_poll";
|
||||||
|
|
||||||
export class Mjolnir {
|
export class Mjolnir {
|
||||||
private displayName: string;
|
private displayName: string;
|
||||||
@ -97,7 +103,10 @@ export class Mjolnir {
|
|||||||
private webapis: WebAPIs;
|
private webapis: WebAPIs;
|
||||||
private protectedRoomActivityTracker: ProtectedRoomActivityTracker;
|
private protectedRoomActivityTracker: ProtectedRoomActivityTracker;
|
||||||
public taskQueue: ThrottlingQueue;
|
public taskQueue: ThrottlingQueue;
|
||||||
|
/*
|
||||||
|
* Config-enabled polling of reports in Synapse, so Mjolnir can react to reports
|
||||||
|
*/
|
||||||
|
private reportPoller?: ReportPoller;
|
||||||
/**
|
/**
|
||||||
* Adds a listener to the client that will automatically accept invitations.
|
* Adds a listener to the client that will automatically accept invitations.
|
||||||
* @param {MatrixClient} client
|
* @param {MatrixClient} client
|
||||||
@ -256,12 +265,13 @@ export class Mjolnir {
|
|||||||
// Setup Web APIs
|
// Setup Web APIs
|
||||||
console.log("Creating Web APIs");
|
console.log("Creating Web APIs");
|
||||||
const reportManager = new ReportManager(this);
|
const reportManager = new ReportManager(this);
|
||||||
reportManager.on("report.new", this.handleReport);
|
reportManager.on("report.new", this.handleReport.bind(this));
|
||||||
this.webapis = new WebAPIs(reportManager, this.ruleServer);
|
this.webapis = new WebAPIs(reportManager, this.ruleServer);
|
||||||
|
if (config.pollReports) {
|
||||||
|
this.reportPoller = new ReportPoller(this, reportManager);
|
||||||
|
}
|
||||||
// Setup join/leave listener
|
// Setup join/leave listener
|
||||||
this.roomJoins = new RoomMemberManager(this.client);
|
this.roomJoins = new RoomMemberManager(this.client);
|
||||||
|
|
||||||
this.taskQueue = new ThrottlingQueue(this, config.backgroundDelayMS);
|
this.taskQueue = new ThrottlingQueue(this, config.backgroundDelayMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,6 +312,20 @@ export class Mjolnir {
|
|||||||
console.log("Starting web server");
|
console.log("Starting web server");
|
||||||
await this.webapis.start();
|
await this.webapis.start();
|
||||||
|
|
||||||
|
if (this.reportPoller) {
|
||||||
|
let reportPollSetting: { from: number } = { from: 0 };
|
||||||
|
try {
|
||||||
|
reportPollSetting = await this.client.getAccountData(REPORT_POLL_EVENT_TYPE);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.body?.errcode !== "M_NOT_FOUND") {
|
||||||
|
throw err;
|
||||||
|
} else {
|
||||||
|
this.logMessage(LogLevel.INFO, "Mjolnir@startup", "report poll setting does not exist yet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.reportPoller.start(reportPollSetting.from);
|
||||||
|
}
|
||||||
|
|
||||||
// Load the state.
|
// Load the state.
|
||||||
this.currentState = STATE_CHECKING_PERMISSIONS;
|
this.currentState = STATE_CHECKING_PERMISSIONS;
|
||||||
|
|
||||||
@ -358,6 +382,7 @@ export class Mjolnir {
|
|||||||
LogService.info("Mjolnir", "Stopping Mjolnir...");
|
LogService.info("Mjolnir", "Stopping Mjolnir...");
|
||||||
this.client.stop();
|
this.client.stop();
|
||||||
this.webapis.stop();
|
this.webapis.stop();
|
||||||
|
this.reportPoller?.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async logMessage(level: LogLevel, module: string, message: string | any, additionalRoomIds: string[] | string | null = null, isRecursive = false): Promise<any> {
|
public async logMessage(level: LogLevel, module: string, message: string | any, additionalRoomIds: string[] | string | null = null, isRecursive = false): Promise<any> {
|
||||||
@ -1163,7 +1188,7 @@ export class Mjolnir {
|
|||||||
return await this.eventRedactionQueue.process(this, roomId);
|
return await this.eventRedactionQueue.process(this, roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleReport(roomId: string, reporterId: string, event: any, reason?: string) {
|
private async handleReport({ roomId, reporterId, event, reason }: { roomId: string, reporterId: string, event: any, reason?: string }) {
|
||||||
for (const protection of this.enabledProtections) {
|
for (const protection of this.enabledProtections) {
|
||||||
await protection.handleReport(this, roomId, reporterId, event, reason);
|
await protection.handleReport(this, roomId, reporterId, event, reason);
|
||||||
}
|
}
|
||||||
|
@ -133,7 +133,7 @@ export async function handleCommand(roomId: string, event: { content: { body: st
|
|||||||
"!mjolnir unban <list shortcode> <user|room|server> <glob> [apply] - Removes an entity from the ban list. If apply is 'true', the users matching the glob will actually be unbanned\n" +
|
"!mjolnir unban <list shortcode> <user|room|server> <glob> [apply] - Removes an entity from the ban list. If apply is 'true', the users matching the glob will actually be unbanned\n" +
|
||||||
"!mjolnir redact <user ID> [room alias/ID] [limit] - Redacts messages by the sender in the target room (or all rooms), up to a maximum number of events in the backlog (default 1000)\n" +
|
"!mjolnir redact <user ID> [room alias/ID] [limit] - Redacts messages by the sender in the target room (or all rooms), up to a maximum number of events in the backlog (default 1000)\n" +
|
||||||
"!mjolnir redact <event permalink> - Redacts a message by permalink\n" +
|
"!mjolnir redact <event permalink> - Redacts a message by permalink\n" +
|
||||||
"!mjolnir kick <user ID> [room alias/ID] [reason] - Kicks a user in a particular room or all protected rooms\n" +
|
"!mjolnir kick <glob> [room alias/ID] [reason] - Kicks a user or all of those matching a glob in a particular room or all protected rooms\n" +
|
||||||
"!mjolnir rules - Lists the rules currently in use by Mjolnir\n" +
|
"!mjolnir rules - Lists the rules currently in use by Mjolnir\n" +
|
||||||
"!mjolnir rules matching <user|room|server> - Lists the rules in use that will match this entity e.g. `!rules matching @foo:example.com` will show all the user and server rules, including globs, that match this user." +
|
"!mjolnir rules matching <user|room|server> - Lists the rules in use that will match this entity e.g. `!rules matching @foo:example.com` will show all the user and server rules, including globs, that match this user." +
|
||||||
"!mjolnir sync - Force updates of all lists and re-apply rules\n" +
|
"!mjolnir sync - Force updates of all lists and re-apply rules\n" +
|
||||||
|
@ -15,14 +15,31 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
import { LogLevel } from "matrix-bot-sdk";
|
import { LogLevel, MatrixGlob, RichReply } from "matrix-bot-sdk";
|
||||||
import config from "../config";
|
import config from "../config";
|
||||||
|
|
||||||
// !mjolnir kick <user|filter> [room] [reason]
|
// !mjolnir kick <user|filter> [room] [reason]
|
||||||
export async function execKickCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
export async function execKickCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
||||||
const userId = parts[2];
|
let force = false;
|
||||||
|
|
||||||
|
const glob = parts[2];
|
||||||
let rooms = [...Object.keys(mjolnir.protectedRooms)];
|
let rooms = [...Object.keys(mjolnir.protectedRooms)];
|
||||||
|
|
||||||
|
if (parts[parts.length - 1] === "--force") {
|
||||||
|
force = true;
|
||||||
|
parts.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.commands.confirmWildcardBan && /[*?]/.test(glob) && !force) {
|
||||||
|
let replyMessage = "Wildcard bans require an addition `--force` argument to confirm";
|
||||||
|
const reply = RichReply.createFor(roomId, event, replyMessage, replyMessage);
|
||||||
|
reply["msgtype"] = "m.notice";
|
||||||
|
await mjolnir.client.sendMessage(roomId, reply);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kickRule = new MatrixGlob(glob);
|
||||||
|
|
||||||
let reason: string | undefined;
|
let reason: string | undefined;
|
||||||
if (parts.length > 3) {
|
if (parts.length > 3) {
|
||||||
let reasonIndex = 3;
|
let reasonIndex = 3;
|
||||||
@ -32,19 +49,29 @@ export async function execKickCommand(roomId: string, event: any, mjolnir: Mjoln
|
|||||||
}
|
}
|
||||||
reason = parts.slice(reasonIndex).join(' ') || '<no reason supplied>';
|
reason = parts.slice(reasonIndex).join(' ') || '<no reason supplied>';
|
||||||
}
|
}
|
||||||
if (!reason) reason = "<none supplied>";
|
if (!reason) reason = '<none supplied>';
|
||||||
|
|
||||||
for (const targetRoomId of rooms) {
|
for (const protectedRoomId of rooms) {
|
||||||
const joinedUsers = await mjolnir.client.getJoinedRoomMembers(targetRoomId);
|
const members = await mjolnir.client.getRoomMembers(protectedRoomId, undefined, ["join"], ["ban", "leave"]);
|
||||||
if (!joinedUsers.includes(userId)) continue; // skip
|
|
||||||
|
|
||||||
await mjolnir.logMessage(LogLevel.INFO, "KickCommand", `Kicking ${userId} in ${targetRoomId} for ${reason}`, targetRoomId);
|
for (const member of members) {
|
||||||
if (!config.noop) {
|
const victim = member.membershipFor;
|
||||||
await mjolnir.taskQueue.push(async () => {
|
|
||||||
return mjolnir.client.kickUser(userId, targetRoomId, reason);
|
if (kickRule.test(victim)) {
|
||||||
});
|
await mjolnir.logMessage(LogLevel.DEBUG, "KickCommand", `Removing ${victim} in ${protectedRoomId}`, protectedRoomId);
|
||||||
} else {
|
|
||||||
await mjolnir.logMessage(LogLevel.WARN, "KickCommand", `Tried to kick ${userId} in ${targetRoomId} but the bot is running in no-op mode.`, targetRoomId);
|
if (!config.noop) {
|
||||||
|
try {
|
||||||
|
await mjolnir.taskQueue.push(async () => {
|
||||||
|
return mjolnir.client.kickUser(victim, protectedRoomId, reason);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
await mjolnir.logMessage(LogLevel.WARN, "KickCommand", `An error happened while trying to kick ${victim}: ${e}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await mjolnir.logMessage(LogLevel.WARN, "KickCommand", `Tried to kick ${victim} in ${protectedRoomId} but the bot is running in no-op mode.`, protectedRoomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +53,7 @@ interface IConfig {
|
|||||||
* of one background task and the start of the next one.
|
* of one background task and the start of the next one.
|
||||||
*/
|
*/
|
||||||
backgroundDelayMS: number;
|
backgroundDelayMS: number;
|
||||||
|
pollReports: boolean;
|
||||||
admin?: {
|
admin?: {
|
||||||
enableMakeRoomAdminCommand?: boolean;
|
enableMakeRoomAdminCommand?: boolean;
|
||||||
}
|
}
|
||||||
@ -122,6 +123,7 @@ const defaultConfig: IConfig = {
|
|||||||
automaticallyRedactForReasons: ["spam", "advertising"],
|
automaticallyRedactForReasons: ["spam", "advertising"],
|
||||||
protectAllJoinedRooms: false,
|
protectAllJoinedRooms: false,
|
||||||
backgroundDelayMS: 500,
|
backgroundDelayMS: 500,
|
||||||
|
pollReports: false,
|
||||||
commands: {
|
commands: {
|
||||||
allowNoPrefix: false,
|
allowNoPrefix: false,
|
||||||
additionalPrefixes: [],
|
additionalPrefixes: [],
|
||||||
|
145
src/report/ReportPoller.ts
Normal file
145
src/report/ReportPoller.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Mjolnir, REPORT_POLL_EVENT_TYPE } from "../Mjolnir";
|
||||||
|
import { ReportManager } from './ReportManager';
|
||||||
|
import { LogLevel } from "matrix-bot-sdk";
|
||||||
|
|
||||||
|
class InvalidStateError extends Error {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to poll synapse's report endpoint, so we can act on new reports
|
||||||
|
*
|
||||||
|
* @param mjolnir The running Mjolnir instance
|
||||||
|
* @param manager The report manager in to which we feed new reports
|
||||||
|
*/
|
||||||
|
export class ReportPoller {
|
||||||
|
/**
|
||||||
|
* https://matrix-org.github.io/synapse/latest/admin_api/event_reports.html
|
||||||
|
* "from" is an opaque token that is returned from the API to paginate reports
|
||||||
|
*/
|
||||||
|
private from = 0;
|
||||||
|
/**
|
||||||
|
* The currently-pending report poll
|
||||||
|
*/
|
||||||
|
private timeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private mjolnir: Mjolnir,
|
||||||
|
private manager: ReportManager,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
private schedulePoll() {
|
||||||
|
if (this.timeout === null) {
|
||||||
|
/*
|
||||||
|
* Important that we use `setTimeout` here, not `setInterval`,
|
||||||
|
* because if there's networking problems and `getAbuseReports`
|
||||||
|
* hangs for longer thank the interval, it could cause a stampede
|
||||||
|
* of requests when networking problems resolve
|
||||||
|
*/
|
||||||
|
this.timeout = setTimeout(
|
||||||
|
this.tryGetAbuseReports.bind(this),
|
||||||
|
30_000 // a minute in milliseconds
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new InvalidStateError("poll already scheduled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAbuseReports() {
|
||||||
|
let response_: {
|
||||||
|
event_reports: { room_id: string, event_id: string, sender: string, reason: string }[],
|
||||||
|
next_token: number | undefined
|
||||||
|
} | undefined;
|
||||||
|
try {
|
||||||
|
response_ = await this.mjolnir.client.doRequest(
|
||||||
|
"GET",
|
||||||
|
"/_synapse/admin/v1/event_reports",
|
||||||
|
{ from: this.from.toString() }
|
||||||
|
);
|
||||||
|
} catch (ex) {
|
||||||
|
await this.mjolnir.logMessage(LogLevel.ERROR, "getAbuseReports", `failed to poll events: ${ex}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = response_!;
|
||||||
|
for (let report of response.event_reports) {
|
||||||
|
if (!(report.room_id in this.mjolnir.protectedRooms)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let event: any; // `any` because `handleServerAbuseReport` uses `any`
|
||||||
|
try {
|
||||||
|
event = (await this.mjolnir.client.doRequest(
|
||||||
|
"GET",
|
||||||
|
`/_synapse/admin/v1/rooms/${report.room_id}/context/${report.event_id}?limit=1`
|
||||||
|
)).event;
|
||||||
|
} catch (ex) {
|
||||||
|
this.mjolnir.logMessage(LogLevel.ERROR, "getAbuseReports", `failed to get context: ${ex}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.manager.handleServerAbuseReport({
|
||||||
|
roomId: report.room_id,
|
||||||
|
reporterId: report.sender,
|
||||||
|
event: event,
|
||||||
|
reason: report.reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This API endpoint returns an opaque `next_token` number that we
|
||||||
|
* need to give back to subsequent requests for pagination, so here we
|
||||||
|
* save it in account data
|
||||||
|
*/
|
||||||
|
if (response.next_token !== undefined) {
|
||||||
|
this.from = response.next_token;
|
||||||
|
try {
|
||||||
|
await this.mjolnir.client.setAccountData(REPORT_POLL_EVENT_TYPE, { from: response.next_token });
|
||||||
|
} catch (ex) {
|
||||||
|
await this.mjolnir.logMessage(LogLevel.ERROR, "getAbuseReports", `failed to update progress: ${ex}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async tryGetAbuseReports() {
|
||||||
|
this.timeout = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.getAbuseReports()
|
||||||
|
} catch (ex) {
|
||||||
|
await this.mjolnir.logMessage(LogLevel.ERROR, "tryGetAbuseReports", `failed to get abuse reports: ${ex}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.schedulePoll();
|
||||||
|
}
|
||||||
|
public start(startFrom: number) {
|
||||||
|
if (this.timeout === null) {
|
||||||
|
this.from = startFrom;
|
||||||
|
this.schedulePoll();
|
||||||
|
} else {
|
||||||
|
throw new InvalidStateError("cannot start an already started poll");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public stop() {
|
||||||
|
if (this.timeout !== null) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
this.timeout = null;
|
||||||
|
} else {
|
||||||
|
throw new InvalidStateError("cannot stop a poll that hasn't started");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -386,6 +386,9 @@ function patchMatrixClientForRetry() {
|
|||||||
// We need to retry.
|
// We need to retry.
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
|
if (attempt >= MAX_REQUEST_ATTEMPTS) {
|
||||||
|
LogService.warn('Mjolnir.client', `Retried request ${params.method} ${params.uri} ${attempt} times, giving up.`);
|
||||||
|
}
|
||||||
// No need-to-retry error? Lucky us!
|
// No need-to-retry error? Lucky us!
|
||||||
// Note that this may very well be an error, just not
|
// Note that this may very well be an error, just not
|
||||||
// one we need to retry.
|
// one we need to retry.
|
||||||
|
@ -231,7 +231,6 @@ describe('Test: We will not be able to ban ourselves via ACL.', function () {
|
|||||||
|
|
||||||
describe('Test: ACL updates will batch when rules are added in succession.', function () {
|
describe('Test: ACL updates will batch when rules are added in succession.', function () {
|
||||||
it('Will batch ACL updates if we spam rules into a BanList', async function () {
|
it('Will batch ACL updates if we spam rules into a BanList', async function () {
|
||||||
this.timeout(180000)
|
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir = config.RUNTIME.client!
|
||||||
const serverName: string = new UserID(await mjolnir.getUserId()).domain
|
const serverName: string = new UserID(await mjolnir.getUserId()).domain
|
||||||
const moderator = await newTestUser({ name: { contains: "moderator" }});
|
const moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||||
@ -268,6 +267,8 @@ describe('Test: ACL updates will batch when rules are added in succession.', fun
|
|||||||
// Give them a bit of a spread over time.
|
// Give them a bit of a spread over time.
|
||||||
await new Promise(resolve => setTimeout(resolve, 5));
|
await new Promise(resolve => setTimeout(resolve, 5));
|
||||||
}
|
}
|
||||||
|
// give the events a chance to appear in the response to `/state`, since this is a problem.
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
// We do this because it should force us to wait until all the ACL events have been applied.
|
// We do this because it should force us to wait until all the ACL events have been applied.
|
||||||
// Even if that does mean the last few events will not go through batching...
|
// Even if that does mean the last few events will not go through batching...
|
||||||
@ -364,9 +365,9 @@ describe('Test: unbaning entities via the BanList.', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe.only('Test: should apply bans to the most recently active rooms first', function () {
|
describe('Test: should apply bans to the most recently active rooms first', function () {
|
||||||
it('Applies bans to the most recently active rooms first', async function () {
|
it('Applies bans to the most recently active rooms first', async function () {
|
||||||
this.timeout(6000000000)
|
this.timeout(180000)
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir = config.RUNTIME.client!
|
||||||
const serverName: string = new UserID(await mjolnir.getUserId()).domain
|
const serverName: string = new UserID(await mjolnir.getUserId()).domain
|
||||||
const moderator = await newTestUser({ name: { contains: "moderator" }});
|
const moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||||
|
47
test/integration/reportPollingTest.ts
Normal file
47
test/integration/reportPollingTest.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Mjolnir } from "../../src/Mjolnir";
|
||||||
|
import { IProtection } from "../../src/protections/IProtection";
|
||||||
|
import { newTestUser } from "./clientHelper";
|
||||||
|
|
||||||
|
describe("Test: Report polling", function() {
|
||||||
|
let client;
|
||||||
|
this.beforeEach(async function () {
|
||||||
|
client = await newTestUser({ name: { contains: "protection-settings" }});
|
||||||
|
})
|
||||||
|
it("Mjolnir correctly retrieves a report from synapse", async function() {
|
||||||
|
this.timeout(40000);
|
||||||
|
|
||||||
|
let protectedRoomId = await this.mjolnir.client.createRoom({ invite: [await client.getUserId()] });
|
||||||
|
await client.joinRoom(protectedRoomId);
|
||||||
|
await this.mjolnir.addProtectedRoom(protectedRoomId);
|
||||||
|
|
||||||
|
const eventId = await client.sendMessage(protectedRoomId, {msgtype: "m.text", body: "uwNd3q"});
|
||||||
|
await new Promise(async resolve => {
|
||||||
|
await this.mjolnir.registerProtection(new class implements IProtection {
|
||||||
|
name = "jYvufI";
|
||||||
|
description = "A test protection";
|
||||||
|
settings = { };
|
||||||
|
handleEvent = async (mjolnir: Mjolnir, roomId: string, event: any) => { };
|
||||||
|
handleReport = (mjolnir: Mjolnir, roomId: string, reporterId: string, event: any, reason?: string) => {
|
||||||
|
if (reason === "x5h1Je") {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await this.mjolnir.enableProtection("jYvufI");
|
||||||
|
await client.doRequest(
|
||||||
|
"POST",
|
||||||
|
`/_matrix/client/r0/rooms/${encodeURIComponent(protectedRoomId)}/report/${encodeURIComponent(eventId)}`, "", {
|
||||||
|
reason: "x5h1Je"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// So I kid you not, it seems like we can quit before the webserver for reports sends a respond to the client (via L#26)
|
||||||
|
// because the promise above gets resolved before we finish awaiting the report sending request on L#31,
|
||||||
|
// then mocha's cleanup code runs (and shuts down the webserver) before the webserver can respond.
|
||||||
|
// Wait a minute 😲😲🤯 it's not even supposed to be using the webserver if this is testing report polling.
|
||||||
|
// Ok, well apparently that needs a big refactor to change, but if you change the config before running this test,
|
||||||
|
// then you can ensure that report polling works. https://github.com/matrix-org/mjolnir/issues/326.
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -401,25 +401,37 @@ describe("Test: Testing RoomMemberManager", function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create and protect rooms.
|
// Create and protect rooms.
|
||||||
// - room 0 remains unprotected, as witness;
|
//
|
||||||
// - room 1 is protected but won't be targeted directly, also as witness.
|
// We reserve two control rooms:
|
||||||
|
// - room 0, also known as the "control unprotected room" is unprotected
|
||||||
|
// (we're not calling `!mjolnir rooms add` for this room), so none
|
||||||
|
// of the operations of `!mjolnir since` shoud affect it. We are
|
||||||
|
// using it to control, at the end of each experiment, that none of
|
||||||
|
// the `!mjolnir since` operations affect it.
|
||||||
|
// - room 1, also known as the "control protected room" is protected
|
||||||
|
// (we are calling `!mjolnir rooms add` for this room), but we are
|
||||||
|
// never directly requesting any `!mjolnir since` action against
|
||||||
|
// this room. We are using it to control, at the end of each experiment,
|
||||||
|
// that none of the `!mjolnir since` operations that should target
|
||||||
|
// one single other room also affect that room. It is, however, affected
|
||||||
|
// by general operations that are designed to affect all protected rooms.
|
||||||
const NUMBER_OF_ROOMS = 18;
|
const NUMBER_OF_ROOMS = 18;
|
||||||
const roomIds: string[] = [];
|
const allRoomIds: string[] = [];
|
||||||
const roomAliases: string[] = [];
|
const allRoomAliases: string[] = [];
|
||||||
const mjolnirUserId = await this.mjolnir.client.getUserId();
|
const mjolnirUserId = await this.mjolnir.client.getUserId();
|
||||||
for (let i = 0; i < NUMBER_OF_ROOMS; ++i) {
|
for (let i = 0; i < NUMBER_OF_ROOMS; ++i) {
|
||||||
const roomId = await this.moderator.createRoom({
|
const roomId = await this.moderator.createRoom({
|
||||||
invite: [mjolnirUserId, ...goodUserIds, ...badUserIds],
|
invite: [mjolnirUserId, ...goodUserIds, ...badUserIds],
|
||||||
});
|
});
|
||||||
roomIds.push(roomId);
|
allRoomIds.push(roomId);
|
||||||
|
|
||||||
const alias = `#since-test-${randomUUID()}:localhost:9999`;
|
const alias = `#since-test-${randomUUID()}:localhost:9999`;
|
||||||
await this.moderator.createRoomAlias(alias, roomId);
|
await this.moderator.createRoomAlias(alias, roomId);
|
||||||
roomAliases.push(alias);
|
allRoomAliases.push(alias);
|
||||||
}
|
}
|
||||||
for (let i = 1; i < roomIds.length; ++i) {
|
for (let i = 1; i < allRoomIds.length; ++i) {
|
||||||
// Protect all rooms except roomIds[0], as witness.
|
// Protect all rooms except allRoomIds[0], as control.
|
||||||
const roomId = roomIds[i];
|
const roomId = allRoomIds[i];
|
||||||
await this.mjolnir.client.joinRoom(roomId);
|
await this.mjolnir.client.joinRoom(roomId);
|
||||||
await this.moderator.setUserPowerLevel(mjolnirUserId, roomId, 100);
|
await this.moderator.setUserPowerLevel(mjolnirUserId, roomId, 100);
|
||||||
await this.moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir rooms add ${roomId}` });
|
await this.moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir rooms add ${roomId}` });
|
||||||
@ -429,8 +441,8 @@ describe("Test: Testing RoomMemberManager", function() {
|
|||||||
do {
|
do {
|
||||||
let protectedRooms = this.mjolnir.protectedRooms;
|
let protectedRooms = this.mjolnir.protectedRooms;
|
||||||
protectedRoomsUpdated = true;
|
protectedRoomsUpdated = true;
|
||||||
for (let i = 1; i < roomIds.length; ++i) {
|
for (let i = 1; i < allRoomIds.length; ++i) {
|
||||||
const roomId = roomIds[i];
|
const roomId = allRoomIds[i];
|
||||||
if (!(roomId in protectedRooms)) {
|
if (!(roomId in protectedRooms)) {
|
||||||
protectedRoomsUpdated = false;
|
protectedRoomsUpdated = false;
|
||||||
await new Promise(resolve => setTimeout(resolve, 1_000));
|
await new Promise(resolve => setTimeout(resolve, 1_000));
|
||||||
@ -440,7 +452,7 @@ describe("Test: Testing RoomMemberManager", function() {
|
|||||||
|
|
||||||
// Good users join before cut date.
|
// Good users join before cut date.
|
||||||
for (let user of this.goodUsers) {
|
for (let user of this.goodUsers) {
|
||||||
for (let roomId of roomIds) {
|
for (let roomId of allRoomIds) {
|
||||||
await user.joinRoom(roomId);
|
await user.joinRoom(roomId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -453,25 +465,30 @@ describe("Test: Testing RoomMemberManager", function() {
|
|||||||
|
|
||||||
// Bad users join after cut date.
|
// Bad users join after cut date.
|
||||||
for (let user of this.badUsers) {
|
for (let user of this.badUsers) {
|
||||||
for (let roomId of roomIds) {
|
for (let roomId of allRoomIds) {
|
||||||
await user.joinRoom(roomId);
|
await user.joinRoom(roomId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finally, prepare our control rooms and separate them
|
||||||
|
// from the regular rooms.
|
||||||
|
const CONTROL_UNPROTECTED_ROOM_ID = allRoomIds[0];
|
||||||
|
const CONTROL_PROTECTED_ID = allRoomIds[1];
|
||||||
|
const roomIds = allRoomIds.slice(2);
|
||||||
|
const roomAliases = allRoomAliases.slice(2);
|
||||||
|
|
||||||
enum Method {
|
enum Method {
|
||||||
kick,
|
kick,
|
||||||
ban,
|
ban,
|
||||||
mute,
|
mute,
|
||||||
unmute,
|
unmute,
|
||||||
}
|
}
|
||||||
const WITNESS_UNPROTECTED_ROOM_ID = roomIds[0];
|
|
||||||
const WITNESS_ROOM_ID = roomIds[1];
|
|
||||||
class Experiment {
|
class Experiment {
|
||||||
// A human-readable name for the command.
|
// A human-readable name for the command.
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
// If `true`, this command should affect room `WITNESS_ROOM_ID`.
|
// If `true`, this command should affect room `CONTROL_PROTECTED_ID`.
|
||||||
// Defaults to `false`.
|
// Defaults to `false`.
|
||||||
readonly shouldAffectWitnessRoom: boolean;
|
readonly shouldAffectControlProtected: boolean;
|
||||||
// The actual command-line.
|
// The actual command-line.
|
||||||
readonly command: (roomId: string, roomAlias: string) => string;
|
readonly command: (roomId: string, roomAlias: string) => string;
|
||||||
// The number of responses we expect to this command.
|
// The number of responses we expect to this command.
|
||||||
@ -484,17 +501,23 @@ describe("Test: Testing RoomMemberManager", function() {
|
|||||||
// Defaults to `false`.
|
// Defaults to `false`.
|
||||||
readonly isSameRoomAsPrevious: boolean;
|
readonly isSameRoomAsPrevious: boolean;
|
||||||
|
|
||||||
|
// The index of the room on which we're acting.
|
||||||
|
//
|
||||||
|
// Initialized by `addTo`.
|
||||||
roomIndex: number | undefined;
|
roomIndex: number | undefined;
|
||||||
|
|
||||||
constructor({name, shouldAffectWitnessRoom, command, n, method, sameRoom}: {name: string, command: (roomId: string, roomAlias: string) => string, shouldAffectWitnessRoom?: boolean, n?: number, method: Method, sameRoom?: boolean}) {
|
constructor({name, shouldAffectControlProtected, command, n, method, sameRoom}: {name: string, command: (roomId: string, roomAlias: string) => string, shouldAffectControlProtected?: boolean, n?: number, method: Method, sameRoom?: boolean}) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.shouldAffectWitnessRoom = typeof shouldAffectWitnessRoom === "undefined" ? false : shouldAffectWitnessRoom;
|
this.shouldAffectControlProtected = typeof shouldAffectControlProtected === "undefined" ? false : shouldAffectControlProtected;
|
||||||
this.command = command;
|
this.command = command;
|
||||||
this.n = typeof n === "undefined" ? 1 : n;
|
this.n = typeof n === "undefined" ? 1 : n;
|
||||||
this.method = method;
|
this.method = method;
|
||||||
this.isSameRoomAsPrevious = typeof sameRoom === "undefined" ? false : sameRoom;
|
this.isSameRoomAsPrevious = typeof sameRoom === "undefined" ? false : sameRoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add an experiment to the list of experiments.
|
||||||
|
//
|
||||||
|
// This is how `roomIndex` gets initialized.
|
||||||
addTo(experiments: Experiment[]) {
|
addTo(experiments: Experiment[]) {
|
||||||
if (this.isSameRoomAsPrevious) {
|
if (this.isSameRoomAsPrevious) {
|
||||||
this.roomIndex = experiments[experiments.length - 1].roomIndex;
|
this.roomIndex = experiments[experiments.length - 1].roomIndex;
|
||||||
@ -586,7 +609,7 @@ describe("Test: Testing RoomMemberManager", function() {
|
|||||||
new Experiment({
|
new Experiment({
|
||||||
name: "kick with date and reason",
|
name: "kick with date and reason",
|
||||||
command: (roomId: string) => `!mjolnir since "${cutDate}" kick 100 ${roomId} bad, bad user`,
|
command: (roomId: string) => `!mjolnir since "${cutDate}" kick 100 ${roomId} bad, bad user`,
|
||||||
shouldAffectWitnessRoom: false,
|
shouldAffectControlProtected: false,
|
||||||
n: 1,
|
n: 1,
|
||||||
method: Method.kick,
|
method: Method.kick,
|
||||||
}),
|
}),
|
||||||
@ -626,19 +649,35 @@ describe("Test: Testing RoomMemberManager", function() {
|
|||||||
new Experiment({
|
new Experiment({
|
||||||
name: "kick with date everywhere",
|
name: "kick with date everywhere",
|
||||||
command: () => `!mjolnir since "${cutDate}" kick 100 * bad, bad user`,
|
command: () => `!mjolnir since "${cutDate}" kick 100 * bad, bad user`,
|
||||||
shouldAffectWitnessRoom: true,
|
shouldAffectControlProtected: true,
|
||||||
n: NUMBER_OF_ROOMS - 1,
|
n: NUMBER_OF_ROOMS - 1,
|
||||||
method: Method.kick,
|
method: Method.kick,
|
||||||
}),
|
}),
|
||||||
]) {
|
]) {
|
||||||
experiment.addTo(EXPERIMENTS);
|
experiment.addTo(EXPERIMENTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Just-in-case health check, before starting.
|
||||||
|
{
|
||||||
|
const usersInUnprotectedControlProtected = await this.mjolnir.client.getJoinedRoomMembers(CONTROL_UNPROTECTED_ROOM_ID);
|
||||||
|
const usersInControlProtected = await this.mjolnir.client.getJoinedRoomMembers(CONTROL_PROTECTED_ID);
|
||||||
|
for (let userId of goodUserIds) {
|
||||||
|
assert.ok(usersInUnprotectedControlProtected.includes(userId), `Initially, good user ${userId} should be in the unprotected control room`);
|
||||||
|
assert.ok(usersInControlProtected.includes(userId), `Initially, good user ${userId} should be in the control room`);
|
||||||
|
}
|
||||||
|
for (let userId of badUserIds) {
|
||||||
|
assert.ok(usersInUnprotectedControlProtected.includes(userId), `Initially, bad user ${userId} should be in the unprotected control room`);
|
||||||
|
assert.ok(usersInControlProtected.includes(userId), `Initially, bad user ${userId} should be in the control room`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < EXPERIMENTS.length; ++i) {
|
for (let i = 0; i < EXPERIMENTS.length; ++i) {
|
||||||
const experiment = EXPERIMENTS[i];
|
const experiment = EXPERIMENTS[i];
|
||||||
const index = experiment.roomIndex! + 1;
|
const index = experiment.roomIndex!;
|
||||||
const roomId = roomIds[index];
|
const roomId = roomIds[index];
|
||||||
const roomAlias = roomAliases[index];
|
const roomAlias = roomAliases[index];
|
||||||
const joined = this.mjolnir.roomJoins.getUsersInRoom(roomId, start, 100);
|
const joined = this.mjolnir.roomJoins.getUsersInRoom(roomId, start, 100);
|
||||||
|
console.debug(`Running experiment ${i} "${experiment.name}" in room index ${index} (${roomId} / ${roomAlias}): \`${experiment.command(roomId, roomAlias)}\``);
|
||||||
assert.ok(joined.length >= 2 * SAMPLE_SIZE, `In experiment ${experiment.name}, we should have seen ${2 * SAMPLE_SIZE} users, saw ${joined.length}`);
|
assert.ok(joined.length >= 2 * SAMPLE_SIZE, `In experiment ${experiment.name}, we should have seen ${2 * SAMPLE_SIZE} users, saw ${joined.length}`);
|
||||||
|
|
||||||
// Run experiment.
|
// Run experiment.
|
||||||
@ -650,12 +689,12 @@ describe("Test: Testing RoomMemberManager", function() {
|
|||||||
|
|
||||||
// Check post-conditions.
|
// Check post-conditions.
|
||||||
const usersInRoom = await this.mjolnir.client.getJoinedRoomMembers(roomId);
|
const usersInRoom = await this.mjolnir.client.getJoinedRoomMembers(roomId);
|
||||||
const usersInUnprotectedWitnessRoom = await this.mjolnir.client.getJoinedRoomMembers(WITNESS_UNPROTECTED_ROOM_ID);
|
const usersInUnprotectedControlProtected = await this.mjolnir.client.getJoinedRoomMembers(CONTROL_UNPROTECTED_ROOM_ID);
|
||||||
const usersInWitnessRoom = await this.mjolnir.client.getJoinedRoomMembers(WITNESS_ROOM_ID);
|
const usersInControlProtected = await this.mjolnir.client.getJoinedRoomMembers(CONTROL_PROTECTED_ID);
|
||||||
for (let userId of goodUserIds) {
|
for (let userId of goodUserIds) {
|
||||||
assert.ok(usersInRoom.includes(userId), `After a ${experiment.name}, good user ${userId} should still be in affected room`);
|
assert.ok(usersInRoom.includes(userId), `After a ${experiment.name}, good user ${userId} should still be in affected room`);
|
||||||
assert.ok(usersInWitnessRoom.includes(userId), `After a ${experiment.name}, good user ${userId} should still be in witness room`);
|
assert.ok(usersInControlProtected.includes(userId), `After a ${experiment.name}, good user ${userId} should still be in control room (${CONTROL_PROTECTED_ID})`);
|
||||||
assert.ok(usersInUnprotectedWitnessRoom.includes(userId), `After a ${experiment.name}, good user ${userId} should still be in unprotected witness room`);
|
assert.ok(usersInUnprotectedControlProtected.includes(userId), `After a ${experiment.name}, good user ${userId} should still be in unprotected control room (${CONTROL_UNPROTECTED_ROOM_ID})`);
|
||||||
}
|
}
|
||||||
if (experiment.method === Method.mute) {
|
if (experiment.method === Method.mute) {
|
||||||
for (let userId of goodUserIds) {
|
for (let userId of goodUserIds) {
|
||||||
@ -678,8 +717,8 @@ describe("Test: Testing RoomMemberManager", function() {
|
|||||||
} else {
|
} else {
|
||||||
for (let userId of badUserIds) {
|
for (let userId of badUserIds) {
|
||||||
assert.ok(!usersInRoom.includes(userId), `After a ${experiment.name}, bad user ${userId} should NOT be in affected room`);
|
assert.ok(!usersInRoom.includes(userId), `After a ${experiment.name}, bad user ${userId} should NOT be in affected room`);
|
||||||
assert.equal(usersInWitnessRoom.includes(userId), !experiment.shouldAffectWitnessRoom, `After a ${experiment.name}, bad user ${userId} should ${experiment.shouldAffectWitnessRoom ? "NOT" : "still"} be in witness room`);
|
assert.equal(usersInControlProtected.includes(userId), !experiment.shouldAffectControlProtected, `After a ${experiment.name}, bad user ${userId} should ${experiment.shouldAffectControlProtected ? "NOT" : "still"} be in control room`);
|
||||||
assert.ok(usersInUnprotectedWitnessRoom.includes(userId), `After a ${experiment.name}, bad user ${userId} should still be in unprotected witness room`);
|
assert.ok(usersInUnprotectedControlProtected.includes(userId), `After a ${experiment.name}, bad user ${userId} should still be in unprotected control room`);
|
||||||
const leaveEvent = await this.mjolnir.client.getRoomStateEvent(roomId, "m.room.member", userId);
|
const leaveEvent = await this.mjolnir.client.getRoomStateEvent(roomId, "m.room.member", userId);
|
||||||
switch (experiment.method) {
|
switch (experiment.method) {
|
||||||
case Method.kick:
|
case Method.kick:
|
||||||
|
@ -5,17 +5,16 @@ import { getFirstReaction } from "./commands/commandUtils";
|
|||||||
|
|
||||||
describe("Test: throttled users can function with Mjolnir.", function () {
|
describe("Test: throttled users can function with Mjolnir.", function () {
|
||||||
it('throttled users survive being throttled by synapse', async function() {
|
it('throttled users survive being throttled by synapse', async function() {
|
||||||
this.timeout(60000);
|
|
||||||
let throttledUser = await newTestUser({ name: { contains: "throttled" }, isThrottled: true });
|
let throttledUser = await newTestUser({ name: { contains: "throttled" }, isThrottled: true });
|
||||||
let throttledUserId = await throttledUser.getUserId();
|
let throttledUserId = await throttledUser.getUserId();
|
||||||
let targetRoom = await throttledUser.createRoom();
|
let targetRoom = await throttledUser.createRoom();
|
||||||
// send enough messages to hit the rate limit.
|
// 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}`})));
|
await Promise.all([...Array(25).keys()].map((i) => throttledUser.sendMessage(targetRoom, {msgtype: 'm.text.', body: `Message #${i}`})));
|
||||||
let messageCount = 0;
|
let messageCount = 0;
|
||||||
await getMessagesByUserIn(throttledUser, throttledUserId, targetRoom, 150, (events) => {
|
await getMessagesByUserIn(throttledUser, throttledUserId, targetRoom, 25, (events) => {
|
||||||
messageCount += events.length;
|
messageCount += events.length;
|
||||||
});
|
});
|
||||||
assert.equal(messageCount, 150, "There should have been 150 messages in this room");
|
assert.equal(messageCount, 25, "There should have been 25 messages in this room");
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -31,7 +30,6 @@ describe("Test: Mjolnir can still sync and respond to commands while throttled",
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Can still perform and respond to a redaction command', async function () {
|
it('Can still perform and respond to a redaction command', async function () {
|
||||||
this.timeout(60000);
|
|
||||||
// Create a few users and a room.
|
// Create a few users and a room.
|
||||||
let badUser = await newTestUser({ name: { contains: "spammer-needs-redacting" } });
|
let badUser = await newTestUser({ name: { contains: "spammer-needs-redacting" } });
|
||||||
let badUserId = await badUser.getUserId();
|
let badUserId = await badUser.getUserId();
|
||||||
@ -45,12 +43,12 @@ describe("Test: Mjolnir can still sync and respond to commands while throttled",
|
|||||||
await badUser.joinRoom(targetRoom);
|
await badUser.joinRoom(targetRoom);
|
||||||
|
|
||||||
// Give Mjolnir some work to do and some messages to sync through.
|
// 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(25).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 Promise.all([...Array(25).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 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}`})));
|
await Promise.all([...Array(25).keys()].map((i) => badUser.sendMessage(targetRoom, {msgtype: 'm.text.', body: `Bad Message #${i}`})));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await moderator.start();
|
await moderator.start();
|
||||||
@ -72,6 +70,6 @@ describe("Test: Mjolnir can still sync and respond to commands while throttled",
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
assert.equal(count, 51, "There should be exactly 51 events from the spammer in this room.");
|
assert.equal(count, 26, "There should be exactly 26 events from the spammer in this room.");
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"./src/**/*",
|
"./src/**/*",
|
||||||
"./test/integration/manualLaunchScript.ts",
|
"./test/integration/manualLaunchScript.ts",
|
||||||
"./test/integration/roomMembersTest.ts",
|
"./test/integration/roomMembersTest.ts",
|
||||||
"./test/integration/banListTest.ts"
|
"./test/integration/banListTest.ts",
|
||||||
|
"./test/integration/reportPollingTest"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user