diff --git a/package.json b/package.json index 53de23a..8194d31 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "jsdom": "^24.0.0", "matrix-appservice-bridge": "^10.3.1", "matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.7.1-element.6", - "matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@3.6.2", + "matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@3.7.1", "matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@3.6.6", "pg": "^8.8.0", "yaml": "^2.3.2" diff --git a/src/appservice/AppService.ts b/src/appservice/AppService.ts index 8c74148..151adb0 100644 --- a/src/appservice/AppService.ts +++ b/src/appservice/AppService.ts @@ -382,6 +382,7 @@ export class MjolnirAppService { await this.bridge.close(); await this.dataStore.close(); await this.api.close(); + this.draupnirManager.unregisterListeners(); } /** diff --git a/src/appservice/AppServiceDraupnirManager.ts b/src/appservice/AppServiceDraupnirManager.ts index ad13d89..c29b650 100644 --- a/src/appservice/AppServiceDraupnirManager.ts +++ b/src/appservice/AppServiceDraupnirManager.ts @@ -83,6 +83,10 @@ export class AppServiceDraupnirManager { return `@${mjolnirRecord.local_part}:${this.serverName}` as StringUserID; } + public unregisterListeners(): void { + this.baseManager.unregisterListeners(); + } + /** * Create the draupnir manager from the datastore and the access control. * @param dataStore The data store interface that has the details for provisioned draupnirs. diff --git a/src/draupnirfactory/DraupnirProtectedRoomsSet.ts b/src/draupnirfactory/DraupnirProtectedRoomsSet.ts index 498605d..070fc22 100644 --- a/src/draupnirfactory/DraupnirProtectedRoomsSet.ts +++ b/src/draupnirfactory/DraupnirProtectedRoomsSet.ts @@ -11,6 +11,7 @@ import { ActionResult, ClientPlatform, + DefaultMixinExtractor, LoggableConfigTracker, Logger, MJOLNIR_PROTECTED_ROOMS_EVENT_TYPE, @@ -219,6 +220,7 @@ export async function makeProtectedRoomsSet( protectedRoomsManager.ok, protectionsConfig.ok, userID, + DefaultMixinExtractor, makeHandleMissingProtectionPermissions( clientPlatform.toRoomMessageSender(), managementRoom.toRoomIDOrAlias() diff --git a/src/draupnirfactory/StandardDraupnirManager.ts b/src/draupnirfactory/StandardDraupnirManager.ts index adb17b8..405d4c9 100644 --- a/src/draupnirfactory/StandardDraupnirManager.ts +++ b/src/draupnirfactory/StandardDraupnirManager.ts @@ -42,6 +42,15 @@ export class StandardDraupnirManager { // nothing to do. } + public unregisterListeners(): void { + for (const [, draupnir] of this.draupnir) { + draupnir.stop(); + } + for (const [, safeModeDraupnir] of this.safeModeDraupnir) { + safeModeDraupnir.stop(); + } + } + public makeSafeModeToggle( clientUserID: StringUserID, managementRoom: MatrixRoomID, @@ -52,6 +61,7 @@ export class StandardDraupnirManager { const draupnirManager = this; const toggle: SafeModeToggle = Object.freeze({ async switchToSafeMode(cause: SafeModeCause) { + draupnirManager.stopDraupnir(clientUserID); return draupnirManager.makeSafeModeDraupnir( clientUserID, managementRoom, @@ -60,6 +70,7 @@ export class StandardDraupnirManager { ); }, async switchToDraupnir() { + draupnirManager.stopDraupnir(clientUserID); return draupnirManager.makeDraupnir( clientUserID, managementRoom, @@ -216,12 +227,15 @@ export class StandardDraupnirManager { public stopDraupnir(clientUserID: StringUserID): void { const draupnir = this.draupnir.get(clientUserID); - if (draupnir === undefined) { - return; - } else { + if (draupnir !== undefined) { draupnir.stop(); this.draupnir.delete(clientUserID); } + const safeModeDraupnir = this.safeModeDraupnir.get(clientUserID); + if (safeModeDraupnir) { + safeModeDraupnir.stop(); + this.safeModeDraupnir.delete(clientUserID); + } } } diff --git a/src/protections/DefaultEnabledProtectionsMigration.ts b/src/protections/DefaultEnabledProtectionsMigration.ts index bff0bed..732ede4 100644 --- a/src/protections/DefaultEnabledProtectionsMigration.ts +++ b/src/protections/DefaultEnabledProtectionsMigration.ts @@ -18,6 +18,7 @@ import { RedactionSynchronisationProtection } from "./RedactionSynchronisation"; import { PolicyChangeNotification } from "./PolicyChangeNotification"; import { JoinRoomsOnInviteProtection } from "./invitation/JoinRoomsOnInviteProtection"; import { RoomsSetBehaviour } from "./ProtectedRooms/RoomsSetBehaviourProtection"; +import { InvalidEventProtection } from "./InvalidEventProtection"; export const DefaultEnabledProtectionsMigration = new SchemedDataManager([ @@ -165,4 +166,25 @@ export const DefaultEnabledProtectionsMigration = [DRAUPNIR_SCHEMA_VERSION_KEY]: toVersion, }); }, + async function enableInvalidEventProtection(input, toVersion) { + if (!Value.Check(MjolnirEnabledProtectionsEvent, input)) { + return ActionError.Result( + `The data for ${MjolnirEnabledProtectionsEventType} is corrupted.` + ); + } + const enabledProtections = new Set(input.enabled); + const protection = findProtection(InvalidEventProtection.name); + if (protection === undefined) { + const message = `Cannot find the ${RoomsSetBehaviour.name} protection`; + return ActionException.Result(message, { + exception: new TypeError(message), + exceptionKind: ActionExceptionKind.Unknown, + }); + } + enabledProtections.add(protection.name); + return Ok({ + enabled: [...enabledProtections], + [DRAUPNIR_SCHEMA_VERSION_KEY]: toVersion, + }); + }, ]); diff --git a/src/protections/DraupnirProtectionsIndex.ts b/src/protections/DraupnirProtectionsIndex.ts index ac14907..0a06249 100644 --- a/src/protections/DraupnirProtectionsIndex.ts +++ b/src/protections/DraupnirProtectionsIndex.ts @@ -16,6 +16,7 @@ import "./BanPropagation"; import "./BasicFlooding"; import "./FirstMessageIsImage"; import "./HomeserverUserPolicyApplication/HomeserverUserPolicyProtection"; +import "./InvalidEventProtection"; import "./JoinWaveShortCircuit"; import "./RedactionSynchronisation"; import "./MembershipChangeProtection"; diff --git a/src/protections/InvalidEventProtection.tsx b/src/protections/InvalidEventProtection.tsx new file mode 100644 index 0000000..1245b41 --- /dev/null +++ b/src/protections/InvalidEventProtection.tsx @@ -0,0 +1,214 @@ +// Copyright 2025 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { Type } from "@sinclair/typebox"; +import { + AbstractProtection, + describeProtection, + EDStatic, + EventConsequences, + EventWithMixins, + isError, + Logger, + Ok, + ProtectedRoomsSet, + Protection, + ProtectionDescription, + RoomMessageSender, + Task, + UserConsequences, +} from "matrix-protection-suite"; +import { Draupnir } from "../Draupnir"; +import { + MatrixRoomID, + StringRoomID, + StringUserID, +} from "@the-draupnir-project/matrix-basic-types"; +import { LazyLeakyBucket } from "../queues/LeakyBucket"; +import { DeadDocumentJSX } from "@the-draupnir-project/interface-manager"; +import { + renderMentionPill, + sendMatrixEventsFromDeadDocument, +} from "@the-draupnir-project/mps-interface-adaptor"; + +const log = new Logger("InvalidEventProtection"); + +const InvalidEventProtectionSettings = Type.Object( + { + warningText: Type.String({ + description: + "The reason to use to notify the user after redacting their infringing message.", + default: + "You have sent an invalid event that could cause problems in some Matrix clients, so we have had to redact it.", + }), + }, + { + title: "InvalidEventProtectionSettings", + } +); + +type InvalidEventProtectionSettings = EDStatic< + typeof InvalidEventProtectionSettings +>; + +export type InvalidEventProtectionDescription = ProtectionDescription< + unknown, + typeof InvalidEventProtectionSettings, + InvalidEventProtectionCapabilities +>; + +export type InvalidEventProtectionCapabilities = { + eventConsequences: EventConsequences; + userConsequences: UserConsequences; +}; + +export class InvalidEventProtection + extends AbstractProtection + implements Protection +{ + private readonly eventConsequences: EventConsequences; + private readonly userConsequences: UserConsequences; + private readonly consequenceBucket = new LazyLeakyBucket( + 1, + 30 * 60_000 // half an hour will do + ); + public constructor( + description: InvalidEventProtectionDescription, + capabilities: InvalidEventProtectionCapabilities, + protectedRoomsSet: ProtectedRoomsSet, + private readonly warningText: string, + private readonly roomMessageSender: RoomMessageSender, + private readonly managentRoomID: StringRoomID + ) { + super(description, capabilities, protectedRoomsSet, {}); + this.eventConsequences = capabilities.eventConsequences; + this.userConsequences = capabilities.userConsequences; + } + + private async redactEventWithMixin(event: EventWithMixins): Promise { + const redactResult = await this.eventConsequences.consequenceForEvent( + event.sourceEvent.room_id, + event.sourceEvent.event_id, + "invalid event mixin" + ); + if (isError(redactResult)) { + log.error( + `Failed to redact and event sent by ${event.sourceEvent.sender} in ${event.sourceEvent.room_id}`, + redactResult.error + ); + } + const managementRoomSendResult = await sendMatrixEventsFromDeadDocument( + this.roomMessageSender, + this.managentRoomID, + +
+ + Copy of invalid event content from {event.sourceEvent.sender} + +
{JSON.stringify(event.sourceEvent.content)}
+
+
, + {} + ); + if (isError(managementRoomSendResult)) { + log.error( + "Failed to send redacted event details to the management room", + managementRoomSendResult.error + ); + } + } + + private async sendWarning(event: EventWithMixins): Promise { + const result = await sendMatrixEventsFromDeadDocument( + this.roomMessageSender, + event.sourceEvent.room_id, + + {renderMentionPill(event.sourceEvent.sender, event.sourceEvent.sender)}{" "} + {this.warningText} + , + { replyToEvent: event.sourceEvent } + ); + if (isError(result)) { + log.error( + "Unable to warn the user", + event.sourceEvent.sender, + result.error + ); + } + } + + private async banUser(event: EventWithMixins): Promise { + const banResult = await this.userConsequences.consequenceForUserInRoom( + event.sourceEvent.room_id, + event.sourceEvent.sender, + "Sending invalid events" + ); + if (isError(banResult)) { + log.error( + "Unable to ban the sender of invalid events", + event.sourceEvent.sender, + event.sourceEvent.room_id, + banResult.error + ); + } + } + + public handleProtectionDisable(): void { + this.consequenceBucket.stop(); + } + + public handleTimelineEventMixins( + _room: MatrixRoomID, + event: EventWithMixins + ): void { + if (!event.mixins.some((mixin) => mixin.isErroneous)) { + return; + } + const infractions = this.consequenceBucket.getTokenCount( + event.sourceEvent.sender + ); + if (infractions > 0) { + void Task(this.banUser(event), { log }); + } else { + void Task(this.sendWarning(event), { log }); + } + this.consequenceBucket.addToken(event.sourceEvent.sender); + void Task(this.redactEventWithMixin(event), { log }); + } +} + +describeProtection< + InvalidEventProtectionCapabilities, + Draupnir, + typeof InvalidEventProtectionSettings +>({ + name: "InvalidEventProtection", + description: `Protect the room against malicious events or evasion of other protections.`, + capabilityInterfaces: { + eventConsequences: "EventConsequences", + userConsequences: "UserConsequences", + }, + defaultCapabilities: { + eventConsequences: "StandardEventConsequences", + userConsequences: "StandardUserConsequences", + }, + configSchema: InvalidEventProtectionSettings, + factory: async ( + decription, + protectedRoomsSet, + draupnir, + capabilitySet, + settings + ) => + Ok( + new InvalidEventProtection( + decription, + capabilitySet, + protectedRoomsSet, + settings.warningText, + draupnir.clientPlatform.toRoomMessageSender(), + draupnir.managementRoomID + ) + ), +}); diff --git a/src/protections/MentionLimitProtection.tsx b/src/protections/MentionLimitProtection.tsx index ff4b30d..f3e5be2 100644 --- a/src/protections/MentionLimitProtection.tsx +++ b/src/protections/MentionLimitProtection.tsx @@ -5,19 +5,22 @@ import { AbstractProtection, - ActionResult, + ContentMixins, EDStatic, EventConsequences, + EventWithMixins, Logger, + MentionsMixin, + MentionsMixinDescription, + NewContentMixinDescription, Ok, ProtectedRoomsSet, Protection, ProtectionDescription, - RoomEvent, + RoomMessageBodyMixinDescription, RoomMessageSender, Task, UserConsequences, - Value, describeProtection, isError, } from "matrix-protection-suite"; @@ -33,52 +36,61 @@ import { renderMentionPill, sendMatrixEventsFromDeadDocument, } from "@the-draupnir-project/mps-interface-adaptor"; +import { Result } from "@gnuxie/typescript-result"; const log = new Logger("MentionLimitProtection"); -const MentionsContentSchema = Type.Object({ - "m.mentions": Type.Object({ - user_ids: Type.Array(Type.String()), - }), -}); +function isMentionsMixinOverLimit( + mentionsMixin: MentionsMixin, + maxMentions: number +): boolean { + return mentionsMixin.user_ids.length > maxMentions; +} -const NewContentMentionsSchema = Type.Object({ - "m.new_content": MentionsContentSchema, -}); - -const WeakTextContentSchema = Type.Object({ - body: Type.Optional(Type.String()), - formatted_body: Type.Optional(Type.String()), -}); - -export function isContainingMentionsOverLimit( - event: RoomEvent, +function isContentContaningMentionsOverLimit( + content: ContentMixins, maxMentions: number, checkBody: boolean ): boolean { - const isOverLimit = (user_ids: string[]): boolean => - user_ids.length > maxMentions; + const mentionMixin = content.findMixin(MentionsMixinDescription); if ( - Value.Check(NewContentMentionsSchema, event.content) && - isOverLimit(event.content["m.new_content"]["m.mentions"].user_ids) + mentionMixin?.isErroneous === false && + isMentionsMixinOverLimit(mentionMixin, maxMentions) ) { return true; } - if ( - Value.Check(MentionsContentSchema, event.content) && - isOverLimit(event.content["m.mentions"].user_ids) - ) { + if (!checkBody) { + return false; + } + const bodyMixin = content.findMixin(RoomMessageBodyMixinDescription); + if (bodyMixin === undefined || bodyMixin.isErroneous) { + return false; + } + return bodyMixin.body.split("@").length - 1 > maxMentions; +} + +export function isContainingMentionsOverLimit( + event: EventWithMixins, + maxMentions: number, + checkBody: boolean +): boolean { + const isTopContentOverLimit = isContentContaningMentionsOverLimit( + event, + maxMentions, + checkBody + ); + if (isTopContentOverLimit) { return true; } - if (checkBody && Value.Check(WeakTextContentSchema, event.content)) { - if ( - event.content.body !== undefined && - event.content.body.split("@").length - 1 > maxMentions - ) { - return true; - } + const newContentMixin = event.findMixin(NewContentMixinDescription); + if (newContentMixin === undefined || newContentMixin.isErroneous) { + return false; } - return false; + return isContentContaningMentionsOverLimit( + newContentMixin, + maxMentions, + checkBody + ); } const MentionLimitProtectionSettings = Type.Object( @@ -141,12 +153,12 @@ export class MentionLimitProtection this.warningText = settings.warningText; this.includeLegacymentions = settings.includeLegacyMentions; } - public async handleTimelineEvent( + public handleTimelineEventMixins( _room: MatrixRoomID, - event: RoomEvent - ): Promise> { - if (event.sender === this.protectedRoomsSet.userID) { - return Ok(undefined); + event: EventWithMixins + ): void { + if (event.sourceEvent.sender === this.protectedRoomsSet.userID) { + return; } if ( isContainingMentionsOverLimit( @@ -155,42 +167,60 @@ export class MentionLimitProtection this.includeLegacymentions ) ) { - const infractions = this.consequenceBucket.getTokenCount(event.sender); - if (infractions > 0) { - const userResult = await this.userConsequences.consequenceForUserInRoom( - event.room_id, - event.sender, - this.warningText - ); - if (isError(userResult)) { - log.error("Failed to ban the user", event.sender, userResult.error); - } - // fall through to the event consequence on purpose so we redact the event too. - } else { - // if they're not being banned we need to tell them why their message got redacted. - void Task( - sendMatrixEventsFromDeadDocument( - this.roomMessageSender, - event.room_id, - - {renderMentionPill(event.sender, event.sender)} {this.warningText} - , - { replyToEvent: event } - ), - { - log, - } - ); - } - this.consequenceBucket.addToken(event.sender); - return await this.eventConsequences.consequenceForEvent( - event.room_id, - event.event_id, + void Task(this.handleEventOverLimit(event), { + log, + }); + } + } + + public async handleEventOverLimit( + event: EventWithMixins + ): Promise> { + const sourceEvent = event.sourceEvent; + const infractions = this.consequenceBucket.getTokenCount( + sourceEvent.sender + ); + if (infractions > 0) { + const userResult = await this.userConsequences.consequenceForUserInRoom( + sourceEvent.room_id, + sourceEvent.sender, this.warningText ); + if (isError(userResult)) { + log.error( + "Failed to ban the user", + sourceEvent.sender, + userResult.error + ); + } + // fall through to the event consequence on purpose so we redact the event too. } else { - return Ok(undefined); + // if they're not being banned we need to tell them why their message got redacted. + void Task( + sendMatrixEventsFromDeadDocument( + this.roomMessageSender, + sourceEvent.room_id, + + {renderMentionPill(sourceEvent.sender, sourceEvent.sender)}{" "} + {this.warningText} + , + { replyToEvent: sourceEvent } + ), + { + log, + } + ); } + this.consequenceBucket.addToken(sourceEvent.sender); + return await this.eventConsequences.consequenceForEvent( + sourceEvent.room_id, + sourceEvent.event_id, + this.warningText + ); + } + + handleProtectionDisable(): void { + this.consequenceBucket.stop(); } } diff --git a/src/protections/WordList.ts b/src/protections/WordList.ts index afec55c..b3cfb70 100644 --- a/src/protections/WordList.ts +++ b/src/protections/WordList.ts @@ -12,6 +12,8 @@ import { AbstractProtection, ActionResult, EventConsequences, + EventWithMixins, + ExtensibleTextMixinDescription, Logger, MembershipChange, MembershipChangeType, @@ -19,12 +21,12 @@ import { ProtectedRoomsSet, Protection, ProtectionDescription, - RoomEvent, RoomMembershipRevision, - RoomMessage, + RoomMessageBodyMixinDescription, + RoomMessageFormattedBodyMixinDescription, + Task, UnknownConfig, UserConsequences, - Value, describeProtection, isError, } from "matrix-protection-suite"; @@ -55,7 +57,8 @@ describeProtection({ name: "WordListProtection", description: "If a user posts a monitored word a set amount of time after joining, they\ - will be banned from that room. This will not publish the ban to a ban list.", + will be banned from that room. This will not publish the ban to a ban list.\ + This protection only targets recently joined users.", capabilityInterfaces: { userConsequences: "UserConsequences", eventConsequences: "EventConsequences", @@ -90,7 +93,7 @@ export class WordListProtection implements Protection { private justJoined: JustJoinedByRoom = new Map(); - private badWords?: RegExp; + private badWords: RegExp; private readonly userConsequences: UserConsequences; private readonly eventConsequences: EventConsequences; @@ -103,6 +106,15 @@ export class WordListProtection super(description, capabilities, protectedRoomsSet, {}); this.userConsequences = capabilities.userConsequences; this.eventConsequences = capabilities.eventConsequences; + // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + const escapeRegExp = (string: string) => { + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + }; + // Create a mega-regex from all the tiny words. + const words = this.draupnir.config.protections.wordlist.words + .filter((word) => word.length !== 0) + .map(escapeRegExp); + this.badWords = new RegExp(words.join("|"), "i"); } public async handleMembershipChange( revision: RoomMembershipRevision, @@ -136,90 +148,93 @@ export class WordListProtection return Ok(undefined); } - public async handleTimelineEvent( + public handleTimelineEventMixins( room: MatrixRoomID, - event: RoomEvent - ): Promise> { + event: EventWithMixins + ): void { // If the sender is draupnir, ignore the message - if (event["sender"] === this.draupnir.clientUserID) { - log.debug(`Ignoring message from self: ${event.event_id}`); - return Ok(undefined); + if (event.sourceEvent["sender"] === this.draupnir.clientUserID) { + log.debug(`Ignoring message from self: ${event.sourceEvent.event_id}`); + return; + } + const bodies = [ + ((mixin) => (mixin?.isErroneous === true ? undefined : mixin?.body))( + event.findMixin(RoomMessageBodyMixinDescription) + ), + ((mixin) => (mixin?.isErroneous ? undefined : mixin?.formatted_body))( + event.findMixin(RoomMessageFormattedBodyMixinDescription) + ), + ...(((mixin) => + mixin?.isErroneous + ? undefined + : mixin?.representations.map( + (representation) => representation.body + ))(event.findMixin(ExtensibleTextMixinDescription)) ?? []), + ].filter((mixin) => mixin !== undefined); + if (bodies.length === 0) { + return; } const minsBeforeTrusting = this.draupnir.config.protections.wordlist.minutesBeforeTrusting; - if (Value.Check(RoomMessage, event)) { - if (!("msgtype" in event.content)) { - return Ok(undefined); - } - const message = - ("formatted_body" in event.content && - event.content["formatted_body"]) || - event.content["body"]; - const roomID = room.toRoomIDOrAlias(); + const roomID = room.toRoomIDOrAlias(); + // Check conditions first + if (minsBeforeTrusting > 0) { + const roomEntry = this.justJoined.get(roomID); + const joinTime = roomEntry?.get(event.sourceEvent["sender"]); + if (joinTime !== undefined) { + // Disregard if the user isn't recently joined - // Check conditions first - if (minsBeforeTrusting > 0) { - const roomEntry = this.justJoined.get(roomID); - const joinTime = roomEntry?.get(event["sender"]); - if (joinTime !== undefined) { - // Disregard if the user isn't recently joined - - // Check if they did join recently, was it within the timeframe - const now = new Date(); - if ( - now.valueOf() - joinTime.valueOf() > - minsBeforeTrusting * 60 * 1000 - ) { - roomEntry?.delete(event["sender"]); // Remove the user - log.info(`${event["sender"]} is no longer considered suspect`); - return Ok(undefined); - } - } else { - // The user isn't in the recently joined users list, no need to keep - // looking - return Ok(undefined); + // Check if they did join recently, was it within the timeframe + const now = new Date(); + if ( + now.valueOf() - joinTime.valueOf() > + minsBeforeTrusting * 60 * 1000 + ) { + roomEntry?.delete(event.sourceEvent["sender"]); // Remove the user + log.info( + `${event.sourceEvent["sender"]} is no longer considered suspect` + ); + return; } + } else { + // The user isn't in the recently joined users list, no need to keep + // looking + return; } - if (!this.badWords) { - // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping - const escapeRegExp = (string: string) => { - return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - }; - - // Create a mega-regex from all the tiny words. - const words = this.draupnir.config.protections.wordlist.words - .filter((word) => word.length !== 0) - .map(escapeRegExp); - this.badWords = new RegExp(words.join("|"), "i"); - } - - const match = this.badWords.exec(message); - if (match) { - const reason = `Said a bad word. Moderators, consult the management room for more information.`; + } + const match = bodies.find((body) => this.badWords.exec(body)); + if (!match) { + return; + } + const reason = `Said a bad word. Moderators, consult the management room for more information.`; + void Task( + (async () => { await this.userConsequences.consequenceForUserInRoom( roomID, - event.sender, + event.sourceEvent.sender, reason ); const messageResult = await this.draupnir.client .sendMessage(this.draupnir.managementRoomID, { msgtype: "m.notice", - body: `Banned ${event.sender} in ${roomID} for saying '${match[0]}'.`, + body: `Banned ${event.sourceEvent.sender} in ${roomID} for saying '${match[0]}'.`, }) .then((_) => Ok(undefined), resultifyBotSDKRequestError); if (isError(messageResult)) { log.error( - `Failed to send a message to the management room after banning ${event.sender} in ${roomID} for saying '${match[0]}'.`, + `Failed to send a message to the management room after banning ${event.sourceEvent.sender} in ${roomID} for saying '${match[0]}'.`, messageResult.error ); } await this.eventConsequences.consequenceForEvent( roomID, - event.event_id, + event.sourceEvent.event_id, reason ); + })(), + { + log, } - } - return Ok(undefined); + ); } } diff --git a/test/unit/protections/MentionLimitProtectionTest.ts b/test/unit/protections/MentionLimitProtectionTest.ts index f63e4f6..595d1c8 100644 --- a/test/unit/protections/MentionLimitProtectionTest.ts +++ b/test/unit/protections/MentionLimitProtectionTest.ts @@ -3,7 +3,13 @@ // // SPDX-License-Identifier: Apache-2.0 -import { RoomEvent } from "matrix-protection-suite"; +import { + DefaultMixinExtractor, + EventWithMixins, + randomRoomID, + randomUserID, + RoomEvent, +} from "matrix-protection-suite"; import { isContainingMentionsOverLimit } from "../../../src/protections/MentionLimitProtection"; import expect from "expect"; @@ -11,8 +17,13 @@ function messageEvent(content: { body?: string; formatted_body?: string; "m.mentions"?: { user_ids: string[] }; -}): RoomEvent { - return { content } as RoomEvent; +}): EventWithMixins { + return DefaultMixinExtractor.parseEvent({ + content, + type: "m.room.message", + sender: randomUserID(), + room_id: randomRoomID([]).toRoomIDOrAlias(), + } as RoomEvent); } describe("MentionLimitProtection test", function () { diff --git a/yarn.lock b/yarn.lock index 91f8bdb..7e406df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2606,10 +2606,10 @@ matrix-appservice@^2.0.0: "@gnuxie/typescript-result" "^1.0.0" await-lock "^2.2.2" -"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@3.6.2": - version "3.6.2" - resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-3.6.2.tgz#7d62433d859156ab1bd288c2813c94d4e04c2de7" - integrity sha512-isXsyZn5EH4V2tyhYx1Mf+L5GfQC7/AdBxoZc7fk5hCsEjWaKj6VxpNoSmhx0Ev3F7zr9I52vRN7hNutumwP6g== +"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-3.7.1.tgz#0f5e128bb05a087b0f144f5466e648c89d88a50d" + integrity sha512-awSsgCqcWtPfNKOHM76Sb5w8UFpB1JDSy5vI5GXWOL2tFl7kXdZ9QXc0ohPC31+jXsKZgXOBaGMY3z830p+DVw== dependencies: "@gnuxie/typescript-result" "^1.0.0" await-lock "^2.2.2"