mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-05-13 19:13:19 +00:00
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.
This commit is contained in:
+37
-72
@@ -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<ActionResult<Draupnir>> {
|
||||
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<void> {
|
||||
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 <code>!mjolnir rooms add ${htmlEscape(inviteEvent.room_id)}</code> `
|
||||
+ `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
|
||||
|
||||
@@ -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<MatrixContext, BaseFunction> = async function tickCrossRenderer(this: MatrixInterfaceAdaptor<MatrixContext, BaseFunction>, client: MatrixSendClient, commandRoomID: StringRoomID, event: RoomEvent, result: ActionResult<unknown>): Promise<void> {
|
||||
const react = async (emote: string) => {
|
||||
export async function reactToEventWithResult(client: MatrixSendClient, event: RoomEvent, result: ActionResult<unknown>): Promise<ActionResult<void>> {
|
||||
// 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<ActionResult<void>> => {
|
||||
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<ActionResult<void>> {
|
||||
try {
|
||||
await renderMatrixAndSend(
|
||||
<root>
|
||||
<details>
|
||||
<summary>{error.mostRelevantElaboration}</summary>
|
||||
{renderDetailsNotice(error)}
|
||||
{renderElaborationTrail(error)}
|
||||
{renderExceptionTrail(error)}
|
||||
</details>
|
||||
</root>,
|
||||
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>): void {
|
||||
if (isError(result)) {
|
||||
void Task(replyToEventWithErrorDetails(client, event, result.error));
|
||||
}
|
||||
void Task(reactToEventWithResult(client, event, result));
|
||||
}
|
||||
|
||||
export const tickCrossRenderer: RendererSignature<MatrixContext, BaseFunction> = async function tickCrossRenderer(this: MatrixInterfaceAdaptor<MatrixContext, BaseFunction>, client: MatrixSendClient, commandRoomID: StringRoomID, event: RoomEvent, result: ActionResult<unknown>): Promise<void> {
|
||||
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<MatrixContext, BaseFunction> =
|
||||
} else {
|
||||
await client.replyNotice(commandRoomID, event, result.error.message);
|
||||
}
|
||||
// reacting is way less important than communicating what happened, do it last.
|
||||
await react('❌');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ReactionListener>): 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
|
||||
|
||||
@@ -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<MjolnirEnabledProtectionsEvent>([
|
||||
async function enableBanPropagationByDefault(input) {
|
||||
@@ -95,5 +96,26 @@ export const DefaultEnabledProtectionsMigration = new SchemedDataManager<Mjolnir
|
||||
enabled: [...enabledProtections],
|
||||
[DRAUPNIR_SCHEMA_VERSION_KEY]: 4,
|
||||
});
|
||||
},
|
||||
async function enableProtectRoomsOnInviteProtection(input) {
|
||||
if (!Value.Check(MjolnirEnabledProtectionsEvent, input)) {
|
||||
return ActionError.Result(
|
||||
`The data for ${MjolnirEnabledProtectionsEventType} is corrupted.`
|
||||
);
|
||||
}
|
||||
const enabledProtections = new Set(input.enabled);
|
||||
const protection = findProtection(ProtectRoomsOnInviteProtection.name);
|
||||
if (protection === undefined) {
|
||||
const message = `Cannot find the ${ProtectRoomsOnInviteProtection.name} protection`;
|
||||
return ActionException.Result(message, {
|
||||
exception: new TypeError(message),
|
||||
exceptionKind: ActionExceptionKind.Unknown
|
||||
});
|
||||
}
|
||||
enabledProtections.add(protection.name);
|
||||
return Ok({
|
||||
enabled: [...enabledProtections],
|
||||
[DRAUPNIR_SCHEMA_VERSION_KEY]: 5,
|
||||
});
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
// Copyright 2022 - 2024 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0
|
||||
//
|
||||
// SPDX-FileAttributionText: <text>
|
||||
// This modified file incorporates work from mjolnir
|
||||
// https://github.com/matrix-org/mjolnir
|
||||
// </text>
|
||||
|
||||
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<Draupnir, {}, ProtectRoomsOnInviteProtectionCapabilities>;
|
||||
|
||||
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<typeof ProtectRoomsOnInvitePromptContext>;
|
||||
|
||||
export class ProtectRoomsOnInviteProtection
|
||||
extends AbstractProtection<ProtectRoomsOnInviteProtectionDescription>
|
||||
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<ActionResult<void>> {
|
||||
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 <root>
|
||||
{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.<br/>
|
||||
If you would like this room protected, use <code>!draupnir rooms add {event.room_id}</code>
|
||||
</root>
|
||||
}
|
||||
void Task((async () => {
|
||||
renderMatrixAndSend(
|
||||
renderUnknownInvite(),
|
||||
this.draupnir.managementRoomID,
|
||||
undefined,
|
||||
this.draupnir.client
|
||||
);
|
||||
return Ok(undefined)
|
||||
})());
|
||||
}
|
||||
|
||||
private async joinInvitedRoom(event: MembershipEvent, room: MatrixRoomReference): Promise<ActionResult<MatrixRoomReference>> {
|
||||
const renderFailedTojoin = (error: ActionError) => {
|
||||
const title = <fragment>Unfortunatley I was unable to accept the invitation from {renderMentionPill(event.sender, event.sender)} to the room {renderRoomPill(room)}.</fragment>;
|
||||
return <root>
|
||||
{renderFailedSingularConsequence(this.description, title, error)}
|
||||
</root>
|
||||
};
|
||||
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<ActionResult<void>> {
|
||||
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 =>
|
||||
<root>
|
||||
{renderMentionPill(event.sender, event.sender)} has invited me to
|
||||
{renderRoomPill(invitedRoomReference)},
|
||||
would you like to protect this room?
|
||||
</root>;
|
||||
const reactionMap = new Map<string, string>(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<string, unknown>, 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
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user