From d1950fbb5419d0a297bbee8baed306e3cd980b78 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Mon, 13 May 2024 16:23:26 +0100 Subject: [PATCH 01/12] Update for new MPS handleExternalInvite handle. --- package.json | 4 ++-- src/Draupnir.ts | 7 +++++-- yarn.lock | 18 +++++++++--------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 34a254bc..d7e2dd61 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,8 @@ "js-yaml": "^4.1.0", "jsdom": "^24.0.0", "matrix-appservice-bridge": "^9.0.1", - "matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@0.20.0", - "matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@0.20.0", + "matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@0.21.0", + "matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@0.21.0", "parse-duration": "^1.0.2", "pg": "^8.8.0", "shell-quote": "^1.7.3", diff --git a/src/Draupnir.ts b/src/Draupnir.ts index 040ac520..5b7150e7 100644 --- a/src/Draupnir.ts +++ b/src/Draupnir.ts @@ -199,7 +199,10 @@ export class Draupnir implements Client { } public handleTimelineEvent(roomID: StringRoomID, event: RoomEvent): void { - Task(this.joinOnInviteListener(roomID, event)); + if (Value.Check(MembershipEvent, event) && event.content.membership === Membership.Invite && event.state_key === this.clientUserID) { + this.protectedRoomsSet.handleExternalInvite(roomID, event); + Task(this.joinOnInviteListener(roomID, event)); + } this.managementRoomMessageListener(roomID, event); this.reactionHandler.handleEvent(roomID, event); if (this.protectedRoomsSet.isProtectedRoom(roomID)) { @@ -250,7 +253,7 @@ export class Draupnir implements Client { * @param {boolean} options.autojoinOnlyIfManager Whether to only accept an invitation by a user present in the `managementRoom`. * @param {string} options.acceptInvitesFromSpace A space of users to accept invites from, ignores invites form users not in this space. */ - private async joinOnInviteListener(roomID: StringRoomID, event: RoomEvent): Promise { + private async joinOnInviteListener(roomID: StringRoomID, event: MembershipEvent): Promise { if (Value.Check(MembershipEvent, event) && event.state_key === this.clientUserID) { const inviteEvent = event; const reportInvite = async () => { diff --git a/yarn.lock b/yarn.lock index 7264e8d4..dbddfc48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2505,15 +2505,15 @@ matrix-appservice@^2.0.0: request-promise "^4.2.6" sanitize-html "^2.8.0" -"matrix-protection-suite-for-matrix-bot-sdk@npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite-for-matrix-bot-sdk/-/matrix-protection-suite-for-matrix-bot-sdk-0.20.0.tgz#c103fc6f66c14606c669f6e71de8049acf504b2b" - integrity sha512-HA85a64+2TqvgSY2us8wKBb7NoeAoeLBiIMf8btBVCJXppED+oiPxSdwsSy6nZmcWxgNUt1SEvlmhlUCm5EJGg== - -"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-0.20.0.tgz#a1ed3a7a2c1ac9bcda870d2c035f165ff2fe376d" - integrity sha512-CHGhyVfmG7Jn8SsxYlvqfFxs5Fyhv0fCP3RVht5pzhErYlvjsdphBRNrkzmq87kNBOJo9kGQ8HWFhxOaU5GCkA== +"matrix-protection-suite-for-matrix-bot-sdk@npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@0.21.0": + version "0.21.0" + resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite-for-matrix-bot-sdk/-/matrix-protection-suite-for-matrix-bot-sdk-0.21.0.tgz#2b03a1409e01508635d31c9e0f7d3650d48467de" + integrity sha512-wRcNYrY8gDxX6ofPjRpE5DI+i+X0q5o2VqFXXYA2TSiYCues0B26DZeRH5qcw8pmMM+voCGSPurLD7RFnm1a8Q== + +"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@0.21.0": + version "0.21.0" + resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-0.21.0.tgz#9f4983cf1a2a6583f7d9ee6ea84fc4428f9add6b" + integrity sha512-hIZpkIv0bEl5aKMv/AzK9Lcbg6CD3lw5VDYQmv04pM2/ja4z5nCsxD9rWeEUl+1IP+i0RjFWLNnbhl8n9nPsnA== dependencies: await-lock "^2.2.2" crypto-js "^4.2.0" From 1041105aa8c3f587df2f6ae6d70bfda3dc633aa3 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Tue, 14 May 2024 23:42:35 +0100 Subject: [PATCH 02/12] ProtectRoomsOnInviteProtection. Currently creates two prompts sometimes for some reason. We also need a way to cancel prompts see https://github.com/the-draupnir-project/Draupnir/issues/423. Especially once we have finished protecting a room, it doesn't make sense for them to keep clicking. --- src/Draupnir.ts | 109 ++++------ .../interface-manager/MatrixHelpRenderer.tsx | 64 +++++- .../MatrixReactionHandler.ts | 7 +- .../DefaultEnabledProtectionsMigration.ts | 22 ++ .../ProtectRoomsOnInviteProtection.tsx | 193 ++++++++++++++++++ src/protections/invitation/inviteCore.ts | 21 ++ 6 files changed, 333 insertions(+), 83 deletions(-) create mode 100644 src/protections/invitation/ProtectRoomsOnInviteProtection.tsx create mode 100644 src/protections/invitation/inviteCore.ts diff --git a/src/Draupnir.ts b/src/Draupnir.ts index 5b7150e7..f4aa141a 100644 --- a/src/Draupnir.ts +++ b/src/Draupnir.ts @@ -25,7 +25,7 @@ limitations under the License. * are NOT distributed, contributed, committed, or licensed under the Apache License. */ -import { ActionResult, Client, ClientPlatform, ClientRooms, EventReport, LoggableConfigTracker, Logger, MatrixRoomID, MatrixRoomReference, Membership, MembershipEvent, Ok, PolicyRoomManager, ProtectedRoomsSet, RoomEvent, RoomMembershipManager, RoomMessage, RoomStateManager, StringRoomID, StringUserID, Task, TextMessageContent, Value, isError, isStringRoomAlias, isStringRoomID, serverName, userLocalpart } from "matrix-protection-suite"; +import { ActionResult, Client, ClientPlatform, ClientRooms, EventReport, LoggableConfigTracker, Logger, MatrixRoomID, MatrixRoomReference, MembershipEvent, Ok, PolicyRoomManager, ProtectedRoomsSet, RoomEvent, RoomMembershipManager, RoomMembershipRevisionIssuer, RoomMessage, RoomStateManager, StringRoomID, StringUserID, Task, TextMessageContent, Value, isError, isStringRoomAlias, isStringRoomID, serverName, userLocalpart } from "matrix-protection-suite"; import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue"; import { findCommandTable } from "./commands/interface-manager/InterfaceCommand"; import { ThrottlingQueue } from "./queues/ThrottlingQueue"; @@ -33,10 +33,9 @@ import ManagementRoomOutput from "./ManagementRoomOutput"; import { ReportPoller } from "./report/ReportPoller"; import { ReportManager } from "./report/ReportManager"; import { MatrixReactionHandler } from "./commands/interface-manager/MatrixReactionHandler"; -import { MatrixSendClient, SynapseAdminClient, resolveRoomReferenceSafe } from "matrix-protection-suite-for-matrix-bot-sdk"; +import { MatrixSendClient, SynapseAdminClient } from "matrix-protection-suite-for-matrix-bot-sdk"; import { IConfig } from "./config"; import { COMMAND_PREFIX, DraupnirContext, extractCommandFromMessageBody, handleCommand } from "./commands/CommandHandler"; -import { htmlEscape } from "./utils"; import { LogLevel } from "matrix-bot-sdk"; import { ARGUMENT_PROMPT_LISTENER, DEFAUILT_ARGUMENT_PROMPT_LISTENER, makeListenerForArgumentPrompt as makeListenerForArgumentPrompt, makeListenerForPromptDefault } from "./commands/interface-manager/MatrixPromptForAccept"; import { RendererMessageCollector } from "./capabilities/RendererMessageCollector"; @@ -44,6 +43,7 @@ import { DraupnirRendererMessageCollector } from "./capabilities/DraupnirRendere import { renderProtectionFailedToStart } from "./protections/ProtectedRoomsSetRenderers"; import { draupnirStatusInfo, renderStatusInfo } from "./commands/StatusCommand"; import { renderMatrixAndSend } from "./commands/interface-manager/DeadDocumentMatrix"; +import { isInvitationForUser } from "./protections/invitation/inviteCore"; const log = new Logger('Draupnir'); @@ -97,6 +97,9 @@ export class Draupnir implements Client { public readonly policyRoomManager: PolicyRoomManager, public readonly roomMembershipManager: RoomMembershipManager, public readonly loggableConfigTracker: LoggableConfigTracker, + /** Mjolnir has a feature where you can choose to accept invitations from a space and not just the management room. */ + public readonly acceptInvitesFromRoom: MatrixRoomID, + public readonly acceptInvitesFromRoomIssuer: RoomMembershipRevisionIssuer, public readonly synapseAdminClient?: SynapseAdminClient, ) { this.managementRoomID = this.managementRoom.toRoomIDOrAlias(); @@ -144,6 +147,34 @@ export class Draupnir implements Client { config: IConfig, loggableConfigTracker: LoggableConfigTracker ): Promise> { + const acceptInvitesFromRoom = await (async () => { + if (config.autojoinOnlyIfManager) { + return Ok(managementRoom) + } else { + if (config.acceptInvitesFromSpace === undefined) { + throw new TypeError(`You cannot leave config.acceptInvitesFromSpace undefined if you have disabled config.autojoinOnlyIfManager`); + } + const room = (() => { + if (isStringRoomID(config.acceptInvitesFromSpace) || isStringRoomAlias(config.acceptInvitesFromSpace)) { + return config.acceptInvitesFromSpace; + } else { + const parseResult = MatrixRoomReference.fromPermalink(config.acceptInvitesFromSpace); + if (isError(parseResult)) { + throw new TypeError(`config.acceptInvitesFromSpace: ${config.acceptInvitesFromSpace} needs to be a room id, alias or permalink`); + } + return parseResult.ok; + } + })(); + return await clientPlatform.toRoomJoiner().joinRoom(room); + } + })(); + if (isError(acceptInvitesFromRoom)) { + return acceptInvitesFromRoom; + } + const acceptInvitesFromRoomIssuer = await roomMembershipManager.getRoomMembershipRevisionIssuer(acceptInvitesFromRoom.ok); + if (isError(acceptInvitesFromRoomIssuer)) { + return acceptInvitesFromRoomIssuer; + } const draupnir = new Draupnir( client, clientUserID, @@ -156,6 +187,8 @@ export class Draupnir implements Client { policyRoomManager, roomMembershipManager, loggableConfigTracker, + acceptInvitesFromRoom.ok, + acceptInvitesFromRoomIssuer.ok, new SynapseAdminClient( client, clientUserID @@ -199,9 +232,8 @@ export class Draupnir implements Client { } public handleTimelineEvent(roomID: StringRoomID, event: RoomEvent): void { - if (Value.Check(MembershipEvent, event) && event.content.membership === Membership.Invite && event.state_key === this.clientUserID) { + if (Value.Check(MembershipEvent, event) && isInvitationForUser(event, this.clientUserID)) { this.protectedRoomsSet.handleExternalInvite(roomID, event); - Task(this.joinOnInviteListener(roomID, event)); } this.managementRoomMessageListener(roomID, event); this.reactionHandler.handleEvent(roomID, event); @@ -242,73 +274,6 @@ export class Draupnir implements Client { this.reportManager.handleTimelineEvent(roomID, event); } - /** - * Adds a listener to the client that will automatically accept invitations. - * FIXME: This is just copied in from Mjolnir and there are plenty of places for uncaught exceptions that will cause havok. - * FIXME: MOVE TO A PROTECTION. - * @param {MatrixSendClient} client - * @param options By default accepts invites from anyone. - * @param {string} options.managementRoom The room to report ignored invitations to if `recordIgnoredInvites` is true. - * @param {boolean} options.recordIgnoredInvites Whether to report invites that will be ignored to the `managementRoom`. - * @param {boolean} options.autojoinOnlyIfManager Whether to only accept an invitation by a user present in the `managementRoom`. - * @param {string} options.acceptInvitesFromSpace A space of users to accept invites from, ignores invites form users not in this space. - */ - private async joinOnInviteListener(roomID: StringRoomID, event: MembershipEvent): Promise { - if (Value.Check(MembershipEvent, event) && event.state_key === this.clientUserID) { - const inviteEvent = event; - const reportInvite = async () => { - if (!this.config.recordIgnoredInvites) return; // Nothing to do - - Task((async () => { - await this.client.sendMessage(this.managementRoomID, { - msgtype: "m.text", - body: `${inviteEvent.sender} has invited me to ${inviteEvent.room_id} but the config prevents me from accepting the invitation. ` - + `If you would like this room protected, use "!mjolnir rooms add ${inviteEvent.room_id}" so I can accept the invite.`, - format: "org.matrix.custom.html", - formatted_body: `${htmlEscape(inviteEvent.sender)} has invited me to ${htmlEscape(inviteEvent.room_id)} but the config prevents me from ` - + `accepting the invitation. If you would like this room protected, use !mjolnir rooms add ${htmlEscape(inviteEvent.room_id)} ` - + `so I can accept the invite.`, - }); - return Ok(undefined); - })()); - }; - - if (this.config.autojoinOnlyIfManager) { - const managementMembership = this.protectedRoomsSet.setMembership.getRevision(this.managementRoomID); - if (managementMembership === undefined) { - throw new TypeError(`Processing an invitation before the protected rooms set has properly initialized. Are we protecting the management room?`); - } - const senderMembership = managementMembership.membershipForUser(inviteEvent.sender); - if (senderMembership?.membership !== Membership.Join) return reportInvite(); // ignore invite - } else { - if (!(isStringRoomID(this.config.acceptInvitesFromSpace) || isStringRoomAlias(this.config.acceptInvitesFromSpace))) { - // FIXME: We need to do StringRoomID stuff at parse time of the config. - throw new TypeError(`${this.config.acceptInvitesFromSpace} is not a valid room ID or Alias`); - } - const spaceReference = MatrixRoomReference.fromRoomIDOrAlias(this.config.acceptInvitesFromSpace); - const spaceID = await resolveRoomReferenceSafe(this.client, spaceReference); - if (isError(spaceID)) { - await this.managementRoomOutput.logMessage(LogLevel.ERROR, 'Draupnir', `Unable to resolve the space ${spaceReference.toPermalink} from config.acceptInvitesFromSpace when trying to accept an invitation from ${inviteEvent.sender}`); - } - const spaceId = await this.client.resolveRoom(this.config.acceptInvitesFromSpace); - const spaceUserIds = await this.client.getJoinedRoomMembers(spaceId) - .catch(async e => { - if (e.body?.errcode === "M_FORBIDDEN") { - await this.managementRoomOutput.logMessage(LogLevel.ERROR, 'Mjolnir', `Mjolnir is not in the space configured for acceptInvitesFromSpace, did you invite it?`); - await this.client.joinRoom(spaceId); - return await this.client.getJoinedRoomMembers(spaceId); - } else { - return Promise.reject(e); - } - }); - if (!spaceUserIds.includes(inviteEvent.sender)) { - return reportInvite(); // ignore invite - } - } - await this.client.joinRoom(roomID); - } - } - /** * Start responding to events. * This will not start the appservice from listening and responding diff --git a/src/commands/interface-manager/MatrixHelpRenderer.tsx b/src/commands/interface-manager/MatrixHelpRenderer.tsx index 4d17715b..fe2938ec 100644 --- a/src/commands/interface-manager/MatrixHelpRenderer.tsx +++ b/src/commands/interface-manager/MatrixHelpRenderer.tsx @@ -10,7 +10,8 @@ import { DocumentNode } from "./DeadDocument"; import { renderMatrixAndSend } from "./DeadDocumentMatrix"; import { LogService } from "matrix-bot-sdk"; import { MatrixSendClient } from "matrix-protection-suite-for-matrix-bot-sdk"; -import { ActionException, ActionResult, MatrixRoomReference, RoomEvent, StringRoomID, isError } from "matrix-protection-suite"; +import { ActionError, ActionException, ActionExceptionKind, ActionResult, MatrixRoomReference, Ok, RoomEvent, StringRoomID, Task, isError, isOk } from "matrix-protection-suite"; +import { renderDetailsNotice, renderElaborationTrail, renderExceptionTrail } from "../../capabilities/CommonRenderers"; function requiredArgument(argumentName: string): string { return `<${argumentName}>`; @@ -88,17 +89,62 @@ export async function renderHelp(client: MatrixSendClient, commandRoomID: String ); } -export const tickCrossRenderer: RendererSignature = async function tickCrossRenderer(this: MatrixInterfaceAdaptor, client: MatrixSendClient, commandRoomID: StringRoomID, event: RoomEvent, result: ActionResult): Promise { - const react = async (emote: string) => { +export async function reactToEventWithResult(client: MatrixSendClient, event: RoomEvent, result: ActionResult): Promise> { + // implement this so we can use it in the invitation protection + // then in the invitation protection makes ure we render when the listener fails + // then in the ban propagation protection also do this. + const react = async (emote: string): Promise> => { try { - await client.unstableApis.addReactionToEvent(commandRoomID, event['event_id'], emote); + await client.unstableApis.addReactionToEvent(event.room_id, event.event_id, emote); + return Ok(undefined); } catch (e) { - LogService.error("tickCrossRenderer", "Couldn't react to the event", event['event_id'], e); + return ActionException.Result(`tickCrossRenderer Couldn't react to the event ${event.event_id}`, { + exception: e, + exceptionKind: ActionExceptionKind.Unknown + }); } - } - if (result.isOkay) { - await react('✅') + }; + if (isOk(result)) { + return await react('✅'); } else { + return await react('❌'); + } +} + +export async function replyToEventWithErrorDetails(client: MatrixSendClient, event: RoomEvent, error: ActionError): Promise> { + try { + await renderMatrixAndSend( + +
+ {error.mostRelevantElaboration} + {renderDetailsNotice(error)} + {renderElaborationTrail(error)} + {renderExceptionTrail(error)} +
+
, + event.room_id, + event, + client, + ); + return Ok(undefined); + } catch (e) { + return ActionException.Result(`replyToEventIfError Couldn't send a reply to the event ${event.event_id}`, { + exception: e, + exceptionKind: ActionExceptionKind.Unknown + }); + } +} + +export function renderActionResultToEvent(client: MatrixSendClient, event: RoomEvent, result: ActionResult): void { + if (isError(result)) { + void Task(replyToEventWithErrorDetails(client, event, result.error)); + } + void Task(reactToEventWithResult(client, event, result)); +} + +export const tickCrossRenderer: RendererSignature = async function tickCrossRenderer(this: MatrixInterfaceAdaptor, client: MatrixSendClient, commandRoomID: StringRoomID, event: RoomEvent, result: ActionResult): Promise { + void Task(reactToEventWithResult(client, event, result)); + if (isError(result)) { if (result.error instanceof ArgumentParseError) { await renderMatrixAndSend( renderArgumentParseError(this.interfaceCommand, result.error), @@ -116,8 +162,6 @@ export const tickCrossRenderer: RendererSignature = } else { await client.replyNotice(commandRoomID, event, result.error.message); } - // reacting is way less important than communicating what happened, do it last. - await react('❌'); } } diff --git a/src/commands/interface-manager/MatrixReactionHandler.ts b/src/commands/interface-manager/MatrixReactionHandler.ts index a204401d..2e5cfac4 100644 --- a/src/commands/interface-manager/MatrixReactionHandler.ts +++ b/src/commands/interface-manager/MatrixReactionHandler.ts @@ -19,11 +19,16 @@ export type ReactionListener = ( annotatedEvent: RoomEvent ) => void; +export declare interface MatrixReactionHandlerListeners { + on(eventName: string, listener: ReactionListener): void; + emit(eventName: string, ...args: Parameters): void; +} + /** * A utility that can be associated with an `MatrixEmitter` to listen for * reactions to Matrix Events. The aim is to simplify reaction UX. */ -export class MatrixReactionHandler extends EventEmitter { +export class MatrixReactionHandler extends EventEmitter implements MatrixReactionHandlerListeners { public constructor( /** * The room the handler is for. Cannot be enabled for every room as the diff --git a/src/protections/DefaultEnabledProtectionsMigration.ts b/src/protections/DefaultEnabledProtectionsMigration.ts index 5368d839..cc7ad730 100644 --- a/src/protections/DefaultEnabledProtectionsMigration.ts +++ b/src/protections/DefaultEnabledProtectionsMigration.ts @@ -6,6 +6,7 @@ import { ActionError,ActionException,ActionExceptionKind,DRAUPNIR_SCHEMA_VERSION_KEY, MjolnirEnabledProtectionsEvent, MjolnirEnabledProtectionsEventType, Ok, SchemedDataManager, Value, findProtection } from "matrix-protection-suite"; import { RedactionSynchronisationProtection } from "./RedactionSynchronisation"; import { PolicyChangeNotification } from "./PolicyChangeNotification"; +import { ProtectRoomsOnInviteProtection } from "./invitation/ProtectRoomsOnInviteProtection"; export const DefaultEnabledProtectionsMigration = new SchemedDataManager([ async function enableBanPropagationByDefault(input) { @@ -95,5 +96,26 @@ export const DefaultEnabledProtectionsMigration = new SchemedDataManager +// +// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0 +// +// SPDX-FileAttributionText: +// This modified file incorporates work from mjolnir +// https://github.com/matrix-org/mjolnir +// + +import { AbstractProtection, ActionError, ActionResult, Logger, MatrixRoomReference, MembershipEvent, Ok, Permalink, ProtectedRoomsSet, ProtectionDescription, RoomEvent, StringRoomID, Task, Value, describeProtection, isError, serverName } from "matrix-protection-suite"; +import { Draupnir } from "../../Draupnir"; +import { DraupnirProtection } from "../Protection"; +import { isInvitationForUser, isSenderJoinedInRevision } from "./inviteCore"; +import { renderMatrixAndSend } from "../../commands/interface-manager/DeadDocumentMatrix"; +import { DocumentNode } from "../../commands/interface-manager/DeadDocument"; +import { JSXFactory } from "../../commands/interface-manager/JSXFactory"; +import { renderActionResultToEvent, renderMentionPill, renderRoomPill } from "../../commands/interface-manager/MatrixHelpRenderer"; +import { renderFailedSingularConsequence } from "../../capabilities/CommonRenderers"; +import { StaticDecode, Type } from "@sinclair/typebox"; + +const log = new Logger('ProtectRoomsOnInviteProtection'); + +export type ProtectRoomsOnInviteProtectionCapabilities = {}; + +export type ProtectRoomsOnInviteProtectionDescription = ProtectionDescription; + +const PROTECT_ROOMS_ON_INVITE_PROMPT_LISTENER = 'me.marewolf.draupnir.protect_rooms_on_invite'; + +// would be nice to be able to use presentation types here idk. +const ProtectRoomsOnInvitePromptContext = Type.Object({ + invited_room: Permalink +}); +// this rule is stupid. +// eslint-disable-next-line no-redeclare +type ProtectRoomsOnInvitePromptContext = StaticDecode; + +export class ProtectRoomsOnInviteProtection + extends AbstractProtection + implements DraupnirProtection< + ProtectRoomsOnInviteProtectionDescription +> { + private readonly protectPromptListener = this.protectListener.bind(this); + public constructor( + description: ProtectRoomsOnInviteProtectionDescription, + capabilities: ProtectRoomsOnInviteProtectionCapabilities, + protectedRoomsSet: ProtectedRoomsSet, + private readonly draupnir: Draupnir, + ) { + super( + description, + capabilities, + protectedRoomsSet, + {} + ) + this.draupnir.reactionHandler.on(PROTECT_ROOMS_ON_INVITE_PROMPT_LISTENER, this.protectPromptListener); + } + + handleProtectionDisable(): void { + this.draupnir.reactionHandler.off(PROTECT_ROOMS_ON_INVITE_PROMPT_LISTENER, this.protectPromptListener); + } + + handleExternalInvite(roomID: StringRoomID, event: MembershipEvent): void { + if (!isInvitationForUser(event, this.protectedRoomsSet.userID)) { + return; + } + void Task(this.checkAgainstRequiredMembershipRoom(event)); + } + + private async checkAgainstRequiredMembershipRoom(event: MembershipEvent): Promise> { + const revision = this.draupnir.acceptInvitesFromRoomIssuer.currentRevision; + if (isSenderJoinedInRevision(event.sender, revision)) { + return await this.joinAndPromptProtect(event); + } else { + this.reportUnknownInvite(event, revision.room); + return Ok(undefined); + } + } + + private reportUnknownInvite(event: MembershipEvent, requiredMembershipRoom: MatrixRoomReference): void { + const renderUnknownInvite = (): DocumentNode => { + return + {renderMentionPill(event.sender, event.sender)} has invited me to + {renderRoomPill(MatrixRoomReference.fromRoomID(event.room_id))} + but they are not joined to {renderRoomPill(requiredMembershipRoom)}, which prevents me from accepting their invitation.
+ If you would like this room protected, use !draupnir rooms add {event.room_id} +
+ } + void Task((async () => { + renderMatrixAndSend( + renderUnknownInvite(), + this.draupnir.managementRoomID, + undefined, + this.draupnir.client + ); + return Ok(undefined) + })()); + } + + private async joinInvitedRoom(event: MembershipEvent, room: MatrixRoomReference): Promise> { + const renderFailedTojoin = (error: ActionError) => { + const title = Unfortunatley I was unable to accept the invitation from {renderMentionPill(event.sender, event.sender)} to the room {renderRoomPill(room)}.; + return + {renderFailedSingularConsequence(this.description, title, error)} + + }; + const joinResult = await this.draupnir.clientPlatform.toRoomJoiner().joinRoom(room); + if (isError(joinResult)) { + await renderMatrixAndSend( + renderFailedTojoin(joinResult.error), + this.draupnir.managementRoomID, + undefined, + this.draupnir.client + ) + } + return joinResult; + } + + private async joinAndPromptProtect(event: MembershipEvent): Promise> { + const invitedRoomReference = MatrixRoomReference.fromRoomID(event.room_id, [serverName(event.sender), serverName(event.state_key)]); + const joinResult = await this.joinInvitedRoom(event, invitedRoomReference); + if (isError(joinResult)) { + return joinResult; + } + const renderPromptProtect = (): DocumentNode => + + {renderMentionPill(event.sender, event.sender)} has invited me to + {renderRoomPill(invitedRoomReference)}, + would you like to protect this room? + ; + const reactionMap = new Map(Object.entries({ 'OK': 'OK' })); + const promptEventID = (await renderMatrixAndSend( + renderPromptProtect(), + this.draupnir.managementRoomID, + undefined, + this.draupnir.client, + this.draupnir.reactionHandler.createAnnotation( + PROTECT_ROOMS_ON_INVITE_PROMPT_LISTENER, + reactionMap, + { + invited_room: invitedRoomReference.toPermalink(), + } + ) + ))[0]; + await this.draupnir.reactionHandler.addReactionsToEvent(this.draupnir.client, this.draupnir.managementRoomID, promptEventID, reactionMap); + return Ok(undefined); + } + + + private protectListener(key: string, _item: unknown, rawContext: unknown, _reactionMap: Map, promptEvent: RoomEvent): void { + if (key !== 'OK') { + return; + } + const context = Value.Decode(ProtectRoomsOnInvitePromptContext, rawContext); + if (isError(context)) { + log.error(`Could not decode context from prompt event`, context.error); + renderActionResultToEvent(this.draupnir.client, promptEvent, context); + return; + } + void Task((async () => { + const resolvedRoom = await this.draupnir.clientPlatform.toRoomResolver().resolveRoom(context.ok.invited_room); + if (isError(resolvedRoom)) { + resolvedRoom.elaborate(`Could not resolve the room to protect from the MatrixRoomReference: ${context.ok.invited_room.toPermalink()}.`); + renderActionResultToEvent(this.draupnir.client, promptEvent, resolvedRoom); + return; + } + const addResult = await this.protectedRoomsSet.protectedRoomsManager.addRoom(resolvedRoom.ok) + if (isError(addResult)) { + addResult.elaborate(`Could not protect the room: ${resolvedRoom.ok.toPermalink()}`); + renderActionResultToEvent(this.draupnir.client, promptEvent, addResult); + return; + } + renderActionResultToEvent(this.draupnir.client, promptEvent, addResult); + })()); + } +} + +describeProtection<{}, Draupnir>({ + name: ProtectRoomsOnInviteProtection.name, + description: "Automatically joins rooms when invited by members of the management room and offers to protect them", + capabilityInterfaces: {}, + defaultCapabilities: {}, + factory(description, protectedRoomsSet, draupnir, capabilities, _settings) { + return Ok( + new ProtectRoomsOnInviteProtection( + description, + capabilities, + protectedRoomsSet, + draupnir + ) + ) + } +}) diff --git a/src/protections/invitation/inviteCore.ts b/src/protections/invitation/inviteCore.ts new file mode 100644 index 00000000..d6650e81 --- /dev/null +++ b/src/protections/invitation/inviteCore.ts @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { Membership, MembershipEvent, RoomMembershipRevision, StringUserID } from "matrix-protection-suite"; + +export function isInvitationForUser( + event: MembershipEvent, + clientUserID: StringUserID +): event is MembershipEvent & { content: { membership: Membership.Invite }} { + return event.state_key === clientUserID + && event.content.membership === Membership.Invite +}; + +export function isSenderJoinedInRevision( + senderUserID: StringUserID, + membership: RoomMembershipRevision +): boolean { + const senderMembership = membership.membershipForUser(senderUserID); + return Boolean(senderMembership?.content.membership === Membership.Join); +} From 3b20fe1122cac9d85caf62a880135aa5d952ef24 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Mon, 20 May 2024 16:56:07 +0100 Subject: [PATCH 03/12] Update to MPS v0.22.0. --- package.json | 4 ++-- yarn.lock | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index d7e2dd61..b5f5826e 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,8 @@ "js-yaml": "^4.1.0", "jsdom": "^24.0.0", "matrix-appservice-bridge": "^9.0.1", - "matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@0.21.0", - "matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@0.21.0", + "matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@0.22.0", + "matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@0.22.0", "parse-duration": "^1.0.2", "pg": "^8.8.0", "shell-quote": "^1.7.3", diff --git a/yarn.lock b/yarn.lock index dbddfc48..bacf621f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2505,15 +2505,15 @@ matrix-appservice@^2.0.0: request-promise "^4.2.6" sanitize-html "^2.8.0" -"matrix-protection-suite-for-matrix-bot-sdk@npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@0.21.0": - version "0.21.0" - resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite-for-matrix-bot-sdk/-/matrix-protection-suite-for-matrix-bot-sdk-0.21.0.tgz#2b03a1409e01508635d31c9e0f7d3650d48467de" - integrity sha512-wRcNYrY8gDxX6ofPjRpE5DI+i+X0q5o2VqFXXYA2TSiYCues0B26DZeRH5qcw8pmMM+voCGSPurLD7RFnm1a8Q== - -"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@0.21.0": - version "0.21.0" - resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-0.21.0.tgz#9f4983cf1a2a6583f7d9ee6ea84fc4428f9add6b" - integrity sha512-hIZpkIv0bEl5aKMv/AzK9Lcbg6CD3lw5VDYQmv04pM2/ja4z5nCsxD9rWeEUl+1IP+i0RjFWLNnbhl8n9nPsnA== +"matrix-protection-suite-for-matrix-bot-sdk@npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@0.22.0": + version "0.22.0" + resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite-for-matrix-bot-sdk/-/matrix-protection-suite-for-matrix-bot-sdk-0.22.0.tgz#71703050b89bf4ddf6462cea2ec26305377210f8" + integrity sha512-mRnYBnffOJxUQNqLnhPaFx9EPPgT2q0ZWkRe7TA1DMhTxsEitk/6c3uQ29vreqvE31fH1U0WqFmrPkQvqRGTww== + +"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@0.22.0": + version "0.22.0" + resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-0.22.0.tgz#5c4fa1616a7c682b5c21dfcf4a22e281c7b64f7d" + integrity sha512-hqV9wFb2DDWmCJYrJ8tr/lTpQnkfVen+w/YGN15XrcY3M6VtciCEKIUYjp1BWkGUWufKbcYMGN/BRSQsT2PpUw== dependencies: await-lock "^2.2.2" crypto-js "^4.2.0" From 95007952fe1499b2818515b2ce09df08ab25e154 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Mon, 20 May 2024 13:25:54 +0100 Subject: [PATCH 04/12] Allow prompts to be cancelled. --- src/Draupnir.ts | 2 +- src/DraupnirBotMode.ts | 2 +- src/appservice/AppService.ts | 2 +- .../bot/AppserviceCommandHandler.ts | 3 +- .../MatrixReactionHandler.ts | 64 +++++++++++++++++-- .../ProtectRoomsOnInviteProtection.tsx | 7 +- 6 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/Draupnir.ts b/src/Draupnir.ts index f4aa141a..92f58c8a 100644 --- a/src/Draupnir.ts +++ b/src/Draupnir.ts @@ -106,7 +106,7 @@ export class Draupnir implements Client { this.managementRoomOutput = new ManagementRoomOutput( this.managementRoomID, this.clientUserID, this.client, this.config ); - this.reactionHandler = new MatrixReactionHandler(this.managementRoom.toRoomIDOrAlias(), client, clientUserID); + this.reactionHandler = new MatrixReactionHandler(this.managementRoom.toRoomIDOrAlias(), client, clientUserID, clientPlatform); this.reportManager = new ReportManager(this); if (config.pollReports) { this.reportPoller = new ReportPoller(this, this.reportManager); diff --git a/src/DraupnirBotMode.ts b/src/DraupnirBotMode.ts index fb048acd..889a5e7f 100644 --- a/src/DraupnirBotMode.ts +++ b/src/DraupnirBotMode.ts @@ -96,7 +96,7 @@ export async function makeDraupnirBotModeFromConfig( DefaultEventDecoder, backingStore ); - const clientCapabilityFactory = new ClientCapabilityFactory(clientsInRoomMap); + const clientCapabilityFactory = new ClientCapabilityFactory(clientsInRoomMap, DefaultEventDecoder); const draupnirFactory = new DraupnirFactory( clientsInRoomMap, clientCapabilityFactory, diff --git a/src/appservice/AppService.ts b/src/appservice/AppService.ts index e7f3e4eb..4e93540c 100644 --- a/src/appservice/AppService.ts +++ b/src/appservice/AppService.ts @@ -128,7 +128,7 @@ export class MjolnirAppService { clientProvider, eventDecoder ); - const clientCapabilityFactory = new ClientCapabilityFactory(clientsInRoomMap); + const clientCapabilityFactory = new ClientCapabilityFactory(clientsInRoomMap, eventDecoder); const botUserID = bridge.getBot().getUserId() as StringUserID; const clientRooms = await clientsInRoomMap.makeClientRooms( botUserID, diff --git a/src/appservice/bot/AppserviceCommandHandler.ts b/src/appservice/bot/AppserviceCommandHandler.ts index 5563a71d..2a4be172 100644 --- a/src/appservice/bot/AppserviceCommandHandler.ts +++ b/src/appservice/bot/AppserviceCommandHandler.ts @@ -57,7 +57,8 @@ export class AppserviceCommandHandler { this.reactionHandler = new MatrixReactionHandler( this.appservice.accessControlRoomID, this.appservice.bridge.getBot().getClient(), - this.appservice.botUserID + this.appservice.botUserID, + clientPlatform ); this.commandContext = { appservice: this.appservice, diff --git a/src/commands/interface-manager/MatrixReactionHandler.ts b/src/commands/interface-manager/MatrixReactionHandler.ts index 2e5cfac4..2e96d3ec 100644 --- a/src/commands/interface-manager/MatrixReactionHandler.ts +++ b/src/commands/interface-manager/MatrixReactionHandler.ts @@ -1,12 +1,13 @@ -/** - * Copyright (C) 2023 Gnuxie - * All rights reserved. - */ +// SPDX-FileCopyrightText: 2023 - 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 import { EventEmitter } from "stream"; import { LogService } from "matrix-bot-sdk"; import { MatrixSendClient } from "matrix-protection-suite-for-matrix-bot-sdk"; -import { ReactionEvent, RoomEvent, StringRoomID, StringUserID, Value } from "matrix-protection-suite"; +import { ActionResult, ClientPlatform, Logger, ReactionEvent, RoomEvent, StringEventID, StringRoomID, StringUserID, Task, Value, isError } from "matrix-protection-suite"; + +const log = new Logger("MatrixReactionHandler"); const REACTION_ANNOTATION_KEY = 'ge.applied-langua.ge.draupnir.reaction_handler'; @@ -42,7 +43,8 @@ export class MatrixReactionHandler extends EventEmitter implements MatrixReactio /** * The user id of the client. Ignores reactions from this user */ - private readonly clientUserID: StringUserID + private readonly clientUserID: StringUserID, + private readonly clientPlatform: ClientPlatform ) { super(); } @@ -127,6 +129,56 @@ export class MatrixReactionHandler extends EventEmitter implements MatrixReactio ).catch(e => (LogService.error('MatrixReactionHandler', `Could not add reaction to event ${eventId}`, e), Promise.reject(e))); } + public async completePrompt( + roomID: StringRoomID, + eventID: StringEventID, + reason?: string + ): Promise> { + const eventRelationsGetter = this.clientPlatform.toRoomEventRelationsGetter(); + const redacter = this.clientPlatform.toRoomEventRedacter(); + return await eventRelationsGetter.forEachRelation( + roomID, + eventID, + { + relationType: 'm.annotation', + eventType: 'm.reaction', + forEachCB: (event) => { + const key = event.content?.["m.relates_to"]?.key + // skip the bots own reactions that mark the event as complete + if (key === '✅' || key === '❌') { + return; + } + void Task((async function () { + redacter.redactEvent(roomID, event.event_id, reason) + })()) + } + } + ); + } + + /** + * Removes all reactions from the prompt event in an attempt to stop it being used further. + */ + public async cancelPrompt( + promptEvent: RoomEvent, + cancelReason?: string + ): Promise> { + const completeResult = await this.completePrompt( + promptEvent.room_id, + promptEvent.event_id, + cancelReason ?? 'prompt cancelled' + ); + if (isError(completeResult)) { + return completeResult; + } + void this.client.unstableApis.addReactionToEvent( + promptEvent.room_id, + promptEvent.event_id, + `🚫 Cancelled by ${promptEvent.sender}` + ).catch(e => log.error(`Could not send cancelled reaction event for prompt ${promptEvent.event_id} in ${promptEvent.room_id}`, e)); + return completeResult; + } + public static createItemizedReactionMap(items: string[]): ItemByReactionKey { return items.reduce( (acc, item, index) => { diff --git a/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx b/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx index 949c9a65..01092a8c 100644 --- a/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx +++ b/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx @@ -128,7 +128,7 @@ export class ProtectRoomsOnInviteProtection {renderRoomPill(invitedRoomReference)}, would you like to protect this room? ; - const reactionMap = new Map(Object.entries({ 'OK': 'OK' })); + const reactionMap = new Map(Object.entries({ 'OK': 'OK', 'Cancel': 'Cancel' })); const promptEventID = (await renderMatrixAndSend( renderPromptProtect(), this.draupnir.managementRoomID, @@ -148,6 +148,10 @@ export class ProtectRoomsOnInviteProtection private protectListener(key: string, _item: unknown, rawContext: unknown, _reactionMap: Map, promptEvent: RoomEvent): void { + if (key === 'Cancel') { + void Task(this.draupnir.reactionHandler.cancelPrompt(promptEvent)); + return; + } if (key !== 'OK') { return; } @@ -171,6 +175,7 @@ export class ProtectRoomsOnInviteProtection return; } renderActionResultToEvent(this.draupnir.client, promptEvent, addResult); + void Task(this.draupnir.reactionHandler.completePrompt(promptEvent.room_id, promptEvent.event_id)); })()); } } From 9792df15d3621d3cec980d168200577672847577 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Mon, 20 May 2024 15:27:05 +0100 Subject: [PATCH 05/12] Use MPS logger in MatrixReactionhandler. --- src/commands/interface-manager/MatrixReactionHandler.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/commands/interface-manager/MatrixReactionHandler.ts b/src/commands/interface-manager/MatrixReactionHandler.ts index 2e96d3ec..7333ee24 100644 --- a/src/commands/interface-manager/MatrixReactionHandler.ts +++ b/src/commands/interface-manager/MatrixReactionHandler.ts @@ -3,7 +3,6 @@ // SPDX-License-Identifier: AFL-3.0 import { EventEmitter } from "stream"; -import { LogService } from "matrix-bot-sdk"; import { MatrixSendClient } from "matrix-protection-suite-for-matrix-bot-sdk"; import { ActionResult, ClientPlatform, Logger, ReactionEvent, RoomEvent, StringEventID, StringRoomID, StringUserID, Task, Value, isError } from "matrix-protection-suite"; @@ -82,17 +81,17 @@ export class MatrixReactionHandler extends EventEmitter implements MatrixReactio } const reactionMap = annotation['reaction_map']; if (typeof reactionMap !== 'object' || reactionMap === null) { - LogService.warn('MatrixReactionHandler', `Missing reaction_map for the annotated event ${relatedEventId} in ${roomID}`); + log.warn( `Missing reaction_map for the annotated event ${relatedEventId} in ${roomID}`); return; } const listenerName = annotation['name']; if (typeof listenerName !== 'string') { - LogService.warn('MatrixReactionHandler', `The event ${relatedEventId} in ${roomID} is missing the name of the annotation`); + log.warn( `The event ${relatedEventId} in ${roomID} is missing the name of the annotation`); return; } const association = reactionMap[reactionKey]; if (association === undefined) { - LogService.info('MatrixReactionHandler', `There wasn't a defined key for ${reactionKey} on event ${relatedEventId} in ${roomID}`); + log.info( `There wasn't a defined key for ${reactionKey} on event ${relatedEventId} in ${roomID}`); return; } this.emit(listenerName, reactionKey, association, annotation['additional_context'], new Map(Object.entries(reactionMap)), annotatedEvent); @@ -126,7 +125,7 @@ export class MatrixReactionHandler extends EventEmitter implements MatrixReactio await [...reactionMap.keys()] .reduce((acc, key) => acc.then(_ => client.unstableApis.addReactionToEvent(roomId, eventId, key)), Promise.resolve() - ).catch(e => (LogService.error('MatrixReactionHandler', `Could not add reaction to event ${eventId}`, e), Promise.reject(e))); + ).catch(e => (log.error( `Could not add reaction to event ${eventId}`, e), Promise.reject(e))); } public async completePrompt( From 180594502d425301cf2adfa778dcdef4587c5539 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Mon, 20 May 2024 15:41:01 +0100 Subject: [PATCH 06/12] Ignore invitation events to rooms we are already protecting. --- src/protections/invitation/ProtectRoomsOnInviteProtection.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx b/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx index 01092a8c..4cd96882 100644 --- a/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx +++ b/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx @@ -64,6 +64,9 @@ export class ProtectRoomsOnInviteProtection if (!isInvitationForUser(event, this.protectedRoomsSet.userID)) { return; } + if (this.protectedRoomsSet.isProtectedRoom(roomID)) { + return; + } void Task(this.checkAgainstRequiredMembershipRoom(event)); } From d156eb3f6e8ece103c05c346a1ccccb449e2c82b Mon Sep 17 00:00:00 2001 From: gnuxie Date: Mon, 20 May 2024 16:32:48 +0100 Subject: [PATCH 07/12] Sometimes we see invitations twice on protect rooms on invite. --- .../invitation/ProtectRoomsOnInviteProtection.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx b/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx index 4cd96882..8b12d1de 100644 --- a/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx +++ b/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx @@ -8,7 +8,7 @@ // https://github.com/matrix-org/mjolnir // -import { AbstractProtection, ActionError, ActionResult, Logger, MatrixRoomReference, MembershipEvent, Ok, Permalink, ProtectedRoomsSet, ProtectionDescription, RoomEvent, StringRoomID, Task, Value, describeProtection, isError, serverName } from "matrix-protection-suite"; +import { AbstractProtection, ActionError, ActionResult, Logger, MatrixRoomReference, MembershipEvent, Ok, Permalink, ProtectedRoomsSet, ProtectionDescription, RoomEvent, StandardDeduplicator, StringRoomID, Task, Value, describeProtection, isError, serverName } from "matrix-protection-suite"; import { Draupnir } from "../../Draupnir"; import { DraupnirProtection } from "../Protection"; import { isInvitationForUser, isSenderJoinedInRevision } from "./inviteCore"; @@ -40,6 +40,7 @@ export class ProtectRoomsOnInviteProtection implements DraupnirProtection< ProtectRoomsOnInviteProtectionDescription > { + private readonly promptedToProtectedDeduplicator = new StandardDeduplicator(); private readonly protectPromptListener = this.protectListener.bind(this); public constructor( description: ProtectRoomsOnInviteProtectionDescription, @@ -67,6 +68,11 @@ export class ProtectRoomsOnInviteProtection if (this.protectedRoomsSet.isProtectedRoom(roomID)) { return; } + // The event handler gets called again when we join the room we were invited to. + // As sometimes we get the invitation a second time from the join section of sync. + if (this.promptedToProtectedDeduplicator.isDuplicate(roomID)) { + return; + } void Task(this.checkAgainstRequiredMembershipRoom(event)); } From 38553b7221aa7868b8e8e88fbdd2ee3d4f0ace36 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 22 May 2024 11:55:23 +0100 Subject: [PATCH 08/12] Time to split out the ProtectRoomsOnInviteProtection. --- .../invitation/ProtectRoomsOnInvite.tsx | 103 ++++++++++++++ .../ProtectRoomsOnInviteProtection.tsx | 96 +++---------- .../invitation/WatchRoomsOnInvite.tsx | 129 ++++++++++++++++++ 3 files changed, 252 insertions(+), 76 deletions(-) create mode 100644 src/protections/invitation/ProtectRoomsOnInvite.tsx create mode 100644 src/protections/invitation/WatchRoomsOnInvite.tsx diff --git a/src/protections/invitation/ProtectRoomsOnInvite.tsx b/src/protections/invitation/ProtectRoomsOnInvite.tsx new file mode 100644 index 00000000..bba82cb3 --- /dev/null +++ b/src/protections/invitation/ProtectRoomsOnInvite.tsx @@ -0,0 +1,103 @@ +// Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. +// Copyright 2022 - 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0 +// +// SPDX-FileAttributionText: +// This modified file incorporates work from mjolnir +// https://github.com/matrix-org/mjolnir +// + +import { Logger, MatrixRoomID, MembershipEvent, Ok, Permalink, ProtectedRoomsSet, RoomEvent, Task, Value, isError } from "matrix-protection-suite"; +import { DocumentNode } from "../../commands/interface-manager/DeadDocument"; +import { renderActionResultToEvent, renderMentionPill, renderRoomPill } from "../../commands/interface-manager/MatrixHelpRenderer"; +import { JSXFactory } from "../../commands/interface-manager/JSXFactory"; +import { renderMatrixAndSend } from "../../commands/interface-manager/DeadDocumentMatrix"; +import { StaticDecode, Type } from "@sinclair/typebox"; +import { Draupnir } from "../../Draupnir"; + +const log = new Logger('ProtectRoomsOnInvite'); + +const PROTECT_ROOMS_ON_INVITE_PROMPT_LISTENER = 'me.marewolf.draupnir.protect_rooms_on_invite'; + +// would be nice to be able to use presentation types here idk. +const ProtectRoomsOnInvitePromptContext = Type.Object({ + invited_room: Permalink +}); +// this rule is stupid. +// eslint-disable-next-line no-redeclare +type ProtectRoomsOnInvitePromptContext = StaticDecode; + +export class ProtectroomsOnInvite { + + private readonly protectPromptListener = this.protectListener.bind(this); + public constructor( + private readonly draupnir: Draupnir, + private readonly protectedRoomsSet: ProtectedRoomsSet, + ) { + this.draupnir.reactionHandler.on(PROTECT_ROOMS_ON_INVITE_PROMPT_LISTENER, this.protectPromptListener); + } + + handleProtectionDisable(): void { + this.draupnir.reactionHandler.off(PROTECT_ROOMS_ON_INVITE_PROMPT_LISTENER, this.protectPromptListener); + } + + public promptToProtect(candidateRoom: MatrixRoomID, invitation: MembershipEvent): void { + void Task((async () => { + const renderPromptProtect = (): DocumentNode => + + {renderMentionPill(invitation.sender, invitation.sender)} has invited me to + {renderRoomPill(candidateRoom)}, + would you like to protect this room? + ; + const reactionMap = new Map(Object.entries({ 'OK': 'OK', 'Cancel': 'Cancel' })); + const promptEventID = (await renderMatrixAndSend( + renderPromptProtect(), + this.draupnir.managementRoomID, + undefined, + this.draupnir.client, + this.draupnir.reactionHandler.createAnnotation( + PROTECT_ROOMS_ON_INVITE_PROMPT_LISTENER, + reactionMap, + { + invited_room: candidateRoom.toPermalink(), + } + ) + ))[0]; + await this.draupnir.reactionHandler.addReactionsToEvent(this.draupnir.client, this.draupnir.managementRoomID, promptEventID, reactionMap); + return Ok(undefined); + })()) + } + + private protectListener(key: string, _item: unknown, rawContext: unknown, _reactionMap: Map, promptEvent: RoomEvent): void { + if (key === 'Cancel') { + void Task(this.draupnir.reactionHandler.cancelPrompt(promptEvent)); + return; + } + if (key !== 'OK') { + return; + } + const context = Value.Decode(ProtectRoomsOnInvitePromptContext, rawContext); + if (isError(context)) { + log.error(`Could not decode context from prompt event`, context.error); + renderActionResultToEvent(this.draupnir.client, promptEvent, context); + return; + } + void Task((async () => { + const resolvedRoom = await this.draupnir.clientPlatform.toRoomResolver().resolveRoom(context.ok.invited_room); + if (isError(resolvedRoom)) { + resolvedRoom.elaborate(`Could not resolve the room to protect from the MatrixRoomReference: ${context.ok.invited_room.toPermalink()}.`); + renderActionResultToEvent(this.draupnir.client, promptEvent, resolvedRoom); + return; + } + const addResult = await this.protectedRoomsSet.protectedRoomsManager.addRoom(resolvedRoom.ok) + if (isError(addResult)) { + addResult.elaborate(`Could not protect the room: ${resolvedRoom.ok.toPermalink()}`); + renderActionResultToEvent(this.draupnir.client, promptEvent, addResult); + return; + } + renderActionResultToEvent(this.draupnir.client, promptEvent, addResult); + void Task(this.draupnir.reactionHandler.completePrompt(promptEvent.room_id, promptEvent.event_id)); + })()); + } +} diff --git a/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx b/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx index 8b12d1de..eea2ea6f 100644 --- a/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx +++ b/src/protections/invitation/ProtectRoomsOnInviteProtection.tsx @@ -8,40 +8,36 @@ // https://github.com/matrix-org/mjolnir // -import { AbstractProtection, ActionError, ActionResult, Logger, MatrixRoomReference, MembershipEvent, Ok, Permalink, ProtectedRoomsSet, ProtectionDescription, RoomEvent, StandardDeduplicator, StringRoomID, Task, Value, describeProtection, isError, serverName } from "matrix-protection-suite"; +import { AbstractProtection, ActionError, ActionResult, MatrixRoomReference, MembershipEvent, Ok, ProtectedRoomsSet, ProtectionDescription, StandardDeduplicator, StringRoomID, Task, describeProtection, isError, serverName } from "matrix-protection-suite"; import { Draupnir } from "../../Draupnir"; import { DraupnirProtection } from "../Protection"; import { isInvitationForUser, isSenderJoinedInRevision } from "./inviteCore"; import { renderMatrixAndSend } from "../../commands/interface-manager/DeadDocumentMatrix"; import { DocumentNode } from "../../commands/interface-manager/DeadDocument"; import { JSXFactory } from "../../commands/interface-manager/JSXFactory"; -import { renderActionResultToEvent, renderMentionPill, renderRoomPill } from "../../commands/interface-manager/MatrixHelpRenderer"; +import { renderMentionPill, renderRoomPill } from "../../commands/interface-manager/MatrixHelpRenderer"; import { renderFailedSingularConsequence } from "../../capabilities/CommonRenderers"; -import { StaticDecode, Type } from "@sinclair/typebox"; - -const log = new Logger('ProtectRoomsOnInviteProtection'); +import { ProtectroomsOnInvite } from "./ProtectRoomsOnInvite"; +import { WatchRoomsOnInvite } from "./WatchRoomsOnInvite"; export type ProtectRoomsOnInviteProtectionCapabilities = {}; export type ProtectRoomsOnInviteProtectionDescription = ProtectionDescription; -const PROTECT_ROOMS_ON_INVITE_PROMPT_LISTENER = 'me.marewolf.draupnir.protect_rooms_on_invite'; - -// would be nice to be able to use presentation types here idk. -const ProtectRoomsOnInvitePromptContext = Type.Object({ - invited_room: Permalink -}); -// this rule is stupid. -// eslint-disable-next-line no-redeclare -type ProtectRoomsOnInvitePromptContext = StaticDecode; - export class ProtectRoomsOnInviteProtection extends AbstractProtection implements DraupnirProtection< ProtectRoomsOnInviteProtectionDescription > { private readonly promptedToProtectedDeduplicator = new StandardDeduplicator(); - private readonly protectPromptListener = this.protectListener.bind(this); + private readonly protectRoomsOnInvite = new ProtectroomsOnInvite( + this.draupnir, + this.protectedRoomsSet + ); + private readonly watchRoomsOnInvite = new WatchRoomsOnInvite( + this.draupnir, + this.protectedRoomsSet + ); public constructor( description: ProtectRoomsOnInviteProtectionDescription, capabilities: ProtectRoomsOnInviteProtectionCapabilities, @@ -54,20 +50,17 @@ export class ProtectRoomsOnInviteProtection protectedRoomsSet, {} ) - this.draupnir.reactionHandler.on(PROTECT_ROOMS_ON_INVITE_PROMPT_LISTENER, this.protectPromptListener); } handleProtectionDisable(): void { - this.draupnir.reactionHandler.off(PROTECT_ROOMS_ON_INVITE_PROMPT_LISTENER, this.protectPromptListener); + this.protectRoomsOnInvite.handleProtectionDisable(); + this.watchRoomsOnInvite.handleProtectionDisable(); } handleExternalInvite(roomID: StringRoomID, event: MembershipEvent): void { if (!isInvitationForUser(event, this.protectedRoomsSet.userID)) { return; } - if (this.protectedRoomsSet.isProtectedRoom(roomID)) { - return; - } // The event handler gets called again when we join the room we were invited to. // As sometimes we get the invitation a second time from the join section of sync. if (this.promptedToProtectedDeduplicator.isDuplicate(roomID)) { @@ -79,7 +72,7 @@ export class ProtectRoomsOnInviteProtection private async checkAgainstRequiredMembershipRoom(event: MembershipEvent): Promise> { const revision = this.draupnir.acceptInvitesFromRoomIssuer.currentRevision; if (isSenderJoinedInRevision(event.sender, revision)) { - return await this.joinAndPromptProtect(event); + return await this.joinAndIssuePrompts(event); } else { this.reportUnknownInvite(event, revision.room); return Ok(undefined); @@ -125,68 +118,19 @@ export class ProtectRoomsOnInviteProtection return joinResult; } - private async joinAndPromptProtect(event: MembershipEvent): Promise> { + private async joinAndIssuePrompts(event: MembershipEvent): Promise> { const invitedRoomReference = MatrixRoomReference.fromRoomID(event.room_id, [serverName(event.sender), serverName(event.state_key)]); const joinResult = await this.joinInvitedRoom(event, invitedRoomReference); if (isError(joinResult)) { return joinResult; } - const renderPromptProtect = (): DocumentNode => - - {renderMentionPill(event.sender, event.sender)} has invited me to - {renderRoomPill(invitedRoomReference)}, - would you like to protect this room? - ; - const reactionMap = new Map(Object.entries({ 'OK': 'OK', 'Cancel': 'Cancel' })); - const promptEventID = (await renderMatrixAndSend( - renderPromptProtect(), - this.draupnir.managementRoomID, - undefined, - this.draupnir.client, - this.draupnir.reactionHandler.createAnnotation( - PROTECT_ROOMS_ON_INVITE_PROMPT_LISTENER, - reactionMap, - { - invited_room: invitedRoomReference.toPermalink(), - } - ) - ))[0]; - await this.draupnir.reactionHandler.addReactionsToEvent(this.draupnir.client, this.draupnir.managementRoomID, promptEventID, reactionMap); + void this.watchRoomsOnInvite.promptIfPossiblePolicyRoom(invitedRoomReference, event); + if (!this.protectedRoomsSet.isProtectedRoom(event.room_id)) { + void this.protectRoomsOnInvite.promptToProtect(invitedRoomReference, event); + } return Ok(undefined); } - - private protectListener(key: string, _item: unknown, rawContext: unknown, _reactionMap: Map, promptEvent: RoomEvent): void { - if (key === 'Cancel') { - void Task(this.draupnir.reactionHandler.cancelPrompt(promptEvent)); - return; - } - if (key !== 'OK') { - return; - } - const context = Value.Decode(ProtectRoomsOnInvitePromptContext, rawContext); - if (isError(context)) { - log.error(`Could not decode context from prompt event`, context.error); - renderActionResultToEvent(this.draupnir.client, promptEvent, context); - return; - } - void Task((async () => { - const resolvedRoom = await this.draupnir.clientPlatform.toRoomResolver().resolveRoom(context.ok.invited_room); - if (isError(resolvedRoom)) { - resolvedRoom.elaborate(`Could not resolve the room to protect from the MatrixRoomReference: ${context.ok.invited_room.toPermalink()}.`); - renderActionResultToEvent(this.draupnir.client, promptEvent, resolvedRoom); - return; - } - const addResult = await this.protectedRoomsSet.protectedRoomsManager.addRoom(resolvedRoom.ok) - if (isError(addResult)) { - addResult.elaborate(`Could not protect the room: ${resolvedRoom.ok.toPermalink()}`); - renderActionResultToEvent(this.draupnir.client, promptEvent, addResult); - return; - } - renderActionResultToEvent(this.draupnir.client, promptEvent, addResult); - void Task(this.draupnir.reactionHandler.completePrompt(promptEvent.room_id, promptEvent.event_id)); - })()); - } } describeProtection<{}, Draupnir>({ diff --git a/src/protections/invitation/WatchRoomsOnInvite.tsx b/src/protections/invitation/WatchRoomsOnInvite.tsx new file mode 100644 index 00000000..8e2be120 --- /dev/null +++ b/src/protections/invitation/WatchRoomsOnInvite.tsx @@ -0,0 +1,129 @@ +// Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. +// Copyright 2022 - 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0 +// +// SPDX-FileAttributionText: +// This modified file incorporates work from mjolnir +// https://github.com/matrix-org/mjolnir +// + +import { ALL_RULE_TYPES, Logger, MJOLNIR_SHORTCODE_EVENT_TYPE, MatrixRoomID, MembershipEvent, Ok, Permalink, ProtectedRoomsSet, RoomEvent, RoomStateRevision, Task, Value, isError } from "matrix-protection-suite"; +import { Draupnir } from "../../Draupnir"; +import { DocumentNode } from "../../commands/interface-manager/DeadDocument"; +import { JSXFactory } from "../../commands/interface-manager/JSXFactory"; +import { renderActionResultToEvent, renderMentionPill, renderRoomPill } from "../../commands/interface-manager/MatrixHelpRenderer"; +import { renderMatrixAndSend } from "../../commands/interface-manager/DeadDocumentMatrix"; +import { StaticDecode, Type } from "@sinclair/typebox"; + +const log = new Logger('WatchRoomsOnInvite'); + +const WATCH_LISTS_ON_INVITE_PROMPT_LISTENER = 'me.marewolf.draupnir.watch_rooms_on_invite'; + +// would be nice to be able to use presentation types here idk. +const WatchRoomsOnInvitePromptContext = Type.Object({ + invited_room: Permalink +}); +// this rule is stupid. +// eslint-disable-next-line no-redeclare +type WatchRoomsOnInvitePromptContext = StaticDecode; + +function isRevisionContainingPolicies(revision: RoomStateRevision) { + return revision.getStateEventsOfTypes(ALL_RULE_TYPES).length > 0; +} + +function isRevisionContainingShortcode(revision: RoomStateRevision) { + return revision.getStateEvent(MJOLNIR_SHORTCODE_EVENT_TYPE, '') !== undefined; +} + +export function isRevisionLikelyPolicyRoom(revision: RoomStateRevision) { + return isRevisionContainingPolicies(revision) || isRevisionContainingShortcode(revision); +} + +export class WatchRoomsOnInvite { + private readonly watchPromptListener = this.watchListener.bind(this); + public constructor( + private readonly draupnir: Draupnir, + private readonly protectedRoomsSet: ProtectedRoomsSet, + ) { + this.draupnir.reactionHandler.on(WATCH_LISTS_ON_INVITE_PROMPT_LISTENER, this.watchPromptListener); + } + + handleProtectionDisable(): void { + this.draupnir.reactionHandler.off(WATCH_LISTS_ON_INVITE_PROMPT_LISTENER, this.watchPromptListener); + } + + public promptIfPossiblePolicyRoom(candidateRoom: MatrixRoomID, invitation: MembershipEvent): void { + void Task((async () => { + const stateRevisionIssuer = await this.draupnir.roomStateManager.getRoomStateRevisionIssuer(candidateRoom); + if (isError(stateRevisionIssuer)) { + return stateRevisionIssuer.elaborate(`Unable to fetch the room state revision issuer to check if newly joined room was a policy room.`); + } + if (!isRevisionLikelyPolicyRoom(stateRevisionIssuer.ok.currentRevision)) { + return Ok(undefined); + } + const promptResult = await this.promptWatchPolicyRoom(candidateRoom, invitation); + if (isError(promptResult)) { + return promptResult.elaborate(`Unable to send prompt to ask if Draupnir should watch a policy room`); + } + return Ok(undefined); + })()); + } + + private async promptWatchPolicyRoom(candidateRoom: MatrixRoomID, invitation: MembershipEvent) { + const renderPromptWatch = (): DocumentNode => + + {renderMentionPill(invitation.sender, invitation.sender)} has invited me to a policy room + {renderRoomPill(candidateRoom)}, + would you like Draupnir to watch this room as a policy list? + ; + const reactionMap = new Map(Object.entries({ 'OK': 'OK', 'Cancel': 'Cancel' })); + const promptEventID = (await renderMatrixAndSend( + renderPromptWatch(), + this.draupnir.managementRoomID, + undefined, + this.draupnir.client, + this.draupnir.reactionHandler.createAnnotation( + WATCH_LISTS_ON_INVITE_PROMPT_LISTENER, + reactionMap, + { + invited_room: candidateRoom.toPermalink(), + } + ) + ))[0]; + await this.draupnir.reactionHandler.addReactionsToEvent(this.draupnir.client, this.draupnir.managementRoomID, promptEventID, reactionMap); + return Ok(undefined); + } + + private watchListener(key: string, _item: unknown, rawContext: unknown, _reactionMap: Map, promptEvent: RoomEvent): void { + if (key === 'Cancel') { + void Task(this.draupnir.reactionHandler.cancelPrompt(promptEvent)); + return; + } + if (key !== 'OK') { + return; + } + const context = Value.Decode(WatchRoomsOnInvitePromptContext, rawContext); + if (isError(context)) { + log.error(`Could not decode context from prompt event`, context.error); + renderActionResultToEvent(this.draupnir.client, promptEvent, context); + return; + } + void Task((async () => { + const resolvedRoom = await this.draupnir.clientPlatform.toRoomResolver().resolveRoom(context.ok.invited_room); + if (isError(resolvedRoom)) { + resolvedRoom.elaborate(`Could not resolve the policy room to watch from the MatrixRoomReference: ${context.ok.invited_room.toPermalink()}.`); + renderActionResultToEvent(this.draupnir.client, promptEvent, resolvedRoom); + return; + } + const addResult = await this.protectedRoomsSet.issuerManager.watchList('direct', resolvedRoom.ok, {}); + if (isError(addResult)) { + addResult.elaborate(`Could not watch the policy room: ${resolvedRoom.ok.toPermalink()}`); + renderActionResultToEvent(this.draupnir.client, promptEvent, addResult); + return; + } + renderActionResultToEvent(this.draupnir.client, promptEvent, addResult); + void Task(this.draupnir.reactionHandler.completePrompt(promptEvent.room_id, promptEvent.event_id)); + })()); + } +} From b14a8723ca1b0ff63a23a0d620c130fea28b4b1e Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 22 May 2024 13:52:40 +0100 Subject: [PATCH 09/12] Rename ProtectRoomsOnInviteProtection to JoinRoomsOnInviteProtection. --- .../DefaultEnabledProtectionsMigration.ts | 8 ++++---- ...ion.tsx => JoinRoomsOnInviteProtection.tsx} | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) rename src/protections/invitation/{ProtectRoomsOnInviteProtection.tsx => JoinRoomsOnInviteProtection.tsx} (91%) diff --git a/src/protections/DefaultEnabledProtectionsMigration.ts b/src/protections/DefaultEnabledProtectionsMigration.ts index cc7ad730..098d0cce 100644 --- a/src/protections/DefaultEnabledProtectionsMigration.ts +++ b/src/protections/DefaultEnabledProtectionsMigration.ts @@ -6,7 +6,7 @@ import { ActionError,ActionException,ActionExceptionKind,DRAUPNIR_SCHEMA_VERSION_KEY, MjolnirEnabledProtectionsEvent, MjolnirEnabledProtectionsEventType, Ok, SchemedDataManager, Value, findProtection } from "matrix-protection-suite"; import { RedactionSynchronisationProtection } from "./RedactionSynchronisation"; import { PolicyChangeNotification } from "./PolicyChangeNotification"; -import { ProtectRoomsOnInviteProtection } from "./invitation/ProtectRoomsOnInviteProtection"; +import { JoinRoomsOnInviteProtection } from "./invitation/JoinRoomsOnInviteProtection"; export const DefaultEnabledProtectionsMigration = new SchemedDataManager([ async function enableBanPropagationByDefault(input) { @@ -97,16 +97,16 @@ export const DefaultEnabledProtectionsMigration = new SchemedDataManager; +export type JoinRoomsOnInviteProtectionDescription = ProtectionDescription; -export class ProtectRoomsOnInviteProtection - extends AbstractProtection +export class JoinRoomsOnInviteProtection + extends AbstractProtection implements DraupnirProtection< - ProtectRoomsOnInviteProtectionDescription + JoinRoomsOnInviteProtectionDescription > { private readonly promptedToProtectedDeduplicator = new StandardDeduplicator(); private readonly protectRoomsOnInvite = new ProtectroomsOnInvite( @@ -39,8 +39,8 @@ export class ProtectRoomsOnInviteProtection this.protectedRoomsSet ); public constructor( - description: ProtectRoomsOnInviteProtectionDescription, - capabilities: ProtectRoomsOnInviteProtectionCapabilities, + description: JoinRoomsOnInviteProtectionDescription, + capabilities: JoinRoomsOnInviteProtectionCapabilities, protectedRoomsSet: ProtectedRoomsSet, private readonly draupnir: Draupnir, ) { @@ -134,13 +134,13 @@ export class ProtectRoomsOnInviteProtection } describeProtection<{}, Draupnir>({ - name: ProtectRoomsOnInviteProtection.name, + name: JoinRoomsOnInviteProtection.name, description: "Automatically joins rooms when invited by members of the management room and offers to protect them", capabilityInterfaces: {}, defaultCapabilities: {}, factory(description, protectedRoomsSet, draupnir, capabilities, _settings) { return Ok( - new ProtectRoomsOnInviteProtection( + new JoinRoomsOnInviteProtection( description, capabilities, protectedRoomsSet, From 8664d91b1acdf0cb8b69aca6c38295b7d3948f85 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 22 May 2024 15:37:31 +0100 Subject: [PATCH 10/12] Update protect prompt for `config.protectAllJoinedRooms`. Stop sending prompts to protect room when config.protectAllJoinedRooms is enabled. --- src/protections/invitation/JoinRoomsOnInviteProtection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protections/invitation/JoinRoomsOnInviteProtection.tsx b/src/protections/invitation/JoinRoomsOnInviteProtection.tsx index 0ab0d455..aede53bd 100644 --- a/src/protections/invitation/JoinRoomsOnInviteProtection.tsx +++ b/src/protections/invitation/JoinRoomsOnInviteProtection.tsx @@ -125,7 +125,7 @@ export class JoinRoomsOnInviteProtection return joinResult; } void this.watchRoomsOnInvite.promptIfPossiblePolicyRoom(invitedRoomReference, event); - if (!this.protectedRoomsSet.isProtectedRoom(event.room_id)) { + if (!this.draupnir.config.protectAllJoinedRooms && !this.protectedRoomsSet.isProtectedRoom(event.room_id)) { void this.protectRoomsOnInvite.promptToProtect(invitedRoomReference, event); } return Ok(undefined); From 48c0fec1340e0f975353c60a76c1aa07d6d76505 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 22 May 2024 16:15:58 +0100 Subject: [PATCH 11/12] Remove `acceptInvitesFromSpace`. https://github.com/the-draupnir-project/Draupnir/issues/433. Sucks balls, mare. --- .../integration/acceptInvitesFromSpaceTest.ts | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 test/integration/acceptInvitesFromSpaceTest.ts diff --git a/test/integration/acceptInvitesFromSpaceTest.ts b/test/integration/acceptInvitesFromSpaceTest.ts deleted file mode 100644 index cccc5315..00000000 --- a/test/integration/acceptInvitesFromSpaceTest.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { MatrixClient } from "matrix-bot-sdk"; -import { newTestUser } from "./clientHelper"; -import { DraupnirTestContext, draupnirClient } from "./mjolnirSetupUtils"; - -describe("Test: Accept Invites From Space", function() { - let client: MatrixClient|undefined; - this.beforeEach(async function () { - client = await newTestUser(this.config.homeserverUrl, { name: { contains: "spacee" }}); - await client.start(); - }) - this.afterEach(async function () { - client?.stop(); - }) - it("Mjolnir should accept an invite from a user in a nominated Space", async function(this: DraupnirTestContext) { - this.timeout(20000); - - const draupnir = this.draupnir; - const draupnirSyncClient = draupnirClient(); - if (draupnir === undefined || draupnirSyncClient === null) { - throw new TypeError("fixtures.ts didn't setup Draupnir"); - } - - const space = await client!.createSpace({ - name: "mjolnir space invite test", - invites: [draupnir.clientUserID], - isPublic: false - }); - - await draupnir.client.joinRoom(space.roomId); - - // we're mutating a static object, which may affect other tests :( - draupnir.config.autojoinOnlyIfManager = false; - draupnir.config.acceptInvitesFromSpace = space.roomId; - - const promise = new Promise(async resolve => { - const newRoomId = await client!.createRoom({ invite: [draupnir.clientUserID] }); - client!.on("room.event", (roomId, event) => { - if ( - roomId === newRoomId - && event.type === "m.room.member" - && event.sender === draupnir.clientUserID - && event.content?.membership === "join" - ) { - resolve(null); - } - }); - }); - await promise; - } as unknown as Mocha.AsyncFunc); -}); From 16435bd8fc73cceb13eccc26bdac1c1418eccdef Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 22 May 2024 16:35:36 +0100 Subject: [PATCH 12/12] Add some spaces around room pills in protection prompts. --- src/protections/invitation/ProtectRoomsOnInvite.tsx | 2 +- src/protections/invitation/WatchRoomsOnInvite.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/protections/invitation/ProtectRoomsOnInvite.tsx b/src/protections/invitation/ProtectRoomsOnInvite.tsx index bba82cb3..1cd0c6bd 100644 --- a/src/protections/invitation/ProtectRoomsOnInvite.tsx +++ b/src/protections/invitation/ProtectRoomsOnInvite.tsx @@ -46,7 +46,7 @@ export class ProtectroomsOnInvite { void Task((async () => { const renderPromptProtect = (): DocumentNode => - {renderMentionPill(invitation.sender, invitation.sender)} has invited me to + {renderMentionPill(invitation.sender, invitation.sender)} has invited me to {renderRoomPill(candidateRoom)}, would you like to protect this room? ; diff --git a/src/protections/invitation/WatchRoomsOnInvite.tsx b/src/protections/invitation/WatchRoomsOnInvite.tsx index 8e2be120..2fbbd3c8 100644 --- a/src/protections/invitation/WatchRoomsOnInvite.tsx +++ b/src/protections/invitation/WatchRoomsOnInvite.tsx @@ -73,7 +73,7 @@ export class WatchRoomsOnInvite { private async promptWatchPolicyRoom(candidateRoom: MatrixRoomID, invitation: MembershipEvent) { const renderPromptWatch = (): DocumentNode => - {renderMentionPill(invitation.sender, invitation.sender)} has invited me to a policy room + {renderMentionPill(invitation.sender, invitation.sender)} has invited me to a policy room {renderRoomPill(candidateRoom)}, would you like Draupnir to watch this room as a policy list? ;