diff --git a/package.json b/package.json index d2cac77f..c84ce72c 100644 --- a/package.json +++ b/package.json @@ -69,8 +69,8 @@ "js-yaml": "^4.1.0", "jsdom": "^24.0.0", "matrix-appservice-bridge": "^9.0.1", - "matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@1.5.2", - "matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@1.5.2", + "matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@1.6.0", + "matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@1.6.0", "parse-duration": "^1.0.2", "pg": "^8.8.0", "shell-quote": "^1.7.3", diff --git a/src/safemode/DraupnirSafeMode.ts b/src/safemode/DraupnirSafeMode.ts index 17bb4ae5..d5fba050 100644 --- a/src/safemode/DraupnirSafeMode.ts +++ b/src/safemode/DraupnirSafeMode.ts @@ -31,13 +31,14 @@ import { } from "../commands/interface-manager/MatrixPromptForAccept"; import { makeCommandDispatcherTimelineListener } from "./ManagementRoom"; import { SafeModeToggle } from "./SafeModeToggle"; -import { Result } from "@gnuxie/typescript-result"; +import { Result, isError } from "@gnuxie/typescript-result"; import { renderSafeModeStatusInfo, safeModeStatusInfo, } from "./commands/StatusCommand"; import { wrapInRoot } from "../commands/interface-manager/MatrixHelpRenderer"; import { sendAndAnnotateWithRecoveryOptions } from "./commands/RecoverCommand"; +import { StandardPersistentConfigEditor } from "./PersistentConfigEditor"; export class SafeModeDraupnir implements MatrixAdaptorContext { public reactionHandler: MatrixReactionHandler; @@ -118,11 +119,26 @@ export class SafeModeDraupnir implements MatrixAdaptorContext { public startupComplete(): void { void Task( - sendAndAnnotateWithRecoveryOptions( - this, - wrapInRoot(renderSafeModeStatusInfo(safeModeStatusInfo(this))), - {} - ) + (async () => { + const editor = new StandardPersistentConfigEditor(this.client); + const configStatus = await editor.supplementStatusWithSafeModeCause( + this.cause + ); + if (isError(configStatus)) { + return configStatus.elaborate( + "Failed to fetch draupnir's persistent configuration" + ); + } + return await sendAndAnnotateWithRecoveryOptions( + this, + wrapInRoot( + renderSafeModeStatusInfo( + safeModeStatusInfo(this.cause, configStatus.ok) + ) + ), + {} + ); + })() ); } diff --git a/src/safemode/PersistentConfigEditor.ts b/src/safemode/PersistentConfigEditor.ts new file mode 100644 index 00000000..d2502348 --- /dev/null +++ b/src/safemode/PersistentConfigEditor.ts @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { Ok, Result, isError } from "@gnuxie/typescript-result"; +import { + ConfigDescription, + ConfigParseError, + ConfigPropertyUseError, + MJOLNIR_PROTECTED_ROOMS_EVENT_TYPE, + MJOLNIR_WATCHED_POLICY_ROOMS_EVENT_TYPE, + MjolnirEnabledProtectionsDescription, + MjolnirEnabledProtectionsEventType, + MjolnirPolicyRoomsDescription, + MjolnirProtectedRoomsDescription, + PersistentConfigData, + StandardPersistentConfigData, +} from "matrix-protection-suite"; +import { + BotSDKAccountDataConfigBackend, + MatrixSendClient, +} from "matrix-protection-suite-for-matrix-bot-sdk"; +import { SafeModeCause, SafeModeReason } from "./SafeModeCause"; + +export type PersistentConfigStatus = { + description: ConfigDescription; + data: unknown; + error: ConfigParseError | undefined; +}; + +export interface PersistentConfigEditor { + getConfigAdaptors(): PersistentConfigData[]; + requestConfigStatus(): Promise>; + /** + * requestConfigStatus, but be sure to update the PeristentConfigStatus list + * with the ConfigPropertyUseError in the safe mode cause, if there is one. + * + * This is because `ConfigPropertyUseError`'s will not show up in parsing, + * only when creating Draupnir itself, and so they won't show up from just requesting + * the config alone. + */ + supplementStatusWithSafeModeCause( + cause: SafeModeCause + ): Promise>; +} + +export class StandardPersistentConfigEditor implements PersistentConfigEditor { + private readonly configAdaptors: PersistentConfigData[] = []; + public constructor(client: MatrixSendClient) { + // We do some sweepy sweepy casting here because the ConfigMirror has methods + // that accept a specific shape, and obviously that means the type parameter + // becomes contravariant. I think the only way to fix this is to make the mirrors + // only work with the general shape rather than the specific one, in the way that + // the `remove` methods do, but I'm not convinced that works either, as those + // methods accept a Record that at least has the keys from the specific shape + // of the config. OK that's not why, because I tried to remove the toMirror method. + // I don't understand why it won't work then... + this.configAdaptors = [ + new StandardPersistentConfigData( + MjolnirPolicyRoomsDescription as unknown as ConfigDescription, + new BotSDKAccountDataConfigBackend( + client, + MJOLNIR_WATCHED_POLICY_ROOMS_EVENT_TYPE + ) + ), + new StandardPersistentConfigData( + MjolnirProtectedRoomsDescription as unknown as ConfigDescription, + new BotSDKAccountDataConfigBackend( + client, + MJOLNIR_PROTECTED_ROOMS_EVENT_TYPE + ) + ), + new StandardPersistentConfigData( + MjolnirEnabledProtectionsDescription as unknown as ConfigDescription, + new BotSDKAccountDataConfigBackend( + client, + MjolnirEnabledProtectionsEventType + ) + ), + ]; + } + getConfigAdaptors(): PersistentConfigData[] { + return this.configAdaptors; + } + + public async requestConfigStatus(): Promise< + Result + > { + const info: PersistentConfigStatus[] = []; + for (const adaptor of this.configAdaptors) { + const dataResult = await adaptor.requestConfig(); + if (isError(dataResult)) { + if (dataResult.error instanceof ConfigParseError) { + info.push({ + description: adaptor.description, + data: dataResult.error.config, + error: dataResult.error, + }); + } else { + return dataResult; + } + } else { + info.push({ + description: adaptor.description, + data: dataResult.ok, + error: undefined, + }); + } + } + return Ok(info); + } + public async supplementStatusWithSafeModeCause( + cause: SafeModeCause + ): Promise> { + const info = await this.requestConfigStatus(); + if (isError(info)) { + return info; + } + if (cause.reason === SafeModeReason.ByRequest) { + return Ok(info.ok); + } + if (!(cause.error instanceof ConfigPropertyUseError)) { + return Ok(info.ok); + } + const relevantStatus = info.ok.find( + (status) => + status.description === + (cause.error as ConfigPropertyUseError).configDescription + ); + if (relevantStatus === undefined) { + throw new TypeError( + "The cause of the safe mode error was not found in the configuration status." + ); + } + relevantStatus.error = new ConfigParseError( + "There was a problem when using a property in the configuration.", + relevantStatus.description as unknown as ConfigDescription, + [cause.error], + relevantStatus.data + ); + return Ok(info.ok); + } +} diff --git a/src/safemode/PersistentConfigRenderer.tsx b/src/safemode/PersistentConfigRenderer.tsx new file mode 100644 index 00000000..1cf987ed --- /dev/null +++ b/src/safemode/PersistentConfigRenderer.tsx @@ -0,0 +1,213 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { + DeadDocumentJSX, + DocumentNode, +} from "@the-draupnir-project/interface-manager"; +import { + MatrixRoomAlias, + MatrixRoomID, + MatrixUserID, +} from "@the-draupnir-project/matrix-basic-types"; +import { + ConfigDescription, + ConfigParseError, + ConfigPropertyError, + ConfigPropertyUseError, +} from "matrix-protection-suite"; +import { + renderMentionPill, + renderRoomPill, +} from "../commands/interface-manager/MatrixHelpRenderer"; +import { PersistentConfigStatus } from "./PersistentConfigEditor"; + +const ConfigStatusIndicator = Object.freeze({ + Ok: "✔", + UseError: "⚠", + ParseError: "❌", +}); + +export interface PersistentConfigRenderer { + renderConfigStatus(config: PersistentConfigStatus): DocumentNode; + renderAdaptorStatus(info: PersistentConfigStatus[]): DocumentNode; +} + +function findError( + propertyKey: string, + errors: ConfigPropertyError[] +): ConfigPropertyError | undefined { + const path = `/${propertyKey}`; + return errors.find((error) => error.path.startsWith(path)); +} + +function findItemError( + propertyKey: string, + index: number, + errors: ConfigPropertyError[] +): ConfigPropertyError | undefined { + const path = `/${propertyKey}/${index}`; + return errors.find((error) => error.path === path); +} + +function renderPrimitiveValue(value: string, type: string): DocumentNode { + return ( + + {value} ({type}) + + ); +} + +function renderConfigPropertyValue(value: unknown): DocumentNode { + if (typeof value === "object" && value !== null) { + if (value instanceof MatrixRoomAlias || value instanceof MatrixRoomID) { + return renderRoomPill(value); + } else if (value instanceof MatrixUserID) { + return renderMentionPill(value.toString(), value.toString()); + } else { + return ( + + {String(value)}{" "} + (object) + + ); + } + } else if (typeof value === "string") { + return renderPrimitiveValue(value, "string"); + } else if (typeof value === "number") { + return renderPrimitiveValue(String(value), "number"); + } else { + return renderPrimitiveValue(String(value), "unknown"); + } +} + +function renderConfigPropertyItem( + propertyKey: string, + index: number, + value: unknown, + errors: ConfigPropertyError[] +): DocumentNode { + const error = findItemError(propertyKey, index, errors); + return ( +
  • + {renderConfigPropertyError(error)} {index}:{" "} + {renderConfigPropertyValue(value)} +
  • + ); +} + +function renderConfigPropertyError( + error: ConfigPropertyError | ConfigParseError | undefined +): string { + if (error === undefined) { + return ConfigStatusIndicator.Ok; + } else if (error instanceof ConfigPropertyUseError) { + return ConfigStatusIndicator.UseError; + } else if (error instanceof ConfigParseError) { + if (error.errors.every((e) => e instanceof ConfigPropertyUseError)) { + return ConfigStatusIndicator.UseError; + } else { + return ConfigStatusIndicator.ParseError; + } + } else { + return ConfigStatusIndicator.ParseError; + } +} + +function renderConfigProperty( + propertyKey: string, + data: Record, + errors: ConfigPropertyError[] +): DocumentNode { + const propertyValue = data[propertyKey]; + const error = findError(propertyKey, errors); + if (Array.isArray(propertyValue)) { + return ( +
  • +
    + + {renderConfigPropertyError(error)} {propertyKey}: + +
      + {propertyValue.map((value, index) => + renderConfigPropertyItem(propertyKey, index, value, errors) + )} +
    +
    +
  • + ); + } + return ( +
  • + {renderConfigPropertyError(error)} + {propertyKey}: {renderConfigPropertyValue(propertyValue)} +
  • + ); +} + +function renderConfigDetails( + error: ConfigParseError | undefined, + description: ConfigDescription, + children: DocumentNode +): DocumentNode { + return ( + + {renderConfigPropertyError(error)}{" "} + {description.schema.title ?? "Untitled Config"} {children} + + ); +} + +function renderBodgedConfig(config: PersistentConfigStatus): DocumentNode { + if (config.data === undefined) { + return renderConfigDetails( + undefined, + config.description, + No data is currently persisted for this config. + ); + } + return renderConfigDetails( + config.error, + config.description, + + The config seems to be entirely invalid:{" "} + {renderConfigPropertyValue(config.data)} + + ); +} + +export const StandardPersistentConfigRenderer = Object.freeze({ + renderConfigStatus(config: PersistentConfigStatus): DocumentNode { + if (typeof config.data !== "object" || config.data === null) { + return renderBodgedConfig(config); + } + return renderConfigDetails( + config.error, + config.description, + + {config.description + .properties() + .map((property) => + renderConfigProperty( + property.name, + config.data as Record, + config.error?.errors ?? [] + ) + )} + + ); + }, + renderAdaptorStatus(info: PersistentConfigStatus[]): DocumentNode { + return ( + + Persistent configuration status: +
      + {info.map((config) => ( +
    • {this.renderConfigStatus(config)}
    • + ))} +
    +
    + ); + }, +}) satisfies PersistentConfigRenderer; diff --git a/src/safemode/commands/StatusCommand.tsx b/src/safemode/commands/StatusCommand.tsx index f83e9834..e7092c66 100644 --- a/src/safemode/commands/StatusCommand.tsx +++ b/src/safemode/commands/StatusCommand.tsx @@ -19,6 +19,11 @@ import { import { SafeModeInterfaceAdaptor } from "./SafeModeAdaptor"; import { renderRecoveryOptions } from "../RecoveryOptions"; import { sendAndAnnotateWithRecoveryOptions } from "./RecoverCommand"; +import { + PersistentConfigStatus, + StandardPersistentConfigEditor, +} from "../PersistentConfigEditor"; +import { StandardPersistentConfigRenderer } from "../PersistentConfigRenderer"; export function safeModeHeader(): DocumentNode { return ( @@ -34,22 +39,23 @@ function renderSafeModeCauseError(error: ResultError): DocumentNode { return ( Draupnir is in safe mode because Draupnir failed to start. -
    - {error.mostRelevantElaboration} -
    - Details can be found by providing the reference{" "} - {error.uuid} - to an administrator. -
    {error.toReadableString()}
    +
    + {error.mostRelevantElaboration} + Details can be found by providing the reference{" "} + {error.uuid} + to an administrator. +
    {error.toReadableString()}
    +
    ); } else { return ( Draupnir is in safe mode because Draupnir failed to start. -
    - {error.mostRelevantElaboration} -
    {error.toReadableString()}
    +
    + {error.mostRelevantElaboration} +
    {error.toReadableString()}
    +
    ); } @@ -68,6 +74,7 @@ function renderSafeModeCause(safeModeCause: SafeModeCause): DocumentNode { export interface SafeModeStatusInfo { safeModeCause: SafeModeCause; + configStatus: PersistentConfigStatus[]; documentationURL: string; version: string; repository: string; @@ -80,14 +87,12 @@ export function renderSafeModeStatusInfo( return ( ⚠️ Draupnir is in safe mode ⚠️ - -
    - {renderSafeModeCause(info.safeModeCause)} -
    -
    +
    + {renderSafeModeCause(info.safeModeCause)}
    {renderRecoveryOptions(info.safeModeCause)}
    + {StandardPersistentConfigRenderer.renderAdaptorStatus(info.configStatus)} Version: {info.version}
    @@ -110,10 +115,12 @@ export function renderSafeModeStatusInfo( } export function safeModeStatusInfo( - safeModeDraupnir: SafeModeDraupnir + cause: SafeModeCause, + configStatus: PersistentConfigStatus[] ): SafeModeStatusInfo { return { - safeModeCause: safeModeDraupnir.cause, + safeModeCause: cause, + configStatus, documentationURL: DOCUMENTATION_URL, version: SOFTWARE_VERSION, repository: PACKAGE_JSON["repository"] ?? "Unknown", @@ -127,7 +134,14 @@ export const SafeModeStatusCommand = describeCommand({ async executor( safeModeDraupnir: SafeModeDraupnir ): Promise> { - return Ok(safeModeStatusInfo(safeModeDraupnir)); + const editor = new StandardPersistentConfigEditor(safeModeDraupnir.client); + const configStatus = await editor.supplementStatusWithSafeModeCause( + safeModeDraupnir.cause + ); + if (isError(configStatus)) { + return configStatus; + } + return Ok(safeModeStatusInfo(safeModeDraupnir.cause, configStatus.ok)); }, }); diff --git a/yarn.lock b/yarn.lock index d4a32bc9..37d24b73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2526,17 +2526,17 @@ 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@1.5.2": - version "1.5.2" - resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite-for-matrix-bot-sdk/-/matrix-protection-suite-for-matrix-bot-sdk-1.5.2.tgz#e238d076df42bc9e509d5903630f2a04faa22db9" - integrity sha512-KqF8om8CdgepGkfVpAwCCLMAW16BwqOMH1Y7GebOToVx78sAq8sxXVgtDuXtpbZ//m42aa8EHsZG4gaqycw0xA== +"matrix-protection-suite-for-matrix-bot-sdk@npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite-for-matrix-bot-sdk/-/matrix-protection-suite-for-matrix-bot-sdk-1.6.0.tgz#ccd3f0cc1e8582e96a745d025c3031509ff1e45c" + integrity sha512-4rBarhvsaiSMPL0390Dkv78/TgQsV6/Ay+7InvF3b0wS9GFXZem7PTvvFs9TjDG0r1iovYSzpvSkoD/y3fBPOg== dependencies: "@gnuxie/typescript-result" "^1.0.0" -"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@1.5.2": - version "1.5.2" - resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-1.5.2.tgz#6dc3d85d6f2b9b36e08d289c80bd440f3ced79dd" - integrity sha512-N9LedoF6E6ComaKFoSzdn/G88XJ1fbIkZh4V7v9DEyKACo3GxlNA7yppc6e1ehFJ+QVEbW653edmwYF92qdjQQ== +"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-1.6.0.tgz#f0d567946dcb4b6bdd274a291985eda103625f8b" + integrity sha512-ywxLum0c5JzxNAWw5yfgDho7jE29RNNDydzSZztbji24LCsh6nuUvJBbW4yeCLtA6qVwDwfVszP4zs66vNurJg== dependencies: "@gnuxie/typescript-result" "^1.0.0" await-lock "^2.2.2"