From 1041105aa8c3f587df2f6ae6d70bfda3dc633aa3 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Tue, 14 May 2024 23:42:35 +0100 Subject: [PATCH] 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); +}