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

Confirmation prompt for recovery options #612

Merged
merged 2 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"@sentry/node": "^7.17.2",
"@sentry/tracing": "^7.17.2",
"@sinclair/typebox": "0.32.34",
"@the-draupnir-project/interface-manager": "2.5.0",
"@the-draupnir-project/interface-manager": "2.6.0",
"@the-draupnir-project/matrix-basic-types": "^0.2.0",
"await-lock": "^2.2.2",
"better-sqlite3": "^9.4.3",
Expand Down
11 changes: 3 additions & 8 deletions src/appservice/bot/AppserviceBotHelp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
TopPresentationSchema,
describeCommand,
} from "@the-draupnir-project/interface-manager";
import { ActionResult, Ok, isError } from "matrix-protection-suite";
import { ActionResult, Ok } from "matrix-protection-suite";
import { MatrixAdaptorContext } from "../../commands/interface-manager/MPSMatrixInterfaceAdaptor";
import { AppserviceBotCommands } from "./AppserviceBotCommandTable";
import { renderTableHelp } from "../../commands/interface-manager/MatrixHelpRenderer";
Expand All @@ -33,13 +33,8 @@ export const AppserviceBotHelpCommand = describeCommand({
parameters: [],
});

function renderAppserviceBotHelp(
appserviceBotCommands: Result<CommandTable>
): Result<DocumentNode> {
if (isError(appserviceBotCommands)) {
return appserviceBotCommands;
}
return Ok(<root>{renderTableHelp(appserviceBotCommands.ok)}</root>);
function renderAppserviceBotHelp(): Result<DocumentNode> {
return Ok(<root>{renderTableHelp(AppserviceBotCommands)}</root>);
}

AppserviceBotInterfaceAdaptor.describeRenderer(AppserviceBotHelpCommand, {
Expand Down
12 changes: 3 additions & 9 deletions src/commands/Help.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// https://github.com/matrix-org/mjolnir
// </text>

import { Ok, isError } from "matrix-protection-suite";
import { Ok } from "matrix-protection-suite";
import {
CommandTable,
DocumentNode,
Expand Down Expand Up @@ -38,13 +38,7 @@ export const DraupnirHelpCommand = describeCommand({
});

DraupnirInterfaceAdaptor.describeRenderer(DraupnirHelpCommand, {
JSXRenderer(result) {
if (isError(result)) {
throw new TypeError(
`We should always be able to get the base command table`
);
} else {
return Ok(renderDraupnirHelp(result.ok));
}
JSXRenderer() {
return Ok(renderDraupnirHelp(DraupnirTopLevelCommands));
},
});
2 changes: 2 additions & 0 deletions src/commands/interface-manager/MPSMatrixInterfaceAdaptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
import { matrixCommandRenderer } from "./MatrixHelpRenderer";
import { promptDefault, promptSuggestions } from "./MatrixPromptForAccept";
import { Result } from "@gnuxie/typescript-result";
import { matrixEventsFromConfirmationPrompt } from "./MatrixPromptForConfirmation";

export interface MatrixEventContext {
roomID: StringRoomID;
Expand Down Expand Up @@ -197,6 +198,7 @@ export const MPSMatrixInterfaceAdaptorCallbacks = Object.freeze({
defaultRenderer: matrixCommandRenderer,
matrixEventsFromDeadDocument,
rendererFailedCB,
matrixEventsFromConfirmationPrompt,
}) satisfies MatrixInterfaceAdaptorCallbacks<
MatrixAdaptorContext,
MatrixEventContext
Expand Down
5 changes: 4 additions & 1 deletion src/commands/interface-manager/MatrixHelpRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
CommandTable,
Command,
CommandTableEntry,
LeafNode,
} from "@the-draupnir-project/interface-manager";
import {
MatrixAdaptorContext,
Expand Down Expand Up @@ -371,6 +372,8 @@ export function renderTableHelp(table: CommandTable): DocumentNode {
);
}

export function wrapInRoot(node: DocumentNode): DocumentNode {
export function wrapInRoot(
node: DocumentNode | LeafNode | (DocumentNode | LeafNode)[]
): DocumentNode {
return <root>{node}</root>;
}
14 changes: 6 additions & 8 deletions src/commands/interface-manager/MatrixPromptForAccept.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,25 @@ import { StringRoomID } from "@the-draupnir-project/matrix-basic-types";

const log = new Logger("MatrixPromptForAccept");

type PromptContext = StaticDecode<typeof PromptContext>;
// FIXME: Remove no-redeclare entirely, it is wrong.
export type CommandPromptContext = StaticDecode<typeof CommandPromptContext>;

const PromptContext = Type.Object({
export const CommandPromptContext = Type.Object({
command_designator: Type.Array(Type.String()),
read_items: Type.Array(Type.String()),
});

type DefaultPromptContext = StaticDecode<typeof DefaultPromptContext>;
// FIXME: Remove no-redeclare entirely, it is wrong.

const DefaultPromptContext = Type.Composite([
PromptContext,
CommandPromptContext,
Type.Object({
default: Type.String(),
}),
]);

function continueCommandAcceptingPrompt(
export function continueCommandAcceptingPrompt(
eventContext: MatrixEventContext,
promptContext: PromptContext,
promptContext: CommandPromptContext,
serializedPrompt: string,
commandDispatcher: MatrixInterfaceCommandDispatcher<MatrixEventContext>,
reactionHandler: MatrixReactionHandler
Expand Down Expand Up @@ -114,7 +112,7 @@ export function makeListenerForArgumentPrompt(
if (annotatedEvent.room_id !== commandRoomID) {
return;
}
const promptContext = Value.Decode(PromptContext, context);
const promptContext = Value.Decode(CommandPromptContext, context);
if (isError(promptContext)) {
log.error(
`malformed event context when trying to accept a prompted argument`,
Expand Down
112 changes: 112 additions & 0 deletions src/commands/interface-manager/MatrixPromptForConfirmation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright 2024 Gnuxie <[email protected]>
//
// SPDX-License-Identifier: Apache-2.0
//
// SPDX-FileAttributionText: <text>
// This modified file incorporates work from Draupnir
// https://github.com/the-draupnir-project/Draupnir
// </text>

import { Ok, Result, isError } from "@gnuxie/typescript-result";
import {
MatrixAdaptorContext,
MatrixEventContext,
sendMatrixEventsFromDeadDocument,
} from "./MPSMatrixInterfaceAdaptor";
import {
MatrixInterfaceAdaptorCallbacks,
MatrixInterfaceCommandDispatcher,
TextPresentationRenderer,
} from "@the-draupnir-project/interface-manager";
import { resultifyBotSDKRequestError } from "matrix-protection-suite-for-matrix-bot-sdk";
import { Logger, Task, Value } from "matrix-protection-suite";
import {
MatrixReactionHandler,
ReactionListener,
} from "./MatrixReactionHandler";
import { StringRoomID } from "@the-draupnir-project/matrix-basic-types";
import {
CommandPromptContext,
continueCommandAcceptingPrompt,
} from "./MatrixPromptForAccept";

const log = new Logger("MatrixPromptForConfirmation");

export const COMMAND_CONFIRMATION_LISTENER =
"me.marewolf.draupnir.command_confirmation";

export function makeConfirmationPromptListener(
commandRoomID: StringRoomID,
commandDispatcher: MatrixInterfaceCommandDispatcher<MatrixEventContext>,
reactionHandler: MatrixReactionHandler
): ReactionListener {
return (key, item, rawContext, _reactionMap, annotatedEvent) => {
if (annotatedEvent.room_id !== commandRoomID) {
return;
}
if (key === "Cancel") {
void Task(reactionHandler.cancelPrompt(annotatedEvent));
return;
}
if (key !== "OK") {
return;
}
const promptContext = Value.Decode(CommandPromptContext, rawContext);
if (isError(promptContext)) {
log.error(
`malformed event context when trying to accept a prompted argument`,
context
);
return;
}
continueCommandAcceptingPrompt(
{ event: annotatedEvent, roomID: annotatedEvent.room_id },
promptContext.ok,
"--no-confirm",
commandDispatcher,
reactionHandler
);
};
}

export const matrixEventsFromConfirmationPrompt = async function (
{ client, clientPlatform, reactionHandler },
{ event },
command,
document
) {
const reactionMap = new Map<string, string>(
Object.entries({ OK: "OK", Cancel: "Cancel" })
);
const sendResult = await sendMatrixEventsFromDeadDocument(
clientPlatform.toRoomMessageSender(),
event.room_id,
document,
{
replyToEvent: event,
additionalContent: reactionHandler.createAnnotation(
COMMAND_CONFIRMATION_LISTENER,
reactionMap,
{
command_designator: command.designator,
read_items: command
.toPartialCommand()
.stream.source.slice(command.designator.length)
.map((p) => TextPresentationRenderer.render(p)),
}
),
}
);
if (isError(sendResult)) {
return sendResult as Result<void>;
}
if (sendResult.ok[0] === undefined) {
throw new TypeError(`We exepct to have sent at least one event`);
}
return await reactionHandler
.addReactionsToEvent(client, event.room_id, sendResult.ok[0], reactionMap)
.then((_) => Ok(undefined), resultifyBotSDKRequestError);
} satisfies MatrixInterfaceAdaptorCallbacks<
MatrixAdaptorContext,
MatrixEventContext
>["matrixEventsFromConfirmationPrompt"];
12 changes: 12 additions & 0 deletions src/safemode/DraupnirSafeMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ import {
import { wrapInRoot } from "../commands/interface-manager/MatrixHelpRenderer";
import { sendAndAnnotateWithRecoveryOptions } from "./commands/RecoverCommand";
import { StandardPersistentConfigEditor } from "./PersistentConfigEditor";
import {
COMMAND_CONFIRMATION_LISTENER,
makeConfirmationPromptListener,
} from "../commands/interface-manager/MatrixPromptForConfirmation";

export class SafeModeDraupnir implements MatrixAdaptorContext {
public reactionHandler: MatrixReactionHandler;
Expand Down Expand Up @@ -86,6 +90,14 @@ export class SafeModeDraupnir implements MatrixAdaptorContext {
this.reactionHandler
)
);
this.reactionHandler.on(
COMMAND_CONFIRMATION_LISTENER,
makeConfirmationPromptListener(
this.commandRoomID,
this.commandDispatcher,
this.reactionHandler
)
);
}

handleTimelineEvent(roomID: StringRoomID, event: RoomEvent): void {
Expand Down
9 changes: 3 additions & 6 deletions src/safemode/commands/HelpCommand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
TopPresentationSchema,
CommandTable,
} from "@the-draupnir-project/interface-manager";
import { Ok, isError } from "matrix-protection-suite";
import { Ok } from "matrix-protection-suite";
import { renderTableHelp } from "../../commands/interface-manager/MatrixHelpRenderer";
import { safeModeHeader } from "./StatusCommand";

Expand All @@ -32,14 +32,11 @@ export const SafeModeHelpCommand = describeCommand({
});

SafeModeInterfaceAdaptor.describeRenderer(SafeModeHelpCommand, {
JSXRenderer(result) {
if (isError(result)) {
throw new TypeError("This should never fail");
}
JSXRenderer() {
return Ok(
<root>
{safeModeHeader()}
{renderTableHelp(result.ok)}
{renderTableHelp(SafeModeCommands)}
</root>
);
},
Expand Down
29 changes: 28 additions & 1 deletion src/safemode/commands/RecoverCommand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,18 @@ export const SafeModeRecoverCommand = describeCommand({
acceptor: StringPresentationType,
description: "The recovery option to enact, e.g. 1.",
}),
keywords: {
keywordDescriptions: {
"no-confirm": {
description: "Do not prompt for confirmation.",
isFlag: true,
},
},
},
async executor(
safeModeDraupnir: SafeModeDraupnir,
_info,
_keywords,
keywords,
_rest,
optionDesignator
): Promise<Result<SafeModeRecoverEffectInfo>> {
Expand All @@ -65,6 +73,12 @@ export const SafeModeRecoverCommand = describeCommand({
`No recovery option with the number ${optionNumber.ok} exists.`
);
}
if (!keywords.getKeywordValue<boolean>("no-confirm", false)) {
return Ok({
recoveryOption: selectedOption,
configStatus: [],
});
}
const recoveryResult = await selectedOption.recover();
if (isError(recoveryResult)) {
return recoveryResult;
Expand All @@ -85,6 +99,19 @@ export const SafeModeRecoverCommand = describeCommand({
});

SafeModeInterfaceAdaptor.describeRenderer(SafeModeRecoverCommand, {
confirmationPromptJSXRenderer(result) {
if (isError(result)) {
return Ok(undefined);
}
const { recoveryOption } = result.ok;
return Ok(
<root>
<h4>You are about to use the following recovery option:</h4>
<p>{recoveryOption.description}</p>
<p>Please confirm that you wish to proceed.</p>
</root>
);
},
JSXRenderer(result) {
if (isError(result)) {
return Ok(undefined);
Expand Down
2 changes: 1 addition & 1 deletion test/integration/commands/recoverCommandDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ async function recoverAndRestart(
(
await safeModeDraupnirWithRecoveryOptions.sendTextCommand(
sender,
"!draupnir recover 1"
"!draupnir recover 1 --no-confirm"
)
).expect("Failed to recover the draupnir");
return (
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -274,10 +274,10 @@
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e"
integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==

"@the-draupnir-project/interface-manager@2.5.0":
version "2.5.0"
resolved "https://registry.yarnpkg.com/@the-draupnir-project/interface-manager/-/interface-manager-2.5.0.tgz#5a41cbbb62d890fb2d539483d046750ef36d4944"
integrity sha512-J7h19l5uejb7eniOL8Uz+ByvLrIfiFQNrPJON7nJRGpnQ2oqZIEqic5f186zSWwEOlE2KhNFAz6bfOiQJDm5qg==
"@the-draupnir-project/interface-manager@2.6.0":
version "2.6.0"
resolved "https://registry.yarnpkg.com/@the-draupnir-project/interface-manager/-/interface-manager-2.6.0.tgz#45c912a0dda36db295bb24560ecc050a9111202b"
integrity sha512-l1v+acM/WK6J8jR29ZX3wPv5k7BPtfpq6EwnRJwPViEfJvNAc2NJDr7pIHrUSpVpJz2we/mW5WWxRejG/Di+4Q==
dependencies:
"@gnuxie/super-cool-stream" "^0.2.1"
"@gnuxie/typescript-result" "^1.0.0"
Expand Down
Loading