mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-03-29 10:29:57 +00:00
Figure out how to seperate some logs into their own rooms.
We still need to figure out whether we want to disable the room discovery or enable it by default.
This commit is contained in:
@@ -26,8 +26,12 @@ import {
|
||||
import { DraupnirInterfaceAdaptor } from "./DraupnirCommandPrerequisites";
|
||||
import {
|
||||
MatrixRoomID,
|
||||
MatrixRoomReference,
|
||||
StringRoomID,
|
||||
} from "@the-draupnir-project/matrix-basic-types";
|
||||
import { PolicyChangeNotification } from "../protections/PolicyChangeNotification";
|
||||
import { RoomTakedownProtection } from "../protections/RoomTakedown/RoomTakedownProtection";
|
||||
import { renderRoomPill } from "../commands/interface-manager/MatrixHelpRenderer";
|
||||
|
||||
export const DraupnirStatusCommand = describeCommand({
|
||||
summary: "Show the status of the bot.",
|
||||
@@ -49,7 +53,8 @@ export type StatusInfo = {
|
||||
version: string;
|
||||
repository: string;
|
||||
documentationURL: string;
|
||||
} & WatchedPolicyRoomsInfo;
|
||||
} & DraupnirNotificationRoomsInfo &
|
||||
WatchedPolicyRoomsInfo;
|
||||
|
||||
export function groupWatchedPolicyRoomsByProtectionStatus(
|
||||
watchedPolicyRooms: WatchedPolicyRooms,
|
||||
@@ -92,6 +97,28 @@ DraupnirInterfaceAdaptor.describeRenderer(DraupnirStatusCommand, {
|
||||
},
|
||||
});
|
||||
|
||||
type DraupnirNotificationRoomsInfo = {
|
||||
readonly policyChangeNotificationRoomID: StringRoomID | undefined;
|
||||
readonly roomDiscoveryNotificationRoomID: StringRoomID | undefined;
|
||||
};
|
||||
|
||||
function extractProtectionNotificationRooms(
|
||||
draupnir: Draupnir
|
||||
): DraupnirNotificationRoomsInfo {
|
||||
return {
|
||||
policyChangeNotificationRoomID: (
|
||||
draupnir.protectedRoomsSet.protections.findEnabledProtection(
|
||||
PolicyChangeNotification.name
|
||||
) as PolicyChangeNotification | undefined
|
||||
)?.notificationRoomID,
|
||||
roomDiscoveryNotificationRoomID: (
|
||||
draupnir.protectedRoomsSet.protections.findEnabledProtection(
|
||||
RoomTakedownProtection.name
|
||||
) as RoomTakedownProtection | undefined
|
||||
)?.discoveryNotificationRoom,
|
||||
};
|
||||
}
|
||||
|
||||
// FIXME: need a shoutout to dependencies in here and NOTICE info.
|
||||
export function draupnirStatusInfo(draupnir: Draupnir): StatusInfo {
|
||||
const watchedListInfo = groupWatchedPolicyRoomsByProtectionStatus(
|
||||
@@ -109,6 +136,7 @@ export function draupnirStatusInfo(draupnir: Draupnir): StatusInfo {
|
||||
documentationURL: DOCUMENTATION_URL,
|
||||
version: SOFTWARE_VERSION,
|
||||
repository: PACKAGE_JSON["repository"] ?? "Unknown",
|
||||
...extractProtectionNotificationRooms(draupnir),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -129,6 +157,44 @@ export function renderPolicyList(list: WatchedPolicyRoom): DocumentNode {
|
||||
);
|
||||
}
|
||||
|
||||
function renderNotificationRooms(info: StatusInfo): DocumentNode {
|
||||
if (
|
||||
info.roomDiscoveryNotificationRoomID === undefined &&
|
||||
info.policyChangeNotificationRoomID === undefined
|
||||
) {
|
||||
return <fragment></fragment>;
|
||||
}
|
||||
const renderNotificationRoom = (
|
||||
name: string,
|
||||
roomID: StringRoomID | undefined
|
||||
) => {
|
||||
if (roomID === undefined) {
|
||||
return <fragment></fragment>;
|
||||
}
|
||||
return (
|
||||
<li>
|
||||
{name}: {renderRoomPill(MatrixRoomReference.fromRoomID(roomID))}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<fragment>
|
||||
<b>Notification rooms:</b>
|
||||
<br />
|
||||
<ul>
|
||||
{renderNotificationRoom(
|
||||
"Policy change",
|
||||
info.policyChangeNotificationRoomID
|
||||
)}
|
||||
{renderNotificationRoom(
|
||||
"Room discovery",
|
||||
info.roomDiscoveryNotificationRoomID
|
||||
)}
|
||||
</ul>
|
||||
</fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderStatusInfo(info: StatusInfo): DocumentNode {
|
||||
const renderPolicyLists = (header: string, lists: WatchedPolicyRoom[]) => {
|
||||
const renderedLists = lists.map(renderPolicyList);
|
||||
@@ -161,6 +227,7 @@ export function renderStatusInfo(info: StatusInfo): DocumentNode {
|
||||
"Subscribed and protected policy rooms",
|
||||
info.subscribedAndProtectedLists
|
||||
)}
|
||||
{renderNotificationRooms(info)}
|
||||
<b>Version: </b>
|
||||
<code>{info.version}</code>
|
||||
<br />
|
||||
|
||||
170
src/protections/NotificationRoom/NotificationRoom.ts
Normal file
170
src/protections/NotificationRoom/NotificationRoom.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
import { Ok, Result, ResultError } from "@gnuxie/typescript-result";
|
||||
import {
|
||||
MatrixRoomID,
|
||||
StringRoomID,
|
||||
StringUserID,
|
||||
} from "@the-draupnir-project/matrix-basic-types";
|
||||
import {
|
||||
isError,
|
||||
Logger,
|
||||
ProtectedRoomsManager,
|
||||
ProtectionDescription,
|
||||
RoomCreator,
|
||||
RoomMembershipManager,
|
||||
RoomStateEventSender,
|
||||
} from "matrix-protection-suite";
|
||||
import { Draupnir } from "../../Draupnir";
|
||||
|
||||
export type SettingChangeAndProtectionEnableCB = (
|
||||
roomID: StringRoomID
|
||||
) => Promise<Result<void>>;
|
||||
|
||||
export class NotificationRoomCreator {
|
||||
public constructor(
|
||||
private readonly protectedRoomsManager: ProtectedRoomsManager,
|
||||
private readonly settingChangeCB: SettingChangeAndProtectionEnableCB,
|
||||
private readonly roomCreateCapability: RoomCreator,
|
||||
private readonly roomStateEventCapability: RoomStateEventSender,
|
||||
private readonly roomName: string,
|
||||
private readonly draupnirUserID: StringUserID,
|
||||
private readonly draupnirManagementRoomID: StringRoomID,
|
||||
private readonly draupnirModerators: StringUserID[],
|
||||
private readonly log: Logger
|
||||
) {
|
||||
// nothing to do.
|
||||
}
|
||||
|
||||
public static async extractMembersFromManagementRoom(
|
||||
managementRoom: MatrixRoomID,
|
||||
draupnirUserID: StringUserID,
|
||||
membershipManager: RoomMembershipManager
|
||||
): Promise<Result<StringUserID[]>> {
|
||||
const membershipRevisionIssuer =
|
||||
await membershipManager.getRoomMembershipRevisionIssuer(managementRoom);
|
||||
if (isError(membershipRevisionIssuer)) {
|
||||
return membershipRevisionIssuer;
|
||||
}
|
||||
const revision = membershipRevisionIssuer.ok.currentRevision;
|
||||
return Ok(
|
||||
[...revision.members()]
|
||||
.map((member) => member.userID)
|
||||
.filter((userID) => userID !== draupnirUserID)
|
||||
);
|
||||
}
|
||||
|
||||
public static async createNotificationRoomFromDraupnir(
|
||||
draupnir: Draupnir,
|
||||
description: ProtectionDescription,
|
||||
settings: Record<string, unknown>,
|
||||
notificationRoomSettingName: string,
|
||||
notificationRoomName: string,
|
||||
log: Logger
|
||||
): Promise<Result<never>> {
|
||||
const moderators =
|
||||
await NotificationRoomCreator.extractMembersFromManagementRoom(
|
||||
draupnir.managementRoom,
|
||||
draupnir.clientUserID,
|
||||
draupnir.roomMembershipManager
|
||||
);
|
||||
if (isError(moderators)) {
|
||||
return moderators.elaborate("Unable to find the draupnir moderators");
|
||||
}
|
||||
const notificationRoomCreator = new NotificationRoomCreator(
|
||||
draupnir.protectedRoomsSet.protectedRoomsManager,
|
||||
async function (roomID: StringRoomID) {
|
||||
const newSettings = description.protectionSettings
|
||||
.toMirror()
|
||||
.setValue(settings, notificationRoomSettingName, roomID);
|
||||
if (isError(newSettings)) {
|
||||
return newSettings;
|
||||
}
|
||||
const result =
|
||||
await draupnir.protectedRoomsSet.protections.changeProtectionSettings(
|
||||
description as unknown as ProtectionDescription,
|
||||
draupnir.protectedRoomsSet,
|
||||
draupnir,
|
||||
newSettings.ok
|
||||
);
|
||||
if (isError(result)) {
|
||||
return result.elaborate(
|
||||
"Unable to add the notification room to the protection settings"
|
||||
);
|
||||
}
|
||||
return Ok(undefined);
|
||||
},
|
||||
draupnir.clientPlatform.toRoomCreator(),
|
||||
draupnir.clientPlatform.toRoomStateEventSender(),
|
||||
notificationRoomName,
|
||||
draupnir.clientUserID,
|
||||
draupnir.managementRoomID,
|
||||
moderators.ok,
|
||||
log
|
||||
);
|
||||
return await notificationRoomCreator.createMissingNotificationRoom();
|
||||
}
|
||||
|
||||
public async createMissingNotificationRoom(): Promise<Result<never>> {
|
||||
const roomTitle = `${this.draupnirUserID}'s ${this.roomName}`;
|
||||
const newRoom = await this.roomCreateCapability.createRoom({
|
||||
preset: "private_chat",
|
||||
name: roomTitle,
|
||||
invite: this.draupnirModerators,
|
||||
});
|
||||
if (isError(newRoom)) {
|
||||
this.log.error(
|
||||
`Failed to create notification room for ${this.roomName}`,
|
||||
newRoom.error
|
||||
);
|
||||
return newRoom;
|
||||
}
|
||||
const protectRoomResult = await this.protectedRoomsManager.addRoom(
|
||||
newRoom.ok
|
||||
);
|
||||
if (isError(protectRoomResult)) {
|
||||
this.log.error(
|
||||
`Failed to protect notification room for ${this.roomName}`,
|
||||
protectRoomResult.error
|
||||
);
|
||||
return protectRoomResult;
|
||||
}
|
||||
const protectionEnableResult = await this.settingChangeCB(
|
||||
newRoom.ok.toRoomIDOrAlias()
|
||||
);
|
||||
const restrictionResult =
|
||||
await this.roomStateEventCapability.sendStateEvent(
|
||||
newRoom.ok,
|
||||
"m.room.join_rules",
|
||||
"",
|
||||
{
|
||||
join_rule: "restricted",
|
||||
allow: [
|
||||
{
|
||||
room_id: this.draupnirManagementRoomID,
|
||||
type: "m.room_membership",
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
if (isError(restrictionResult)) {
|
||||
this.log.error(
|
||||
`Failed to restrict notification room for ${this.roomName}`,
|
||||
restrictionResult.error
|
||||
);
|
||||
return restrictionResult;
|
||||
}
|
||||
if (isError(protectionEnableResult)) {
|
||||
this.log.error(
|
||||
`Failed to enable protection for notification room for ${this.roomName}`,
|
||||
protectionEnableResult.error
|
||||
);
|
||||
return protectionEnableResult;
|
||||
}
|
||||
return ResultError.Result(
|
||||
`A notification room titled "${roomTitle}" has been created for this protection's messages, and the protection has been restarted separately`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
import {
|
||||
AbstractProtection,
|
||||
ActionResult,
|
||||
EDStatic,
|
||||
Logger,
|
||||
Ok,
|
||||
PolicyListRevision,
|
||||
@@ -20,7 +21,7 @@ import {
|
||||
PolicyRuleMatchType,
|
||||
ProtectedRoomsSet,
|
||||
ProtectionDescription,
|
||||
UnknownConfig,
|
||||
StringRoomIDSchema,
|
||||
describeProtection,
|
||||
isError,
|
||||
} from "matrix-protection-suite";
|
||||
@@ -40,15 +41,32 @@ import {
|
||||
} from "@the-draupnir-project/interface-manager";
|
||||
import { sendMatrixEventsFromDeadDocument } from "../commands/interface-manager/MPSMatrixInterfaceAdaptor";
|
||||
import { renderRuleHashes, renderRuleClearText } from "../commands/Rules";
|
||||
import { NotificationRoomCreator } from "./NotificationRoom/NotificationRoom";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
const log = new Logger("PolicyChangeNotification");
|
||||
|
||||
// FIXME: Add these rooms to the status command!!.
|
||||
|
||||
const PolicyChangeNotificationSettings = Type.Object({
|
||||
notificationRoomID: Type.Optional(
|
||||
Type.Union([StringRoomIDSchema, Type.Undefined()], {
|
||||
default: undefined,
|
||||
description: "The room where notifications should be sent.",
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export type PolicyChangeNotificationSettings = EDStatic<
|
||||
typeof PolicyChangeNotificationSettings
|
||||
>;
|
||||
|
||||
export type PolicyChangeNotificationCapabilitites = Record<never, never>;
|
||||
|
||||
export type PolicyChangeNotificationProtectionDescription =
|
||||
ProtectionDescription<
|
||||
Draupnir,
|
||||
UnknownConfig,
|
||||
typeof PolicyChangeNotificationSettings,
|
||||
PolicyChangeNotificationCapabilitites
|
||||
>;
|
||||
|
||||
@@ -62,7 +80,8 @@ export class PolicyChangeNotification
|
||||
description: PolicyChangeNotificationProtectionDescription,
|
||||
capabilities: PolicyChangeNotificationCapabilitites,
|
||||
protectedRoomsSet: ProtectedRoomsSet,
|
||||
private readonly draupnir: Draupnir
|
||||
private readonly draupnir: Draupnir,
|
||||
public readonly notificationRoomID: StringRoomID
|
||||
) {
|
||||
super(description, capabilities, protectedRoomsSet, {});
|
||||
}
|
||||
@@ -92,7 +111,7 @@ export class PolicyChangeNotification
|
||||
}
|
||||
const sendResult = await sendMatrixEventsFromDeadDocument(
|
||||
this.draupnir.clientPlatform.toRoomMessageSender(),
|
||||
this.draupnir.managementRoomID,
|
||||
this.notificationRoomID,
|
||||
<root>{renderGroupedChanges(groupedChanges.ok)}</root>,
|
||||
{}
|
||||
);
|
||||
@@ -159,24 +178,40 @@ function renderGroupedChanges(groupedChanges: GroupedChange[]): DocumentNode {
|
||||
return <fragment>{groupedChanges.map(renderListChanges)}</fragment>;
|
||||
}
|
||||
|
||||
describeProtection<PolicyChangeNotificationCapabilitites, Draupnir>({
|
||||
describeProtection<
|
||||
PolicyChangeNotificationCapabilitites,
|
||||
Draupnir,
|
||||
typeof PolicyChangeNotificationSettings
|
||||
>({
|
||||
name: PolicyChangeNotification.name,
|
||||
description: "Provides notification of policy changes from watched lists.",
|
||||
capabilityInterfaces: {},
|
||||
defaultCapabilities: {},
|
||||
configSchema: PolicyChangeNotificationSettings,
|
||||
async factory(
|
||||
description,
|
||||
protectedRoomsSet,
|
||||
draupnir,
|
||||
capabilities,
|
||||
_settings
|
||||
settings
|
||||
) {
|
||||
if (settings.notificationRoomID === undefined) {
|
||||
return await NotificationRoomCreator.createNotificationRoomFromDraupnir(
|
||||
draupnir,
|
||||
description as unknown as ProtectionDescription,
|
||||
settings,
|
||||
"notificationRoomID",
|
||||
"Policy Change Notifications",
|
||||
log
|
||||
);
|
||||
}
|
||||
return Ok(
|
||||
new PolicyChangeNotification(
|
||||
description,
|
||||
capabilities,
|
||||
protectedRoomsSet,
|
||||
draupnir
|
||||
draupnir,
|
||||
settings.notificationRoomID
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
@@ -38,6 +38,7 @@ import { wrapInRoot } from "../../commands/interface-manager/MatrixHelpRenderer"
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { EDStatic } from "matrix-protection-suite/dist/Interface/Static";
|
||||
import { renderDiscoveredRoom } from "./RoomDiscoveryRenderer";
|
||||
import { NotificationRoomCreator } from "../NotificationRoom/NotificationRoom";
|
||||
|
||||
const log = new Logger("RoomTakedownProtection");
|
||||
|
||||
@@ -52,8 +53,7 @@ const RoomTakedownProtectionSettings = Type.Object(
|
||||
discoveryNotificationRoom: Type.Optional(
|
||||
Type.Union([StringRoomIDSchema, Type.Undefined()], {
|
||||
default: undefined,
|
||||
description:
|
||||
"The room where notifications should be sent. Currently broken and needs to be edited from a state event while we figure something out",
|
||||
description: "The room where notifications should be sent.",
|
||||
})
|
||||
),
|
||||
discoveryNotificationEnabled: Type.Boolean({
|
||||
@@ -92,7 +92,7 @@ export class RoomTakedownProtection
|
||||
private readonly roomMessageSender: RoomMessageSender,
|
||||
private readonly discoveryNotificationEnabled: boolean,
|
||||
private readonly discoveryNotificationMembershipThreshold: number,
|
||||
private readonly discoveryNotificationRoom: StringRoomID,
|
||||
public readonly discoveryNotificationRoom: StringRoomID,
|
||||
private readonly roomDiscovery: RoomDiscovery | undefined
|
||||
) {
|
||||
super(description, capabilities, protectedRoomsSet, {});
|
||||
@@ -173,6 +173,19 @@ describeProtection<
|
||||
capabilitySet,
|
||||
settings
|
||||
) {
|
||||
if (
|
||||
settings.discoveryNotificationEnabled &&
|
||||
settings.discoveryNotificationRoom === undefined
|
||||
) {
|
||||
return await NotificationRoomCreator.createNotificationRoomFromDraupnir(
|
||||
draupnir,
|
||||
description as unknown as ProtectionDescription,
|
||||
settings,
|
||||
"discoveryNotificationRoom",
|
||||
"Room Discovery Notification",
|
||||
log
|
||||
);
|
||||
}
|
||||
if (
|
||||
draupnir.stores.hashStore === undefined ||
|
||||
draupnir.stores.roomAuditLog === undefined
|
||||
|
||||
Reference in New Issue
Block a user