diff --git a/test/integration/commands/commandUtils.ts b/test/integration/commands/commandUtils.ts new file mode 100644 index 0000000..3af1389 --- /dev/null +++ b/test/integration/commands/commandUtils.ts @@ -0,0 +1,47 @@ +import { MatrixClient } from "matrix-bot-sdk"; + +/** + * Returns a promise that resolves to an event that is reacting to the event produced by targetEventThunk. + * @param client A MatrixClient that is already in the targetRoom that can be started to listen for the event produced by targetEventThunk. + * @param targetRoom The room to listen for the reaction in. + * @param reactionKey The reaction key to wait for. + * @param targetEventThunk A function that produces an event ID when called. This event ID is then used to listen for a reaction. + * @returns The reaction event. + */ +export async function onReactionTo(client: MatrixClient, targetRoom: string, reactionKey: string, targetEventThunk: () => Promise): Promise { + let reactionEvents = []; + const addEvent = function (roomId, event) { + if (roomId !== targetRoom) return; + if (event.type !== 'm.reaction') return; + reactionEvents.push(event); + }; + let targetCb; + try { + client.on('room.event', addEvent) + client.start(); + const targetEventId = await targetEventThunk(); + for (let event of reactionEvents) { + const relates_to = event.content['m.relates_to']; + if (relates_to.event_id === targetEventId && relates_to.key === reactionKey) { + return event; + } + } + return await new Promise((resolve, reject) => { + targetCb = function(roomId, event) { + if (roomId !== targetRoom) return; + if (event.type !== 'm.reaction') return; + const relates_to = event.content['m.relates_to']; + if (relates_to.event_id === targetEventId && relates_to.key === reactionKey) { + resolve(event) + } + } + client.on('room.event', targetCb); + }); + } finally { + client.stop() + client.removeListener('room.event', addEvent); + if (targetCb) { + client.removeListener('room.event', targetCb); + } + } +} diff --git a/test/integration/commands/redactCommandTest.ts b/test/integration/commands/redactCommandTest.ts new file mode 100644 index 0000000..581b37f --- /dev/null +++ b/test/integration/commands/redactCommandTest.ts @@ -0,0 +1,115 @@ +import { strict as assert } from "assert"; + +import config from "../../../src/config"; +import { newTestUser } from "../clientHelper"; +import { getMessagesByUserIn } from "../../../src/utils"; +import { LogService } from "matrix-bot-sdk"; +import { onReactionTo } from "./commandUtils"; + + describe("Test: The redaction command", async () => { + 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(20000); + // Create a few users and a room. + let badUser = await newTestUser(false, "spammer-needs-redacting"); + let badUserId = await badUser.getUserId(); + const mjolnir = config.RUNTIME.client! + let mjolnirUserId = await mjolnir.getUserId(); + let moderator = await newTestUser(false, "moderator"); + this.moderator = moderator; + await moderator.joinRoom(config.managementRoom); + let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]}); + await moderator.setUserPowerLevel(mjolnirUserId, targetRoom, 100); + await badUser.joinRoom(targetRoom); + moderator.sendMessage(config.managementRoom, {msgtype: 'm.text.', body: `!mjolnir rooms add ${targetRoom}`}); + + LogService.debug("redactionTest", `targetRoom: ${targetRoom}, managementRoom: ${config.managementRoom}`); + // Sandwich irrelevant messages in bad messages. + await badUser.sendMessage(targetRoom, {msgtype: 'm.text', body: "Very Bad Stuff"}); + await Promise.all([...Array(50).keys()].map((i) => moderator.sendMessage(targetRoom, {msgtype: 'm.text.', body: `Irrelevant Message #${i}`}))); + for (let i = 0; i < 5; i++) { + await badUser.sendMessage(targetRoom, {msgtype: 'm.text', body: "Very Bad Stuff"}); + } + await Promise.all([...Array(50).keys()].map((i) => moderator.sendMessage(targetRoom, {msgtype: 'm.text.', body: `Irrelevant Message #${i}`}))); + await badUser.sendMessage(targetRoom, {msgtype: 'm.text', body: "Very Bad Stuff"}); + + await onReactionTo(moderator, config.managementRoom, '✅', async () => { + return await moderator.sendMessage(config.managementRoom, { msgtype: 'm.text', body: `!mjolnir redact ${badUserId} ${targetRoom}` }); + }); + await getMessagesByUserIn(moderator, badUserId, targetRoom, 1000, function(events) { + 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 even 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)}`) + } + }) + }); + }) + 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(20000); + // Create a few users and a room. + let badUser = await newTestUser(false, "spammer-needs-redacting"); + let badUserId = await badUser.getUserId(); + const mjolnir = config.RUNTIME.client! + let mjolnirUserId = await mjolnir.getUserId(); + let moderator = await newTestUser(false, "moderator"); + this.moderator = moderator; + await moderator.joinRoom(config.managementRoom); + let targetRooms = []; + for (let i = 0; i < 5; i++) { + let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]}); + await moderator.setUserPowerLevel(mjolnirUserId, targetRoom, 100); + await badUser.joinRoom(targetRoom); + await moderator.sendMessage(config.managementRoom, {msgtype: 'm.text.', body: `!mjolnir rooms add ${targetRoom}`}); + targetRooms.push(targetRoom); + + // Sandwich irrelevant messages in bad messages. + await badUser.sendMessage(targetRoom, {msgtype: 'm.text', body: "Very Bad Stuff"}); + await Promise.all([...Array(50).keys()].map((j) => moderator.sendMessage(targetRoom, {msgtype: 'm.text.', body: `Irrelevant Message #${j}`}))); + for (let j = 0; j < 5; j++) { + await badUser.sendMessage(targetRoom, {msgtype: 'm.text', body: "Very Bad Stuff"}); + } + await Promise.all([...Array(50).keys()].map((j) => moderator.sendMessage(targetRoom, {msgtype: 'm.text.', body: `Irrelevant Message #${j}`}))); + await badUser.sendMessage(targetRoom, {msgtype: 'm.text', body: "Very Bad Stuff"}); + } + + await onReactionTo(moderator, config.managementRoom, '✅', async () => { + return await moderator.sendMessage(config.managementRoom, { msgtype: 'm.text', body: `!mjolnir redact ${badUserId}` }); + }); + + targetRooms.map(async targetRoom => { + await getMessagesByUserIn(moderator, badUserId, targetRoom, 1000, function(events) { + 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 even 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)}`) + } + }) + }) + }); + }); + it("Redacts a single event when instructed to.", async function () { + this.timeout(20000); + // Create a few users and a room. + let badUser = await newTestUser(false, "spammer-needs-redacting"); + let badUserId = await badUser.getUserId(); + const mjolnir = config.RUNTIME.client! + let mjolnirUserId = await mjolnir.getUserId(); + let moderator = await newTestUser(false, "moderator"); + this.moderator = moderator; + await moderator.joinRoom(config.managementRoom); + let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]}); + await moderator.setUserPowerLevel(mjolnirUserId, targetRoom, 100); + await badUser.joinRoom(targetRoom); + moderator.sendMessage(config.managementRoom, {msgtype: 'm.text.', body: `!mjolnir rooms add ${targetRoom}`}); + let eventToRedact = await badUser.sendMessage(targetRoom, {msgtype: 'm.text', body: "Very Bad Stuff"}); + + await onReactionTo(moderator, config.managementRoom, '✅', async () => { + return await moderator.sendMessage(config.managementRoom, {msgtype: 'm.text', body: `!mjolnir redact https://matrix.to/#/${encodeURIComponent(targetRoom)}/${encodeURIComponent(eventToRedact)}`}); + }); + + let redactedEvent = await moderator.getEvent(targetRoom, eventToRedact); + assert.equal(Object.keys(redactedEvent.content).length, 0, "This event should have been redacted"); + }) +}); diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index cd7a464..fc01344 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -12,6 +12,7 @@ export const mochaHooks = { console.log("mochaHooks.beforeEach"); this.managementRoomAlias = config.managementRoom; this.mjolnir = await makeMjolnir(); + config.RUNTIME.client = this.mjolnir.client; this.mjolnir.start(); console.log("mochaHooks.beforeEach DONE"); } @@ -24,7 +25,7 @@ export const mochaHooks = { let managementRoomId = config.managementRoom; config.managementRoom = this.managementRoomAlias; // remove alias from management room and leave it. - await teardownManagementRoom(this.mjolnir.client, managementRoomId, this.managementRoomAlias); + await teardownManagementRoom(this.mjolnir.client, managementRoomId, this.managementRoomAlias); } ] };