diff --git a/README.md b/README.md
index b12b06c..d382cb0 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@ This requires two configuration steps:
1. In your Mjölnir configuration file, typically `/etc/mjolnir/config/production.yaml`, copy and paste the `web` section from `default.yaml`, if you don't have it yet (it appears with version 1.20) and set `enabled: true` for both `web` and
`abuseReporting`.
-2. Setup a reverse proxy that will redirect requests from `^/_matrix/client/r0/rooms/([^/]*)/report/(.*)$` to `http://host:port/api/1/report/$1/$2`, where `host` is the host where you run Mjölnir, and `port` is the port you configured in `production.yaml`. For an example nginx configuration, see `test/nginx.conf`. It's the confirmation we use during runtime testing.
+2. Setup a reverse proxy that will redirect requests from `^/_matrix/client/(r0|v3)/rooms/([^/]*)/report/(.*)$` to `http://host:port/api/1/report/$2/$3`, where `host` is the host where you run Mjölnir, and `port` is the port you configured in `production.yaml`. For an example nginx configuration, see `test/nginx.conf`. It's the confirmation we use during runtime testing.
### Security note
diff --git a/test/integration/abuseReportTest.ts b/test/integration/abuseReportTest.ts
index 2bbb9bb..9484a83 100644
--- a/test/integration/abuseReportTest.ts
+++ b/test/integration/abuseReportTest.ts
@@ -1,6 +1,5 @@
import { strict as assert } from "assert";
-import config from "../../src/config";
import { matrixClient } from "./mjolnirSetupUtils";
import { newTestUser } from "./clientHelper";
import { ReportManager, ABUSE_ACTION_CONFIRMATION_KEY, ABUSE_REPORT_KEY } from "../../src/report/ReportManager";
@@ -20,207 +19,214 @@ const REPORT_NOTICE_REGEXPS = {
describe("Test: Reporting abuse", async () => {
- it('Mjölnir intercepts abuse reports', async function() {
- this.timeout(60000);
+ // 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[] = [];
- matrixClient()!.on("room.event", (roomId, event) => {
- if (roomId = this.mjolnir.managementRoomId) {
- notices.push(event);
+ // 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 = `BAD: ${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 | 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]}`);
+ }
+ }
}
});
-
- // 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 = `BAD: ${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 goodEventId = await goodUser.sendText(roomId, goodText);
- 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 = []
-
- // Time to report, first without a comment, then with one.
- try {
- await goodUser.doRequest("POST", `/_matrix/client/r0/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/r0/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/r0/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/r0/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/r0/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 = [];
- 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 = 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 = [];
- matrixClient().on("room.event", (roomId, event) => {
+ let notices: any[] = [];
+ this.mjolnir.client.on("room.event", (roomId, event) => {
if (roomId = this.mjolnir.managementRoomId) {
notices.push(event);
}
@@ -228,7 +234,7 @@ describe("Test: Reporting abuse", async () => {
// Create a moderator.
let moderatorUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reporting-abuse-moderator-user" }});
- matrixClient().inviteUser(await moderatorUser.getUserId(), this.mjolnir.managementRoomId);
+ this.mjolnir.client.inviteUser(await moderatorUser.getUserId(), this.mjolnir.managementRoomId);
await moderatorUser.joinRoom(this.mjolnir.managementRoomId);
// Create a few users and a room.
@@ -244,8 +250,8 @@ describe("Test: Reporting abuse", async () => {
await goodUser.joinRoom(roomId);
// Setup Mjölnir as moderator for our room.
- await moderatorUser.inviteUser(await matrixClient().getUserId(), roomId);
- await moderatorUser.setUserPowerLevel(await matrixClient().getUserId(), roomId, 100);
+ 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.
@@ -275,7 +281,7 @@ describe("Test: Reporting abuse", async () => {
console.log("Test: Reporting abuse - wait");
await new Promise(resolve => setTimeout(resolve, 1000));
- let mjolnirRooms = new Set(await matrixClient().getJoinedRooms());
+ 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
@@ -293,7 +299,7 @@ describe("Test: Reporting abuse", async () => {
assert.ok(noticeId, "We should have found our notice");
// Find the buttons.
- let buttons = [];
+ let buttons: any[] = [];
for (let event of notices) {
if (event["type"] != "m.reaction") {
continue;
@@ -347,7 +353,7 @@ describe("Test: Reporting abuse", async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
// This should have redacted the message.
- let newBadEvent = await matrixClient().getEvent(roomId, badEventId);
+ 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");
});
});
diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts
index d1975af..d19c0ac 100644
--- a/test/integration/fixtures.ts
+++ b/test/integration/fixtures.ts
@@ -12,7 +12,7 @@ export const mochaHooks = {
console.error("---- entering test", JSON.stringify(this.currentTest.title)); // Makes MatrixClient error logs a bit easier to parse.
console.log("mochaHooks.beforeEach");
// Sometimes it takes a little longer to register users.
- this.timeout(10000);
+ this.timeout(30000);
const config = this.config = configRead();
this.managementRoomAlias = config.managementRoom;
this.mjolnir = await makeMjolnir(config);
diff --git a/test/nginx.conf b/test/nginx.conf
index 9b73091..c4add6a 100644
--- a/test/nginx.conf
+++ b/test/nginx.conf
@@ -6,8 +6,10 @@ http {
server {
listen 8081;
- location ~ ^/_matrix/client/r0/rooms/([^/]*)/report/(.*)$ {
+ location ~ ^/_matrix/client/(r0|v3)/rooms/([^/]*)/report/(.*)$ {
# Abuse reports should be sent to Mjölnir.
+ # The r0 endpoint is deprecated but still used by many clients.
+ # As of this writing, the v3 endpoint is the up-to-date version.
# Add CORS, otherwise a browser will refuse this request.
add_header 'Access-Control-Allow-Origin' '*' always; # Note: '*' is for testing purposes. For your own server, you probably want to tighten this.
@@ -18,8 +20,8 @@ http {
add_header 'Access-Control-Max-Age' 1728000; # cache preflight value for 20 days
# Alias the regexps, to ensure that they're not rewritten.
- set $room_id $1;
- set $event_id $2;
+ set $room_id $2;
+ set $event_id $3;
proxy_pass http://127.0.0.1:8082/api/1/report/$room_id/$event_id;
}
location / {