diff --git a/config/default.yaml b/config/default.yaml index 6cfb2a8..3f71520 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -57,6 +57,14 @@ noop: false # server struggles with /state requests then set this to true. fasterMembershipChecks: false +# A case-insensitive list of ban reasons to automatically redact a user's +# messages for. Typically this is useful to avoid having to type two commands +# to the bot. Use asterisks to represent globs (ie: "spam*testing" would match +# "spam for testing" as well as "spamtesting"). +automaticallyRedactForReasons: + - "spam" + - "advertising" + # A list of rooms to protect (matrix.to URLs) protectedRooms: - "https://matrix.to/#/#yourroom:example.org" diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index 7ba2b5d..af04ea1 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { LogLevel, LogService, MatrixClient, Permalinks } from "matrix-bot-sdk"; +import { LogLevel, LogService, MatrixClient, MatrixGlob, Permalinks } from "matrix-bot-sdk"; import BanList, { ALL_RULE_TYPES } from "./models/BanList"; import { applyServerAcls } from "./actions/ApplyAcl"; import { RoomUpdateError } from "./models/RoomUpdateError"; @@ -42,12 +42,17 @@ export class Mjolnir { private currentState: string = STATE_NOT_STARTED; private protections: IProtection[] = []; private redactionQueue = new AutomaticRedactionQueue(); + private automaticRedactionReasons: MatrixGlob[] = []; constructor( public readonly client: MatrixClient, public readonly protectedRooms: { [roomId: string]: string }, private banLists: BanList[], ) { + for (const reason of config.automaticallyRedactForReasons) { + this.automaticRedactionReasons.push(new MatrixGlob(reason.toLowerCase())); + } + client.on("room.event", this.handleEvent.bind(this)); client.on("room.message", async (roomId, event) => { @@ -95,6 +100,10 @@ export class Mjolnir { return this.redactionQueue; } + public get automaticRedactGlobs(): MatrixGlob[] { + return this.automaticRedactionReasons; + } + public start() { return this.client.start().then(async () => { this.currentState = STATE_CHECKING_PERMISSIONS; diff --git a/src/actions/ApplyBan.ts b/src/actions/ApplyBan.ts index 5a4cc4a..0b2ac1c 100644 --- a/src/actions/ApplyBan.ts +++ b/src/actions/ApplyBan.ts @@ -21,6 +21,7 @@ import config from "../config"; import { logMessage } from "../LogProxy"; import { LogLevel } from "matrix-bot-sdk"; import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache"; +import { redactUserMessagesIn } from "../utils"; /** * Applies the member bans represented by the ban lists to the provided rooms, returning the @@ -67,6 +68,11 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir await logMessage(LogLevel.DEBUG, "ApplyBan", `Banning ${member.userId} in ${roomId} for: ${userRule.reason}`); if (!config.noop) { + // Always prioritize redactions above bans + if (mjolnir.automaticRedactGlobs.find(g => g.test(userRule.reason.toLowerCase()))) { + await redactUserMessagesIn(mjolnir.client, member.userId, [roomId]); + } + await mjolnir.client.banUser(member.userId, roomId, userRule.reason); } else { await logMessage(LogLevel.WARN, "ApplyBan", `Tried to ban ${member.userId} in ${roomId} but Mjolnir is running in no-op mode`); diff --git a/src/commands/RedactCommand.ts b/src/commands/RedactCommand.ts index a5dbe33..873fb92 100644 --- a/src/commands/RedactCommand.ts +++ b/src/commands/RedactCommand.ts @@ -15,10 +15,7 @@ limitations under the License. */ import { Mjolnir } from "../Mjolnir"; -import { getMessagesByUserSinceLastJoin } from "../utils"; -import config from "../config"; -import { logMessage } from "../LogProxy"; -import { LogLevel } from "matrix-bot-sdk"; +import { redactUserMessagesIn } from "../utils"; // !mjolnir redact [room alias] export async function execRedactCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { @@ -29,19 +26,7 @@ export async function execRedactCommand(roomId: string, event: any, mjolnir: Mjo } const targetRoomIds = roomAlias ? [roomAlias] : Object.keys(mjolnir.protectedRooms); - for (const targetRoomId of targetRoomIds) { - await logMessage(LogLevel.DEBUG, "RedactCommand", `Fetching sent messages for ${userId} in ${targetRoomId} to redact...`); - - const eventsToRedact = await getMessagesByUserSinceLastJoin(mjolnir.client, userId, targetRoomId); - for (const victimEvent of eventsToRedact) { - await logMessage(LogLevel.DEBUG, "RedactCommand", `Redacting ${victimEvent['event_id']} in ${targetRoomId}`); - if (!config.noop) { - await mjolnir.client.redactEvent(targetRoomId, victimEvent['event_id']); - } else { - await logMessage(LogLevel.WARN, "RedactCommand", `Tried to redact ${victimEvent['event_id']} in ${targetRoomId} but Mjolnir is running in no-op mode`); - } - } - } + await redactUserMessagesIn(mjolnir.client, userId, targetRoomIds); await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); } diff --git a/src/config.ts b/src/config.ts index 2fffbea..e09f971 100644 --- a/src/config.ts +++ b/src/config.ts @@ -35,6 +35,7 @@ interface IConfig { noop: boolean; protectedRooms: string[]; // matrix.to urls fasterMembershipChecks: boolean; + automaticallyRedactForReasons: string[]; // case-insensitive globs /** * Config options only set at runtime. Try to avoid using the objects diff --git a/src/utils.ts b/src/utils.ts index 360be19..30df8d5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -14,7 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { LogService, MatrixClient, MatrixGlob } from "matrix-bot-sdk"; +import { LogLevel, LogService, MatrixClient, MatrixGlob } from "matrix-bot-sdk"; +import { logMessage } from "./LogProxy"; +import config from "./config"; export function setToArray(set: Set): T[] { const arr: T[] = []; @@ -35,6 +37,22 @@ export function isTrueJoinEvent(event: any): boolean { return membership === 'join' && prevMembership !== "join"; } +export async function redactUserMessagesIn(client: MatrixClient, userIdOrGlob: string, targetRoomIds: string[]) { + for (const targetRoomId of targetRoomIds) { + await logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Fetching sent messages for ${userIdOrGlob} in ${targetRoomId} to redact...`); + + const eventsToRedact = await getMessagesByUserSinceLastJoin(client, userIdOrGlob, targetRoomId); + for (const victimEvent of eventsToRedact) { + await logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Redacting ${victimEvent['event_id']} in ${targetRoomId}`); + if (!config.noop) { + await client.redactEvent(targetRoomId, victimEvent['event_id']); + } else { + await logMessage(LogLevel.WARN, "utils#redactUserMessagesIn", `Tried to redact ${victimEvent['event_id']} in ${targetRoomId} but Mjolnir is running in no-op mode`); + } + } + } +} + /** * Gets all the events sent by a user (or users if using wildcards) in a given room ID, since * the time they joined.