refactor: centralize ack reaction removal
This commit is contained in:
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { EventEmitter } from "node:events";
|
||||
|
||||
import { shouldAckReaction } from "clawdbot/plugin-sdk";
|
||||
import { removeAckReactionAfterReply, shouldAckReaction } from "clawdbot/plugin-sdk";
|
||||
import type { ClawdbotConfig, PluginRuntime } from "clawdbot/plugin-sdk";
|
||||
import {
|
||||
handleBlueBubblesWebhookRequest,
|
||||
@@ -138,6 +138,7 @@ function createMockRuntime(): PluginRuntime {
|
||||
},
|
||||
reactions: {
|
||||
shouldAckReaction,
|
||||
removeAckReactionAfterReply,
|
||||
},
|
||||
groups: {
|
||||
resolveGroupPolicy: mockResolveGroupPolicy as unknown as PluginRuntime["channel"]["groups"]["resolveGroupPolicy"],
|
||||
|
||||
@@ -1750,29 +1750,26 @@ async function processMessage(
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
if (
|
||||
removeAckAfterReply &&
|
||||
sentMessage &&
|
||||
ackReactionPromise &&
|
||||
ackReactionValue &&
|
||||
chatGuidForActions &&
|
||||
ackMessageId
|
||||
) {
|
||||
void ackReactionPromise.then((didAck) => {
|
||||
if (!didAck) return;
|
||||
sendBlueBubblesReaction({
|
||||
chatGuid: chatGuidForActions,
|
||||
messageGuid: ackMessageId,
|
||||
emoji: ackReactionValue,
|
||||
remove: true,
|
||||
opts: { cfg: config, accountId: account.accountId },
|
||||
}).catch((err) => {
|
||||
if (sentMessage && chatGuidForActions && ackMessageId) {
|
||||
core.channel.reactions.removeAckReactionAfterReply({
|
||||
removeAfterReply: removeAckAfterReply,
|
||||
ackReactionPromise,
|
||||
ackReactionValue: ackReactionValue ?? null,
|
||||
remove: () =>
|
||||
sendBlueBubblesReaction({
|
||||
chatGuid: chatGuidForActions,
|
||||
messageGuid: ackMessageId,
|
||||
emoji: ackReactionValue ?? "",
|
||||
remove: true,
|
||||
opts: { cfg: config, accountId: account.accountId },
|
||||
}),
|
||||
onError: (err) => {
|
||||
logVerbose(
|
||||
core,
|
||||
runtime,
|
||||
`ack reaction removal failed chatGuid=${chatGuidForActions} msg=${ackMessageId}: ${String(err)}`,
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
if (chatGuidForActions && baseUrl && password && !sentMessage) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { shouldAckReaction, shouldAckReactionForWhatsApp } from "./ack-reactions.js";
|
||||
import {
|
||||
removeAckReactionAfterReply,
|
||||
shouldAckReaction,
|
||||
shouldAckReactionForWhatsApp,
|
||||
} from "./ack-reactions.js";
|
||||
|
||||
describe("shouldAckReaction", () => {
|
||||
it("honors direct and group-all scopes", () => {
|
||||
@@ -222,3 +226,44 @@ describe("shouldAckReactionForWhatsApp", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeAckReactionAfterReply", () => {
|
||||
it("removes only when ack succeeded", async () => {
|
||||
const remove = vi.fn().mockResolvedValue(undefined);
|
||||
const onError = vi.fn();
|
||||
removeAckReactionAfterReply({
|
||||
removeAfterReply: true,
|
||||
ackReactionPromise: Promise.resolve(true),
|
||||
ackReactionValue: "👀",
|
||||
remove,
|
||||
onError,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(remove).toHaveBeenCalledTimes(1);
|
||||
expect(onError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips removal when ack did not happen", async () => {
|
||||
const remove = vi.fn().mockResolvedValue(undefined);
|
||||
removeAckReactionAfterReply({
|
||||
removeAfterReply: true,
|
||||
ackReactionPromise: Promise.resolve(false),
|
||||
ackReactionValue: "👀",
|
||||
remove,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(remove).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips when not configured", async () => {
|
||||
const remove = vi.fn().mockResolvedValue(undefined);
|
||||
removeAckReactionAfterReply({
|
||||
removeAfterReply: false,
|
||||
ackReactionPromise: Promise.resolve(true),
|
||||
ackReactionValue: "👀",
|
||||
remove,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(remove).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,3 +53,19 @@ export function shouldAckReactionForWhatsApp(params: {
|
||||
shouldBypassMention: params.groupActivated,
|
||||
});
|
||||
}
|
||||
|
||||
export function removeAckReactionAfterReply(params: {
|
||||
removeAfterReply: boolean;
|
||||
ackReactionPromise: Promise<boolean> | null;
|
||||
ackReactionValue: string | null;
|
||||
remove: () => Promise<void>;
|
||||
onError?: (err: unknown) => void;
|
||||
}) {
|
||||
if (!params.removeAfterReply) return;
|
||||
if (!params.ackReactionPromise) return;
|
||||
if (!params.ackReactionValue) return;
|
||||
void params.ackReactionPromise.then((didAck) => {
|
||||
if (!didAck) return;
|
||||
params.remove().catch((err) => params.onError?.(err));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,10 @@ import {
|
||||
extractShortModelName,
|
||||
type ResponsePrefixContext,
|
||||
} from "../../auto-reply/reply/response-prefix-template.js";
|
||||
import { shouldAckReaction as shouldAckReactionGate } from "../../channels/ack-reactions.js";
|
||||
import {
|
||||
removeAckReactionAfterReply,
|
||||
shouldAckReaction as shouldAckReactionGate,
|
||||
} from "../../channels/ack-reactions.js";
|
||||
import {
|
||||
formatInboundEnvelope,
|
||||
formatThreadStarterEnvelope,
|
||||
@@ -394,19 +397,18 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
`discord: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${replyTarget}`,
|
||||
);
|
||||
}
|
||||
if (removeAckAfterReply && ackReactionPromise && ackReaction) {
|
||||
const ackReactionValue = ackReaction;
|
||||
void ackReactionPromise.then((didAck) => {
|
||||
if (!didAck) return;
|
||||
removeReactionDiscord(message.channelId, message.id, ackReactionValue, {
|
||||
rest: client.rest,
|
||||
}).catch((err) => {
|
||||
logVerbose(
|
||||
`discord: failed to remove ack reaction from ${message.channelId}/${message.id}: ${String(err)}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
removeAckReactionAfterReply({
|
||||
removeAfterReply: removeAckAfterReply,
|
||||
ackReactionPromise,
|
||||
ackReactionValue: ackReaction,
|
||||
remove: () =>
|
||||
removeReactionDiscord(message.channelId, message.id, ackReaction, { rest: client.rest }),
|
||||
onError: (err) => {
|
||||
logVerbose(
|
||||
`discord: failed to remove ack reaction from ${message.channelId}/${message.id}: ${String(err)}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
if (isGuildMessage && historyLimit > 0) {
|
||||
clearHistoryEntries({
|
||||
historyMap: guildHistories,
|
||||
|
||||
@@ -122,7 +122,11 @@ export type {
|
||||
AckReactionScope,
|
||||
WhatsAppAckReactionMode,
|
||||
} from "../channels/ack-reactions.js";
|
||||
export { shouldAckReaction, shouldAckReactionForWhatsApp } from "../channels/ack-reactions.js";
|
||||
export {
|
||||
removeAckReactionAfterReply,
|
||||
shouldAckReaction,
|
||||
shouldAckReactionForWhatsApp,
|
||||
} from "../channels/ack-reactions.js";
|
||||
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";
|
||||
export type { NormalizedLocation } from "../channels/location.js";
|
||||
export { formatLocationText, toLocationContext } from "../channels/location.js";
|
||||
|
||||
@@ -25,7 +25,7 @@ import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../a
|
||||
import { createMemoryGetTool, createMemorySearchTool } from "../../agents/tools/memory-tool.js";
|
||||
import { handleSlackAction } from "../../agents/tools/slack-actions.js";
|
||||
import { handleWhatsAppAction } from "../../agents/tools/whatsapp-actions.js";
|
||||
import { shouldAckReaction } from "../../channels/ack-reactions.js";
|
||||
import { removeAckReactionAfterReply, shouldAckReaction } from "../../channels/ack-reactions.js";
|
||||
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
|
||||
import { discordMessageActions } from "../../channels/plugins/actions/discord.js";
|
||||
import { telegramMessageActions } from "../../channels/plugins/actions/telegram.js";
|
||||
@@ -201,6 +201,7 @@ export function createPluginRuntime(): PluginRuntime {
|
||||
},
|
||||
reactions: {
|
||||
shouldAckReaction,
|
||||
removeAckReactionAfterReply,
|
||||
},
|
||||
groups: {
|
||||
resolveGroupPolicy: resolveChannelGroupPolicy,
|
||||
|
||||
@@ -20,6 +20,8 @@ type BuildMentionRegexes = typeof import("../../auto-reply/reply/mentions.js").b
|
||||
type MatchesMentionPatterns =
|
||||
typeof import("../../auto-reply/reply/mentions.js").matchesMentionPatterns;
|
||||
type ShouldAckReaction = typeof import("../../channels/ack-reactions.js").shouldAckReaction;
|
||||
type RemoveAckReactionAfterReply =
|
||||
typeof import("../../channels/ack-reactions.js").removeAckReactionAfterReply;
|
||||
type ResolveChannelGroupPolicy =
|
||||
typeof import("../../config/group-policy.js").resolveChannelGroupPolicy;
|
||||
type ResolveChannelGroupRequireMention =
|
||||
@@ -214,6 +216,7 @@ export type PluginRuntime = {
|
||||
};
|
||||
reactions: {
|
||||
shouldAckReaction: ShouldAckReaction;
|
||||
removeAckReactionAfterReply: RemoveAckReactionAfterReply;
|
||||
};
|
||||
groups: {
|
||||
resolveGroupPolicy: ResolveChannelGroupPolicy;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../../../auto-reply/reply/response-prefix-template.js";
|
||||
import { dispatchInboundMessage } from "../../../auto-reply/dispatch.js";
|
||||
import { clearHistoryEntries } from "../../../auto-reply/reply/history.js";
|
||||
import { removeAckReactionAfterReply } from "../../../channels/ack-reactions.js";
|
||||
import { createReplyDispatcherWithTyping } from "../../../auto-reply/reply/reply-dispatcher.js";
|
||||
import { resolveStorePath, updateLastRoute } from "../../../config/sessions.js";
|
||||
import { danger, logVerbose, shouldLogVerbose } from "../../../globals.js";
|
||||
@@ -152,21 +153,26 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
);
|
||||
}
|
||||
|
||||
if (ctx.removeAckAfterReply && prepared.ackReactionPromise && prepared.ackReactionMessageTs) {
|
||||
const messageTs = prepared.ackReactionMessageTs;
|
||||
const ackValue = prepared.ackReactionValue;
|
||||
void prepared.ackReactionPromise.then((didAck) => {
|
||||
if (!didAck) return;
|
||||
removeSlackReaction(message.channel, messageTs, ackValue, {
|
||||
token: ctx.botToken,
|
||||
client: ctx.app.client,
|
||||
}).catch((err) => {
|
||||
logVerbose(
|
||||
`slack: failed to remove ack reaction from ${message.channel}/${message.ts}: ${String(err)}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
removeAckReactionAfterReply({
|
||||
removeAfterReply: ctx.removeAckAfterReply,
|
||||
ackReactionPromise: prepared.ackReactionPromise,
|
||||
ackReactionValue: prepared.ackReactionValue,
|
||||
remove: () =>
|
||||
removeSlackReaction(
|
||||
message.channel,
|
||||
prepared.ackReactionMessageTs ?? "",
|
||||
prepared.ackReactionValue,
|
||||
{
|
||||
token: ctx.botToken,
|
||||
client: ctx.app.client,
|
||||
},
|
||||
),
|
||||
onError: (err) => {
|
||||
logVerbose(
|
||||
`slack: failed to remove ack reaction from ${message.channel}/${message.ts}: ${String(err)}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
if (prepared.isRoomish && ctx.historyLimit > 0) {
|
||||
clearHistoryEntries({
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js";
|
||||
import { clearHistoryEntries } from "../auto-reply/reply/history.js";
|
||||
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
|
||||
import { removeAckReactionAfterReply } from "../channels/ack-reactions.js";
|
||||
import { danger, logVerbose } from "../globals.js";
|
||||
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
|
||||
import { deliverReplies } from "./bot/delivery.js";
|
||||
@@ -184,16 +185,18 @@ export const dispatchTelegramMessage = async ({
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (removeAckAfterReply && ackReactionPromise && msg.message_id && reactionApi) {
|
||||
void ackReactionPromise.then((didAck) => {
|
||||
if (!didAck) return;
|
||||
reactionApi(chatId, msg.message_id, []).catch((err) => {
|
||||
logVerbose(
|
||||
`telegram: failed to remove ack reaction from ${chatId}/${msg.message_id}: ${String(err)}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
removeAckReactionAfterReply({
|
||||
removeAfterReply: removeAckAfterReply,
|
||||
ackReactionPromise,
|
||||
ackReactionValue: ackReactionPromise ? "ack" : null,
|
||||
remove: () => reactionApi?.(chatId, msg.message_id ?? 0, []) ?? Promise.resolve(),
|
||||
onError: (err) => {
|
||||
if (!msg.message_id) return;
|
||||
logVerbose(
|
||||
`telegram: failed to remove ack reaction from ${chatId}/${msg.message_id}: ${String(err)}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
if (isGroup && historyKey && historyLimit > 0) {
|
||||
clearHistoryEntries({ historyMap: groupHistories, historyKey });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user