Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display persistent configs in safe mode #592

Merged
merged 8 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions src/safemode/PersistentConfigEditor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
//
// SPDX-License-Identifier: AFL-3.0

import { Ok, Result, isError } from "@gnuxie/typescript-result";
import { TObject } from "@sinclair/typebox";
import {
ConfigDescription,
ConfigParseError,
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";

export type PersistentConfigStatus = {
readonly description: ConfigDescription;
readonly data: unknown;
readonly error: ConfigParseError | undefined;
};

export interface PersistentConfigEditor {
getConfigAdaptors(): PersistentConfigData[];
requestConfigStatus(): Promise<Result<PersistentConfigStatus[]>>;
}

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.
this.configAdaptors = [
new StandardPersistentConfigData(
MjolnirPolicyRoomsDescription as unknown as ConfigDescription<TObject>,
new BotSDKAccountDataConfigBackend(
client,
MJOLNIR_WATCHED_POLICY_ROOMS_EVENT_TYPE
)
),
new StandardPersistentConfigData(
MjolnirProtectedRoomsDescription as unknown as ConfigDescription<TObject>,
new BotSDKAccountDataConfigBackend(
client,
MJOLNIR_PROTECTED_ROOMS_EVENT_TYPE
)
),
new StandardPersistentConfigData(
MjolnirEnabledProtectionsDescription as unknown as ConfigDescription<TObject>,
new BotSDKAccountDataConfigBackend(
client,
MjolnirEnabledProtectionsEventType
)
),
];
Gnuxie marked this conversation as resolved.
Show resolved Hide resolved
}
getConfigAdaptors(): PersistentConfigData[] {
return this.configAdaptors;
}

public async requestConfigStatus(): Promise<
Result<PersistentConfigStatus[]>
> {
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);
}
}
164 changes: 164 additions & 0 deletions src/safemode/PersistentConfigRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
//
// 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 { ConfigParseError, ConfigPropertyError } from "matrix-protection-suite";
import {
renderMentionPill,
renderRoomPill,
} from "../commands/interface-manager/MatrixHelpRenderer";
import { PersistentConfigStatus } from "./PersistentConfigEditor";

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 (
<fragment>
<code>{value}</code> <span>({type})</span>
</fragment>
);
}

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 (
<fragment>
<code>{String(value)}</code>{" "}
<span data-mx-color="#D2691E">(object)</span>
</fragment>
);
}
} 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 (
<li>
{renderConfigPropertyError(error)}
<code>{index}</code>: {renderConfigPropertyValue(value)}
</li>
);
}

function renderConfigPropertyError(
error: ConfigPropertyError | ConfigParseError | undefined
): string {
return error === undefined ? "🟢" : "🔴";
}

function renderConfigProperty(
propertyKey: string,
data: Record<string, unknown>,
errors: ConfigPropertyError[]
): DocumentNode {
const propertyValue = data[propertyKey];
const error = findError(propertyKey, errors);
if (Array.isArray(propertyValue)) {
return (
<li>
{renderConfigPropertyError(error)}
<code>{propertyKey}</code>:{" "}
<ul>
{propertyValue.map((value, index) =>
renderConfigPropertyItem(propertyKey, index, value, errors)
)}
</ul>
</li>
);
}
return (
<li>
{renderConfigPropertyError(error)}
<code>{propertyKey}</code>: {renderConfigPropertyValue(propertyValue)}
</li>
);
}

function renderBodgedConfig(value: unknown): DocumentNode {
return (
<fragment>
The config seems to be entirely invalid:{" "}
{renderConfigPropertyValue(value)}
</fragment>
);
}

export const StandardPersistentConfigRenderer = Object.freeze({
renderConfigStatus(config: PersistentConfigStatus): DocumentNode {
if (typeof config.data !== "object" || config.data === null) {
return renderBodgedConfig(config.data);
}
return (
<details>
<summary>
{renderConfigPropertyError(config.error)}{" "}
{config.description.schema.title ?? "Untitled Config"}
</summary>
{config.description
.properties()
.map((property) =>
renderConfigProperty(
property.name,
config.data as Record<string, unknown>,
config.error?.errors ?? []
)
)}
</details>
);
},
renderAdaptorStatus(info: PersistentConfigStatus[]): DocumentNode {
return (
<ul>
{info.map((config) => (
<li>{this.renderConfigStatus(config)}</li>
))}
</ul>
);
},
}) satisfies PersistentConfigRenderer;
Loading