Modify 'listInfo' from status command.

This function now distinguishes between lists we are watching, lists
that are protected, and lists that we have parted from.

I don't like it. This should exist on the `PolicyRoomsManager` and
this is a complete and total mess. I don't know if there is time to
fix it yet.
This commit is contained in:
gnuxie committed 2025-02-10 10:07:51 +00:00
1 parent e9e163e9f3
commit 5d727d7114
10 files changed
+212 -99

No files matched your search

+9 -2
View File
@@ -298,17 +298,24 @@ export class Draupnir implements Client, MatrixAdaptorContext {
* This means this is only used in the index.ts.
*/
public async startupComplete(): Promise<void> {
const statusInfo = await draupnirStatusInfo(this);
try {
await this.managementRoomOutput.logMessage(
LogLevel.INFO,
"Draupnir@startup",
"Startup complete. Now monitoring rooms."
);
const statusInfo = await draupnirStatusInfo(this);
if (isError(statusInfo)) {
log.error(
`Was unable to get the status information for draupnir`,
statusInfo.error
);
return;
}
await sendMatrixEventsFromDeadDocument(
this.clientPlatform.toRoomMessageSender(),
this.managementRoomID,
renderStatusInfo(statusInfo),
renderStatusInfo(statusInfo.ok),
{}
);
} catch (ex) {
+10
View File
@@ -20,8 +20,10 @@ import {
} from "matrix-protection-suite";
import { findPolicyRoomIDFromShortcode } from "./CreateBanListCommand";
import {
MatrixRoomID,
MatrixRoomReference,
MatrixUserID,
StringRoomID,
StringUserID,
} from "@the-draupnir-project/matrix-basic-types";
import {
@@ -57,6 +59,8 @@ export type DraupnirBanCommandContext = {
defaultReasons: string[];
roomResolver: RoomResolver;
clientUserID: StringUserID;
allJoinedRooms: StringRoomID[];
protectedRooms: MatrixRoomID[];
};
export const DraupnirBanCommand = describeCommand({
@@ -108,6 +112,8 @@ export const DraupnirBanCommand = describeCommand({
policyRoomManager,
roomResolver,
clientUserID,
allJoinedRooms,
protectedRooms,
}: DraupnirBanCommandContext,
_info: BasicInvocationInformation,
_keywords,
@@ -120,6 +126,8 @@ export const DraupnirBanCommand = describeCommand({
? await findPolicyRoomIDFromShortcode(
issuerManager,
policyRoomManager,
allJoinedRooms,
protectedRooms,
clientUserID,
policyRoomDesignator
)
@@ -172,6 +180,8 @@ DraupnirContextToCommandContextTranslator.registerTranslation(
defaultReasons: draupnir.config.commands.ban.defaultReasons,
roomResolver: draupnir.clientPlatform.toRoomResolver(),
clientUserID: draupnir.clientUserID,
allJoinedRooms: draupnir.clientRooms.currentRevision.allJoinedRooms,
protectedRooms: draupnir.protectedRoomsSet.allProtectedRooms,
};
}
);
+16 -4
View File
@@ -18,10 +18,11 @@ import {
PropagationType,
isError,
} from "matrix-protection-suite";
import { listInfo } from "./StatusCommand";
import { getWatchedPolicyRoomsInfo } from "./StatusCommand";
import { Draupnir } from "../Draupnir";
import {
MatrixRoomID,
StringRoomID,
StringUserID,
} from "@the-draupnir-project/matrix-basic-types";
import {
@@ -91,13 +92,24 @@ DraupnirInterfaceAdaptor.describeRenderer(DraupnirListCreateCommand, {
export async function findPolicyRoomIDFromShortcode(
issuerManager: PolicyListConfig,
policyRoomManager: PolicyRoomManager,
allJoinedRooms: StringRoomID[],
protectedRooms: MatrixRoomID[],
editingClientUserID: StringUserID,
shortcode: string
): Promise<ActionResult<MatrixRoomID>> {
const info = await listInfo(issuerManager, policyRoomManager);
const matchingRevisions = info.filter(
(list) => list.revision.shortcode === shortcode
const info = await getWatchedPolicyRoomsInfo(
issuerManager,
policyRoomManager,
allJoinedRooms,
protectedRooms
);
if (isError(info)) {
return info;
}
const matchingRevisions = [
...info.ok.subscribedAndProtectedLists,
...info.ok.subscribedLists,
].filter((list) => list.revision.shortcode === shortcode);
if (matchingRevisions.length === 0 || matchingRevisions[0] === undefined) {
return ActionError.Result(
`Could not find a policy room from the shortcode: ${shortcode}`
+18 -27
View File
@@ -31,8 +31,8 @@ import { Draupnir } from "../Draupnir";
import { Result } from "@gnuxie/typescript-result";
import { DraupnirInterfaceAdaptor } from "./DraupnirCommandPrerequisites";
import {
listInfo as getListInfo,
ListInfo,
getWatchedPolicyRoomsInfo,
WatchedPolicyRoomInfo,
renderPolicyList,
} from "./StatusCommand";
@@ -43,9 +43,9 @@ type RoomListItem = {
};
type ListRoomsCommandInfo = {
joinedAndProtectedLists: ListInfo[];
joinedAndWatchedLists: ListInfo[];
partedAndWatchedLists: ListInfo[];
joinedAndProtectedLists: WatchedPolicyRoomInfo[];
joinedAndWatchedLists: WatchedPolicyRoomInfo[];
partedAndWatchedLists: RoomListItem[];
joinedAndProtectedRooms: RoomListItem[];
joinedAndUnprotectedRooms: RoomListItem[];
partedAndProtectedRooms: RoomListItem[];
@@ -57,10 +57,15 @@ export const DraupnirListProtectedRoomsCommand = describeCommand({
async executor(draupnir: Draupnir): Promise<Result<ListRoomsCommandInfo>> {
const allJoinedRooms = draupnir.clientRooms.currentRevision.allJoinedRooms;
const allProtectedRooms = draupnir.protectedRoomsSet.allProtectedRooms;
const listInfo = await getListInfo(
const listInfo = await getWatchedPolicyRoomsInfo(
draupnir.protectedRoomsSet.issuerManager,
draupnir.policyRoomManager
draupnir.policyRoomManager,
draupnir.clientRooms.currentRevision.allJoinedRooms,
draupnir.protectedRoomsSet.allProtectedRooms
);
if (isError(listInfo)) {
return listInfo;
}
const makeRoomListItem = (room: MatrixRoomID) => {
const revision = draupnir.protectedRoomsSet.setRoomState.getRevision(
room.toRoomIDOrAlias()
@@ -72,24 +77,10 @@ export const DraupnirListProtectedRoomsCommand = describeCommand({
}
};
return Ok({
joinedAndProtectedLists: listInfo.filter(
(list) =>
allJoinedRooms.includes(list.revision.room.toRoomIDOrAlias()) &&
allProtectedRooms.find(
(room) =>
room.toRoomIDOrAlias() === list.revision.room.toRoomIDOrAlias()
)
),
joinedAndWatchedLists: listInfo.filter(
(list) =>
allJoinedRooms.includes(list.revision.room.toRoomIDOrAlias()) &&
!allProtectedRooms.find(
(room) =>
room.toRoomIDOrAlias() === list.revision.room.toRoomIDOrAlias()
)
),
partedAndWatchedLists: listInfo.filter(
(list) => !allJoinedRooms.includes(list.revision.room.toRoomIDOrAlias())
joinedAndProtectedLists: listInfo.ok.subscribedAndProtectedLists,
joinedAndWatchedLists: listInfo.ok.subscribedLists,
partedAndWatchedLists: listInfo.ok.subscribedButPartedLists.map(
(profile) => makeRoomListItem(profile.room)
),
joinedAndProtectedRooms: allProtectedRooms
.filter((room) => allJoinedRooms.includes(room.toRoomIDOrAlias()))
@@ -110,7 +101,7 @@ export const DraupnirListProtectedRoomsCommand = describeCommand({
});
function renderPolicyLists(
rooms: ListInfo[],
rooms: WatchedPolicyRoomInfo[],
options: { name: string }
): DocumentNode {
if (rooms.length === 0) {
@@ -178,7 +169,7 @@ DraupnirInterfaceAdaptor.describeRenderer(DraupnirListProtectedRoomsCommand, {
{renderPolicyLists(result.ok.joinedAndWatchedLists, {
name: "Joined and watched unprotected policy rooms",
})}
{renderPolicyLists(result.ok.partedAndWatchedLists, {
{renderRoomList(result.ok.partedAndWatchedLists, {
name: "Parted policy rooms that are still marked as watched",
})}
{renderRoomList(result.ok.joinedAndProtectedRooms, {
+23 -7
View File
@@ -14,7 +14,7 @@ import {
PolicyRule,
isError,
} from "matrix-protection-suite";
import { listInfo } from "./StatusCommand";
import { getWatchedPolicyRoomsInfo } from "./StatusCommand";
import {
StringRoomID,
MatrixRoomID,
@@ -90,12 +90,20 @@ export const DraupnirListRulesCommand = describeCommand({
summary: "Lists the rules currently in use by Draupnir.",
parameters: [],
async executor(draupnir: Draupnir): Promise<Result<ListMatches[]>> {
const infoResult = await listInfo(
const infoResult = await getWatchedPolicyRoomsInfo(
draupnir.protectedRoomsSet.issuerManager,
draupnir.policyRoomManager
draupnir.policyRoomManager,
draupnir.clientRooms.currentRevision.allJoinedRooms,
draupnir.protectedRoomsSet.allProtectedRooms
);
if (isError(infoResult)) {
return infoResult;
}
return Ok(
infoResult.map((policyRoom) => ({
[
...infoResult.ok.subscribedAndProtectedLists,
...infoResult.ok.subscribedLists,
].map((policyRoom) => ({
room: policyRoom.revision.room,
roomID: policyRoom.revision.room.toRoomIDOrAlias(),
profile: policyRoom.watchedListProfile,
@@ -127,12 +135,20 @@ export const DraupnirRulesMatchingCommand = describeCommand({
_rest,
entity
): Promise<Result<ListMatches[]>> {
const policyRooms = await listInfo(
const policyRooms = await getWatchedPolicyRoomsInfo(
draupnir.protectedRoomsSet.issuerManager,
draupnir.policyRoomManager
draupnir.policyRoomManager,
draupnir.clientRooms.currentRevision.allJoinedRooms,
draupnir.protectedRoomsSet.allProtectedRooms
);
if (isError(policyRooms)) {
return policyRooms;
}
return Ok(
policyRooms.map((policyRoom) => {
[
...policyRooms.ok.subscribedAndProtectedLists,
...policyRooms.ok.subscribedLists,
].map((policyRoom) => {
return {
room: policyRoom.revision.room,
roomID: policyRoom.revision.room.toRoomIDOrAlias(),
+92 -51
View File
@@ -1,4 +1,4 @@
// Copyright 2022 Gnuxie <Gnuxie@protonmail.com>
// Copyright 2022 - 2025 Gnuxie <Gnuxie@protonmail.com>
// Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0
@@ -26,60 +26,103 @@ import {
describeCommand,
} from "@the-draupnir-project/interface-manager";
import { DraupnirInterfaceAdaptor } from "./DraupnirCommandPrerequisites";
import {
MatrixRoomID,
StringRoomID,
} from "@the-draupnir-project/matrix-basic-types";
import { Result } from "@gnuxie/typescript-result";
export const DraupnirStatusCommand = describeCommand({
summary: "Show the status of the bot.",
parameters: [],
async executor(draupnir: Draupnir): Promise<ActionResult<StatusInfo>> {
return Ok(await draupnirStatusInfo(draupnir));
return await draupnirStatusInfo(draupnir);
},
});
export interface ListInfo {
export interface WatchedPolicyRoomInfo {
watchedListProfile: PolicyRoomWatchProfile;
revision: PolicyRoomRevision;
}
export interface StatusInfo {
export type WatchedPolicyRoomsInfo = {
subscribedLists: WatchedPolicyRoomInfo[];
subscribedAndProtectedLists: WatchedPolicyRoomInfo[];
subscribedButPartedLists: PolicyRoomWatchProfile[];
};
export type StatusInfo = {
numberOfProtectedRooms: number;
numberOfUniqueMembers: number;
subscribedLists: ListInfo[];
subscribedAndProtectedLists: ListInfo[];
version: string;
repository: string;
documentationURL: string;
} & WatchedPolicyRoomsInfo;
async function findRevisionIssuers(
policyRoomManager: PolicyRoomManager,
profiles: PolicyRoomWatchProfile[]
): Promise<Result<WatchedPolicyRoomInfo[]>> {
const issuers: WatchedPolicyRoomInfo[] = [];
for (const profile of profiles) {
const issuerResult = await policyRoomManager.getPolicyRoomRevisionIssuer(
profile.room
);
if (isError(issuerResult)) {
return issuerResult.elaborate(
"Unable to find a policy room revision issuer for a watched policy list"
);
}
issuers.push({
watchedListProfile: profile,
revision: issuerResult.ok.currentRevision,
});
}
return Ok(issuers);
}
export async function listInfo(
export async function getWatchedPolicyRoomsInfo(
issuerManager: PolicyListConfig,
policyRoomManager: PolicyRoomManager
): Promise<ListInfo[]> {
policyRoomManager: PolicyRoomManager,
allJoinedRooms: StringRoomID[],
protectedRooms: MatrixRoomID[]
): Promise<Result<WatchedPolicyRoomsInfo>> {
const watchedListProfiles = issuerManager.allWatchedLists;
const issuerResults = await Promise.all(
watchedListProfiles.map((profile) =>
policyRoomManager.getPolicyRoomRevisionIssuer(profile.room)
const subscribedAndProtectedLists = await findRevisionIssuers(
policyRoomManager,
watchedListProfiles.filter(
(profile) =>
allJoinedRooms.includes(profile.room.toRoomIDOrAlias()) &&
protectedRooms.find(
(protectedRoom) =>
protectedRoom.toRoomIDOrAlias() === profile.room.toRoomIDOrAlias()
)
)
);
return issuerResults.map((result) => {
if (isError(result)) {
throw result.expect(
"We should be able to get the issuers for all of the watched lits."
);
}
const revision = result.ok.currentRevision;
const associatedProfile = watchedListProfiles.find(
if (isError(subscribedAndProtectedLists)) {
return subscribedAndProtectedLists;
}
const subscribedLists = await findRevisionIssuers(
policyRoomManager,
watchedListProfiles.filter(
(profile) =>
profile.room.toRoomIDOrAlias() === revision.room.toRoomIDOrAlias()
);
if (associatedProfile === undefined) {
throw new TypeError(
`Shouldn't be possible to have got a result for a list profile we don't have`
);
}
return {
watchedListProfile: associatedProfile,
revision: revision,
};
allJoinedRooms.includes(profile.room.toRoomIDOrAlias()) &&
!protectedRooms.find(
(protectedRoom) =>
protectedRoom.toRoomIDOrAlias() === profile.room.toRoomIDOrAlias()
)
)
);
if (isError(subscribedLists)) {
return subscribedLists;
}
const subscribedButPartedLists = watchedListProfiles.filter(
(profile) => !allJoinedRooms.includes(profile.room.toRoomIDOrAlias())
);
return Ok({
subscribedLists: subscribedLists.ok,
subscribedAndProtectedLists: subscribedAndProtectedLists.ok,
subscribedButPartedLists,
});
}
@@ -95,35 +138,30 @@ DraupnirInterfaceAdaptor.describeRenderer(DraupnirStatusCommand, {
// FIXME: need a shoutout to dependencies in here and NOTICE info.
export async function draupnirStatusInfo(
draupnir: Draupnir
): Promise<StatusInfo> {
const watchedListInfo = await listInfo(
): Promise<Result<StatusInfo>> {
const watchedListInfo = await getWatchedPolicyRoomsInfo(
draupnir.protectedRoomsSet.issuerManager,
draupnir.policyRoomManager
draupnir.policyRoomManager,
draupnir.clientRooms.currentRevision.allJoinedRooms,
draupnir.protectedRoomsSet.allProtectedRooms
);
const protectedWatchedLists = watchedListInfo.filter((info) =>
draupnir.protectedRoomsSet.isProtectedRoom(
info.revision.room.toRoomIDOrAlias()
)
);
const unprotectedListProfiles = watchedListInfo.filter(
(info) =>
!draupnir.protectedRoomsSet.isProtectedRoom(
info.revision.room.toRoomIDOrAlias()
)
);
return {
if (isError(watchedListInfo)) {
return watchedListInfo;
}
return Ok({
numberOfProtectedRooms: draupnir.protectedRoomsSet.allProtectedRooms.length,
numberOfUniqueMembers:
draupnir.protectedRoomsSet.setMembership.currentRevision.uniqueMemberCount(),
subscribedLists: unprotectedListProfiles,
subscribedAndProtectedLists: protectedWatchedLists,
subscribedLists: watchedListInfo.ok.subscribedLists,
subscribedAndProtectedLists: watchedListInfo.ok.subscribedAndProtectedLists,
subscribedButPartedLists: watchedListInfo.ok.subscribedButPartedLists,
documentationURL: DOCUMENTATION_URL,
version: SOFTWARE_VERSION,
repository: PACKAGE_JSON["repository"] ?? "Unknown",
};
});
}
export function renderPolicyList(list: ListInfo): DocumentNode {
export function renderPolicyList(list: WatchedPolicyRoomInfo): DocumentNode {
return (
<li>
<a href={list.revision.room.toPermalink()}>
@@ -141,7 +179,10 @@ export function renderPolicyList(list: ListInfo): DocumentNode {
}
export function renderStatusInfo(info: StatusInfo): DocumentNode {
const renderPolicyLists = (header: string, lists: ListInfo[]) => {
const renderPolicyLists = (
header: string,
lists: WatchedPolicyRoomInfo[]
) => {
const renderedLists = lists.map(renderPolicyList);
return (
<fragment>
+10
View File
@@ -24,7 +24,9 @@ import { findPolicyRoomIDFromShortcode } from "./CreateBanListCommand";
import {
isStringUserID,
MatrixGlob,
MatrixRoomID,
MatrixUserID,
StringRoomID,
StringUserID,
} from "@the-draupnir-project/matrix-basic-types";
import {
@@ -115,6 +117,8 @@ export type DraupnirUnbanCommandContext = {
roomUnbanner: RoomUnbanner;
unlistedUserRedactionQueue: UnlistedUserRedactionQueue;
roomInviter: RoomInviter;
allJoinedRooms: StringRoomID[];
protectedRooms: MatrixRoomID[];
};
export const DraupnirUnbanCommand = describeCommand({
@@ -178,12 +182,16 @@ export const DraupnirUnbanCommand = describeCommand({
issuerManager,
clientUserID,
unlistedUserRedactionQueue,
allJoinedRooms,
protectedRooms,
} = context;
const policyRoomReference =
typeof policyRoomDesignator === "string"
? await findPolicyRoomIDFromShortcode(
issuerManager,
policyRoomManager,
allJoinedRooms,
protectedRooms,
clientUserID,
policyRoomDesignator
)
@@ -252,6 +260,8 @@ DraupnirContextToCommandContextTranslator.registerTranslation(
roomUnbanner: draupnir.clientPlatform.toRoomUnbanner(),
unlistedUserRedactionQueue: draupnir.unlistedUserRedactionQueue,
roomInviter: draupnir.clientPlatform.toRoomInviter(),
allJoinedRooms: draupnir.clientRooms.currentRevision.allJoinedRooms,
protectedRooms: draupnir.protectedRoomsSet.allProtectedRooms,
};
}
);
+18 -8
View File
@@ -43,7 +43,7 @@ import {
import { Draupnir } from "../Draupnir";
import { resolveRoomReferenceSafe } from "matrix-protection-suite-for-matrix-bot-sdk";
import { DraupnirProtection } from "./Protection";
import { listInfo } from "../commands/StatusCommand";
import { getWatchedPolicyRoomsInfo } from "../commands/StatusCommand";
import { MatrixReactionHandler } from "../commands/interface-manager/MatrixReactionHandler";
import {
MatrixRoomID,
@@ -342,10 +342,6 @@ export class BanPropagationProtection
PolicyRuleType.User,
Recommendation.Ban
);
const policyRoomInfo = await listInfo(
draupnir.protectedRoomsSet.issuerManager,
draupnir.policyRoomManager
);
const roomsBannedFrom =
this.protectedRoomsSet.setRoomMembership.allRooms.filter(
(revision) =>
@@ -370,14 +366,28 @@ export class BanPropagationProtection
(map, rule) => addRule(map, rule),
new Map<StringRoomID, PolicyRule[]>()
);
const policyRoomInfo = await getWatchedPolicyRoomsInfo(
draupnir.protectedRoomsSet.issuerManager,
draupnir.policyRoomManager,
draupnir.clientRooms.currentRevision.allJoinedRooms,
draupnir.protectedRoomsSet.allProtectedRooms
);
if (isError(policyRoomInfo)) {
log.error(
`Unable to fetch policy rooms info to be able to prompt to unban ${change.userID}`,
policyRoomInfo.error
);
return;
}
await promptUnbanPropagation(
this.draupnir,
change,
change.roomID,
[...rulesByPolicyRoom.entries()].map(([policyRoomID, rules]) => {
const info = policyRoomInfo.find(
(i) => i.revision.room.toRoomIDOrAlias() === policyRoomID
);
const info = [
...policyRoomInfo.ok.subscribedAndProtectedLists,
...policyRoomInfo.ok.subscribedLists,
].find((i) => i.revision.room.toRoomIDOrAlias() === policyRoomID);
if (info === undefined) {
throw new TypeError(
`Shouldn't be possible to have a rule from an unwatched list.`
+12
View File
@@ -82,6 +82,10 @@ describe("Test the DraupnirBanCommand", function () {
issuerManager: protectedRoomsSet.issuerManager,
defaultReasons: ["spam"],
clientUserID: `@draupnir:ourserver.example.com` as StringUserID,
allJoinedRooms: protectedRoomsSet.allProtectedRooms.map((room) =>
room.toRoomIDOrAlias()
),
protectedRooms: protectedRoomsSet.allProtectedRooms,
},
{},
MatrixUserIDPresentationType.wrap(
@@ -115,6 +119,10 @@ describe("Test the DraupnirBanCommand", function () {
issuerManager: protectedRoomsSet.issuerManager,
defaultReasons: ["spam"],
clientUserID: `@draupnir:ourserver.example.com` as StringUserID,
allJoinedRooms: protectedRoomsSet.allProtectedRooms.map((room) =>
room.toRoomIDOrAlias()
),
protectedRooms: protectedRoomsSet.allProtectedRooms,
},
{},
MatrixUserIDPresentationType.wrap(
@@ -168,6 +176,10 @@ describe("Test the DraupnirBanCommand", function () {
issuerManager: protectedRoomsSet.issuerManager,
defaultReasons: ["spam"],
clientUserID: `@draupnir:ourserver.example.com` as StringUserID,
allJoinedRooms: protectedRoomsSet.allProtectedRooms.map((room) =>
room.toRoomIDOrAlias()
),
protectedRooms: protectedRoomsSet.allProtectedRooms,
},
{
rest: ["spam"],
+4
View File
@@ -142,6 +142,10 @@ describe("Test the DraupnirUnbanCommand", function () {
return Ok(undefined);
},
}),
allJoinedRooms: protectedRoomsSet.allProtectedRooms.map((room) =>
room.toRoomIDOrAlias()
),
protectedRooms: protectedRoomsSet.allProtectedRooms,
},
{
rest: ["spam"],