mjolnir/test/integration/abuseReportTest.ts
David Teller 7b0edadd17
Update tests and instructions to intercept reports also with v3 endpoint (#388)
In both our instructions and our tests, we use the r0 endpoint to intercept abuse reports. This endpoint is deprecated and not implemented by all clients. This PR updates the instructions and tests to the new endpoint.
2022-10-18 15:48:39 +02:00

360 lines
17 KiB
TypeScript

import { strict as assert } from "assert";
import { matrixClient } from "./mjolnirSetupUtils";
import { newTestUser } from "./clientHelper";
import { ReportManager, ABUSE_ACTION_CONFIRMATION_KEY, ABUSE_REPORT_KEY } from "../../src/report/ReportManager";
/**
* Test the ability to turn abuse reports into room messages.
*/
const REPORT_NOTICE_REGEXPS = {
reporter: /Filed by (?<reporterDisplay>[^ ]*) \((?<reporterId>[^ ]*)\)/,
accused: /Against (?<accusedDisplay>[^ ]*) \((?<accusedId>[^ ]*)\)/,
room: /Room (?<roomAliasOrId>[^ ]*)/,
event: /Event (?<eventId>[^ ]*) Go to event/,
content: /Content (?<eventContent>.*)/,
comments: /Comments Comments (?<comments>.*)/
};
describe("Test: Reporting abuse", async () => {
// Testing with successive versions of the API.
//
// As of this writing, v3 is the standard, while r0 is deprecated. However,
// both versions are still in use in the wild.
// Note that this version change only affects the actual URL at which reports
// are sent.
for (let endpoint of ['v3', 'r0']) {
it(`Mjölnir intercepts abuse reports with endpoint ${endpoint}`, async function() {
this.timeout(90000);
// Listen for any notices that show up.
let notices: any[] = [];
this.mjolnir.client.on("room.event", (roomId, event) => {
if (roomId = this.mjolnir.managementRoomId) {
notices.push(event);
}
});
// Create a few users and a room.
let goodUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reporting-abuse-good-user" }});
let badUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reporting-abuse-bad-user" }});
let goodUserId = await goodUser.getUserId();
let badUserId = await badUser.getUserId();
let roomId = await goodUser.createRoom({ invite: [await badUser.getUserId()] });
await goodUser.inviteUser(await badUser.getUserId(), roomId);
await badUser.joinRoom(roomId);
console.log("Test: Reporting abuse - send messages");
// Exchange a few messages.
let goodText = `GOOD: ${Math.random()}`; // Will NOT be reported.
let badText = `BAD: ${Math.random()}`; // Will be reported as abuse.
let badText2 = `BAD: ${Math.random()}`; // Will be reported as abuse.
let badText3 = `<b>BAD</b>: ${Math.random()}`; // Will be reported as abuse.
let badText4 = [...Array(1024)].map(_ => `${Math.random()}`).join(""); // Text is too long.
let badText5 = [...Array(1024)].map(_ => "ABC").join("\n"); // Text has too many lines.
let badEventId = await badUser.sendText(roomId, badText);
let badEventId2 = await badUser.sendText(roomId, badText2);
let badEventId3 = await badUser.sendText(roomId, badText3);
let badEventId4 = await badUser.sendText(roomId, badText4);
let badEventId5 = await badUser.sendText(roomId, badText5);
let badEvent2Comment = `COMMENT: ${Math.random()}`;
console.log("Test: Reporting abuse - send reports");
let reportsToFind: any[] = []
// Time to report, first without a comment, then with one.
try {
await goodUser.doRequest("POST", `/_matrix/client/${endpoint}/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId)}`);
reportsToFind.push({
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId,
text: badText,
comment: null,
});
} catch (e) {
console.error("Could not send first report", e.body || e);
throw e;
}
try {
await goodUser.doRequest("POST", `/_matrix/client/${endpoint}/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId2)}`, "", {
reason: badEvent2Comment
});
reportsToFind.push({
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId2,
text: badText2,
comment: badEvent2Comment,
});
} catch (e) {
console.error("Could not send second report", e.body || e);
throw e;
}
try {
await goodUser.doRequest("POST", `/_matrix/client/${endpoint}/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId3)}`, "");
reportsToFind.push({
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId3,
text: badText3,
comment: null,
});
} catch (e) {
console.error("Could not send third report", e.body || e);
throw e;
}
try {
await goodUser.doRequest("POST", `/_matrix/client/${endpoint}/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId4)}`, "");
reportsToFind.push({
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId4,
text: null,
textPrefix: badText4.substring(0, 256),
comment: null,
});
} catch (e) {
console.error("Could not send fourth report", e.body || e);
throw e;
}
try {
await goodUser.doRequest("POST", `/_matrix/client/${endpoint}/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId5)}`, "");
reportsToFind.push({
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId5,
text: null,
textPrefix: badText5.substring(0, 256).split("\n").join(" "),
comment: null,
});
} catch (e) {
console.error("Could not send fifth report", e.body || e);
throw e;
}
console.log("Test: Reporting abuse - wait");
await new Promise(resolve => setTimeout(resolve, 1000));
let found: any[] = [];
for (let toFind of reportsToFind) {
for (let event of notices) {
if ("content" in event && "body" in event.content) {
if (!(ABUSE_REPORT_KEY in event.content) || event.content[ABUSE_REPORT_KEY].event_id != toFind.eventId) {
// Not a report or not our report.
continue;
}
let report = event.content[ABUSE_REPORT_KEY];
let body = event.content.body as string;
let matches: Map<string, RegExpMatchArray> | null = new Map();
for (let key of Object.keys(REPORT_NOTICE_REGEXPS)) {
let match = body.match(REPORT_NOTICE_REGEXPS[key]);
if (match) {
console.debug("We have a match", key, REPORT_NOTICE_REGEXPS[key], match.groups);
} else {
console.debug("Not a match", key, REPORT_NOTICE_REGEXPS[key]);
// Not a report, skipping.
matches = null;
break;
}
matches.set(key, match);
}
if (!matches) {
// Not a report, skipping.
continue;
}
assert(body.length < 3000, `The report shouldn't be too long ${body.length}`);
assert(body.split("\n").length < 200, "The report shouldn't have too many newlines.");
assert.equal(matches.get("event")!.groups!.eventId, toFind.eventId, "The report should specify the correct event id");;
assert.equal(matches.get("reporter")!.groups!.reporterId, toFind.reporterId, "The report should specify the correct reporter");
assert.equal(report.reporter_id, toFind.reporterId, "The embedded report should specify the correct reporter");
assert.ok(toFind.reporterId.includes(matches.get("reporter")!.groups!.reporterDisplay), "The report should display the correct reporter");
assert.equal(matches.get("accused")!.groups!.accusedId, toFind.accusedId, "The report should specify the correct accused");
assert.equal(report.accused_id, toFind.accusedId, "The embedded report should specify the correct accused");
assert.ok(toFind.accusedId.includes(matches.get("accused")!.groups!.accusedDisplay), "The report should display the correct reporter");
if (toFind.text) {
assert.equal(matches.get("content")!.groups!.eventContent, toFind.text, "The report should contain the text we inserted in the event");
}
if (toFind.textPrefix) {
assert.ok(matches.get("content")!.groups!.eventContent.startsWith(toFind.textPrefix), `The report should contain a prefix of the long text we inserted in the event: ${toFind.textPrefix} in? ${matches.get("content")!.groups!.eventContent}`);
}
if (toFind.comment) {
assert.equal(matches.get("comments")!.groups!.comments, toFind.comment, "The report should contain the comment we added");
}
assert.equal(matches.get("room")!.groups!.roomAliasOrId, roomId, "The report should specify the correct room");
assert.equal(report.room_id, roomId, "The embedded report should specify the correct room");
found.push(toFind);
break;
}
}
}
assert.deepEqual(found, reportsToFind);
// Since Mjölnir is not a member of the room, the only buttons we should find
// are `help` and `ignore`.
for (let event of notices) {
if (event.content && event.content["m.relates_to"] && event.content["m.relates_to"]["key"]) {
let regexp = /\/([[^]]*)\]/;
let matches = event.content["m.relates_to"]["key"].match(regexp);
if (!matches) {
continue;
}
switch (matches[1]) {
case "bad-report":
case "help":
continue;
default:
throw new Error(`Didn't expect label ${matches[1]}`);
}
}
}
});
}
it('The redact action works', async function() {
this.timeout(60000);
// Listen for any notices that show up.
let notices: any[] = [];
this.mjolnir.client.on("room.event", (roomId, event) => {
if (roomId = this.mjolnir.managementRoomId) {
notices.push(event);
}
});
// Create a moderator.
let moderatorUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reporting-abuse-moderator-user" }});
this.mjolnir.client.inviteUser(await moderatorUser.getUserId(), this.mjolnir.managementRoomId);
await moderatorUser.joinRoom(this.mjolnir.managementRoomId);
// Create a few users and a room.
let goodUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reacting-abuse-good-user" }});
let badUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reacting-abuse-bad-user" }});
let goodUserId = await goodUser.getUserId();
let badUserId = await badUser.getUserId();
let roomId = await moderatorUser.createRoom({ invite: [await badUser.getUserId()] });
await moderatorUser.inviteUser(await goodUser.getUserId(), roomId);
await moderatorUser.inviteUser(await badUser.getUserId(), roomId);
await badUser.joinRoom(roomId);
await goodUser.joinRoom(roomId);
// Setup Mjölnir as moderator for our room.
await moderatorUser.inviteUser(await this.mjolnir.client.getUserId(), roomId);
await moderatorUser.setUserPowerLevel(await this.mjolnir.client.getUserId(), roomId, 100);
console.log("Test: Reporting abuse - send messages");
// Exchange a few messages.
let goodText = `GOOD: ${Math.random()}`; // Will NOT be reported.
let badText = `BAD: ${Math.random()}`; // Will be reported as abuse.
let goodEventId = await goodUser.sendText(roomId, goodText);
let badEventId = await badUser.sendText(roomId, badText);
let goodEventId2 = await goodUser.sendText(roomId, goodText);
console.log("Test: Reporting abuse - send reports");
// Time to report.
let reportToFind = {
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId,
text: badText,
comment: null,
};
try {
await goodUser.doRequest("POST", `/_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId)}`);
} catch (e) {
console.error("Could not send first report", e.body || e);
throw e;
}
console.log("Test: Reporting abuse - wait");
await new Promise(resolve => setTimeout(resolve, 1000));
let mjolnirRooms = new Set(await this.mjolnir.client.getJoinedRooms());
assert.ok(mjolnirRooms.has(roomId), "Mjölnir should be a member of the room");
// Find the notice
let noticeId;
for (let event of notices) {
if ("content" in event && ABUSE_REPORT_KEY in event.content) {
if (!(ABUSE_REPORT_KEY in event.content) || event.content[ABUSE_REPORT_KEY].event_id != badEventId) {
// Not a report or not our report.
continue;
}
noticeId = event.event_id;
break;
}
}
assert.ok(noticeId, "We should have found our notice");
// Find the buttons.
let buttons: any[] = [];
for (let event of notices) {
if (event["type"] != "m.reaction") {
continue;
}
if (event["content"]["m.relates_to"]["rel_type"] != "m.annotation") {
continue;
}
if (event["content"]["m.relates_to"]["event_id"] != noticeId) {
continue;
}
buttons.push(event);
}
// Find the redact button... and click it.
let redactButtonId = null;
for (let button of buttons) {
if (button["content"]["m.relates_to"]["key"].includes("[redact-message]")) {
redactButtonId = button["event_id"];
await moderatorUser.sendEvent(this.mjolnir.managementRoomId, "m.reaction", button["content"]);
break;
}
}
assert.ok(redactButtonId, "We should have found the redact button");
await new Promise(resolve => setTimeout(resolve, 1000));
// This should have triggered a confirmation request, with more buttons!
let confirmEventId = null;
for (let event of notices) {
console.debug("Is this the confirm button?", event);
if (!event["content"]["m.relates_to"]) {
console.debug("Not a reaction");
continue;
}
if (!event["content"]["m.relates_to"]["key"].includes("[confirm]")) {
console.debug("Not confirm");
continue;
}
if (!event["content"]["m.relates_to"]["event_id"] == redactButtonId) {
console.debug("Not reaction to redact button");
continue;
}
// It's the confirm button, click it!
confirmEventId = event["event_id"];
await moderatorUser.sendEvent(this.mjolnir.managementRoomId, "m.reaction", event["content"]);
break;
}
assert.ok(confirmEventId, "We should have found the confirm button");
await new Promise(resolve => setTimeout(resolve, 1000));
// This should have redacted the message.
let newBadEvent = await this.mjolnir.client.getEvent(roomId, badEventId);
assert.deepEqual(Object.keys(newBadEvent.content), [], "Redaction should have removed the content of the offending event");
});
});