refactor: extract Discord ack reaction helpers

This commit is contained in:
Peter Steinberger
2026-04-04 01:27:48 +09:00
parent 226e2389f6
commit 570ed4285e
6 changed files with 129 additions and 118 deletions

View File

@@ -115,6 +115,10 @@ export async function handleDiscordMessagingAction(
accountId: accountId ?? "default",
})
: undefined;
const withReactionRuntimeOptions = <T extends Record<string, unknown>>(extra?: T) => ({
...(reactionRuntimeOptions ?? cfgOptions),
...(extra ?? {}),
});
const normalizeMessage = (message: unknown) => {
if (!message || typeof message !== "object") {
return message;
@@ -137,44 +141,28 @@ export async function handleDiscordMessagingAction(
removeErrorMessage: "Emoji is required to remove a Discord reaction.",
});
if (remove) {
if (reactionRuntimeOptions) {
await discordMessagingActionRuntime.removeReactionDiscord(channelId, messageId, emoji, {
...reactionRuntimeOptions,
});
} else {
await discordMessagingActionRuntime.removeReactionDiscord(
channelId,
messageId,
emoji,
cfgOptions,
);
}
return jsonResult({ ok: true, removed: emoji });
}
if (isEmpty) {
const removed = reactionRuntimeOptions
? await discordMessagingActionRuntime.removeOwnReactionsDiscord(channelId, messageId, {
...reactionRuntimeOptions,
})
: await discordMessagingActionRuntime.removeOwnReactionsDiscord(
channelId,
messageId,
cfgOptions,
);
return jsonResult({ ok: true, removed: removed.removed });
}
if (reactionRuntimeOptions) {
await discordMessagingActionRuntime.reactMessageDiscord(channelId, messageId, emoji, {
...reactionRuntimeOptions,
});
} else {
await discordMessagingActionRuntime.reactMessageDiscord(
await discordMessagingActionRuntime.removeReactionDiscord(
channelId,
messageId,
emoji,
cfgOptions,
withReactionRuntimeOptions(),
);
return jsonResult({ ok: true, removed: emoji });
}
if (isEmpty) {
const removed = await discordMessagingActionRuntime.removeOwnReactionsDiscord(
channelId,
messageId,
withReactionRuntimeOptions(),
);
return jsonResult({ ok: true, removed: removed.removed });
}
await discordMessagingActionRuntime.reactMessageDiscord(
channelId,
messageId,
emoji,
withReactionRuntimeOptions(),
);
return jsonResult({ ok: true, added: emoji });
}
case "reactions": {
@@ -189,11 +177,7 @@ export async function handleDiscordMessagingAction(
const reactions = await discordMessagingActionRuntime.fetchReactionsDiscord(
channelId,
messageId,
{
...cfgOptions,
...(reactionRuntimeOptions ?? {}),
limit,
},
withReactionRuntimeOptions({ limit }),
);
return jsonResult({ ok: true, reactions });
}

View File

@@ -0,0 +1,70 @@
import type { RequestClient } from "@buape/carbon";
import {
createStatusReactionController,
logAckFailure,
type StatusReactionAdapter,
} from "openclaw/plugin-sdk/channel-feedback";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import { createDiscordRuntimeAccountContext } from "../client.js";
import { reactMessageDiscord, removeReactionDiscord } from "../send.js";
import type { DiscordReactionRuntimeContext } from "../send.types.js";
export function createDiscordAckReactionContext(params: {
rest: RequestClient;
cfg: OpenClawConfig;
accountId: string;
}): DiscordReactionRuntimeContext {
return {
rest: params.rest,
...createDiscordRuntimeAccountContext({
cfg: params.cfg,
accountId: params.accountId,
}),
};
}
export function createDiscordAckReactionAdapter(params: {
channelId: string;
messageId: string;
reactionContext: DiscordReactionRuntimeContext;
}): StatusReactionAdapter {
return {
setReaction: async (emoji) => {
await reactMessageDiscord(params.channelId, params.messageId, emoji, params.reactionContext);
},
removeReaction: async (emoji) => {
await removeReactionDiscord(
params.channelId,
params.messageId,
emoji,
params.reactionContext,
);
},
};
}
export function queueInitialDiscordAckReaction(params: {
enabled: boolean;
shouldSendAckReaction: boolean;
ackReaction: string | undefined;
statusReactions: ReturnType<typeof createStatusReactionController>;
reactionAdapter: StatusReactionAdapter;
target: string;
}) {
if (params.enabled) {
void params.statusReactions.setQueued();
return;
}
if (!params.shouldSendAckReaction || !params.ackReaction) {
return;
}
void params.reactionAdapter.setReaction(params.ackReaction).catch((err) => {
logAckFailure({
log: logVerbose,
channel: "discord",
target: params.target,
error: err,
});
});
}

View File

@@ -7,7 +7,6 @@ import {
logAckFailure,
logTypingFailure,
shouldAckReaction as shouldAckReactionGate,
type StatusReactionAdapter,
} from "openclaw/plugin-sdk/channel-feedback";
import {
formatInboundEnvelope,
@@ -39,12 +38,14 @@ import { stripReasoningTagsFromText } from "openclaw/plugin-sdk/text-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-runtime";
import { resolveDiscordMaxLinesPerMessage } from "../accounts.js";
import { chunkDiscordTextWithMode } from "../chunk.js";
import { createDiscordRuntimeAccountContext } from "../client.js";
import { resolveDiscordDraftStreamingChunking } from "../draft-chunking.js";
import { createDiscordDraftStream } from "../draft-stream.js";
import { reactMessageDiscord, removeReactionDiscord } from "../send.js";
import { editMessageDiscord } from "../send.messages.js";
import type { DiscordReactionRuntimeContext } from "../send.types.js";
import {
createDiscordAckReactionAdapter,
createDiscordAckReactionContext,
queueInitialDiscordAckReaction,
} from "./ack-reactions.js";
import { normalizeDiscordSlug } from "./allow-list.js";
import { resolveTimestampMs } from "./format.js";
import {
@@ -81,65 +82,6 @@ async function loadReplyRuntime() {
return await replyRuntimePromise;
}
function createDiscordAckReactionContext(params: {
rest: RequestClient;
cfg: OpenClawConfig;
accountId: string;
}): DiscordReactionRuntimeContext {
return {
rest: params.rest,
...createDiscordRuntimeAccountContext({
cfg: params.cfg,
accountId: params.accountId,
}),
};
}
function createDiscordAckReactionAdapter(params: {
channelId: string;
messageId: string;
reactionContext: DiscordReactionRuntimeContext;
}): StatusReactionAdapter {
return {
setReaction: async (emoji) => {
await reactMessageDiscord(params.channelId, params.messageId, emoji, params.reactionContext);
},
removeReaction: async (emoji) => {
await removeReactionDiscord(
params.channelId,
params.messageId,
emoji,
params.reactionContext,
);
},
};
}
function queueInitialDiscordAckReaction(params: {
enabled: boolean;
shouldSendAckReaction: boolean;
ackReaction: string | undefined;
statusReactions: ReturnType<typeof createStatusReactionController>;
reactionAdapter: StatusReactionAdapter;
target: string;
}) {
if (params.enabled) {
void params.statusReactions.setQueued();
return;
}
if (!params.shouldSendAckReaction || !params.ackReaction) {
return;
}
void params.reactionAdapter.setReaction(params.ackReaction).catch((err) => {
logAckFailure({
log: logVerbose,
channel: "discord",
target: params.target,
error: err,
});
});
}
function isProcessAborted(abortSignal?: AbortSignal): boolean {
return Boolean(abortSignal?.aborted);
}

View File

@@ -6,7 +6,26 @@ import {
formatReactionEmoji,
normalizeReactionEmoji,
} from "./send.shared.js";
import type { DiscordReactionSummary, DiscordReactOpts } from "./send.types.js";
import type {
DiscordReactionRuntimeContext,
DiscordReactionSummary,
DiscordReactOpts,
} from "./send.types.js";
function createDiscordReactionRuntimeClient(opts: DiscordReactionRuntimeContext) {
return createDiscordClient(opts, opts.cfg);
}
function resolveDiscordReactionClient(opts: DiscordReactOpts) {
const cfg = opts.cfg ?? loadConfig();
return createDiscordClient(opts, cfg);
}
function isDiscordReactionRuntimeContext(
opts: DiscordReactOpts,
): opts is DiscordReactionRuntimeContext {
return Boolean(opts.rest && opts.cfg && opts.accountId);
}
export async function reactMessageDiscord(
channelId: string,
@@ -14,8 +33,9 @@ export async function reactMessageDiscord(
emoji: string,
opts: DiscordReactOpts = {},
) {
const cfg = opts.cfg ?? loadConfig();
const { rest, request } = createDiscordClient(opts, cfg);
const { rest, request } = isDiscordReactionRuntimeContext(opts)
? createDiscordReactionRuntimeClient(opts)
: resolveDiscordReactionClient(opts);
const encoded = normalizeReactionEmoji(emoji);
await request(
() => rest.put(Routes.channelMessageOwnReaction(channelId, messageId, encoded)),
@@ -30,8 +50,9 @@ export async function removeReactionDiscord(
emoji: string,
opts: DiscordReactOpts = {},
) {
const cfg = opts.cfg ?? loadConfig();
const { rest } = createDiscordClient(opts, cfg);
const { rest } = isDiscordReactionRuntimeContext(opts)
? createDiscordReactionRuntimeClient(opts)
: resolveDiscordReactionClient(opts);
const encoded = normalizeReactionEmoji(emoji);
await rest.delete(Routes.channelMessageOwnReaction(channelId, messageId, encoded));
return { ok: true };
@@ -42,8 +63,9 @@ export async function removeOwnReactionsDiscord(
messageId: string,
opts: DiscordReactOpts = {},
): Promise<{ ok: true; removed: string[] }> {
const cfg = opts.cfg ?? loadConfig();
const { rest } = createDiscordClient(opts, cfg);
const { rest } = isDiscordReactionRuntimeContext(opts)
? createDiscordReactionRuntimeClient(opts)
: resolveDiscordReactionClient(opts);
const message = (await rest.get(Routes.channelMessage(channelId, messageId))) as {
reactions?: Array<{ emoji: { id?: string | null; name?: string | null } }>;
};
@@ -74,8 +96,9 @@ export async function fetchReactionsDiscord(
messageId: string,
opts: DiscordReactOpts & { limit?: number } = {},
): Promise<DiscordReactionSummary[]> {
const cfg = opts.cfg ?? loadConfig();
const { rest } = createDiscordClient(opts, cfg);
const { rest } = isDiscordReactionRuntimeContext(opts)
? createDiscordReactionRuntimeClient(opts)
: resolveDiscordReactionClient(opts);
const message = (await rest.get(Routes.channelMessage(channelId, messageId))) as {
reactions?: Array<{
count: number;

View File

@@ -61,7 +61,6 @@ export type {
DiscordChannelCreate,
DiscordChannelEdit,
DiscordChannelMove,
DiscordOptionalRuntimeAccountContext,
DiscordChannelPermissionSet,
DiscordEmojiUpload,
DiscordMessageEdit,

View File

@@ -33,13 +33,6 @@ export type DiscordRuntimeAccountContext = {
accountId: string;
};
export type DiscordOptionalRuntimeAccountContext =
| DiscordRuntimeAccountContext
| {
cfg?: undefined;
accountId?: undefined;
};
export type DiscordReactOpts = {
cfg?: OpenClawConfig;
accountId?: string;