Skip to content

Commit

Permalink
Merge pull request #612 from the-draupnir-project/gnuxie/no-confirm
Browse files Browse the repository at this point in the history
Confirmation prompt for recovery options

Fixes #602.
  • Loading branch information
Gnuxie authored Oct 11, 2024
2 parents 039f1a8 + d6b3f2e commit 57071b9
Show file tree
Hide file tree
Showing 12 changed files with 179 additions and 39 deletions.
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 <Gnuxie@protonmail.com>
//
// 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

0 comments on commit 57071b9

Please sign in to comment.