fix(whatsapp): remove ack reactions after replies

This commit is contained in:
Peter Steinberger
2026-04-26 05:35:24 +01:00
parent 427e485f76
commit 9b93b7df62
16 changed files with 329 additions and 32 deletions

View File

@@ -1,5 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import {
createAckReactionHandle,
removeAckReactionHandleAfterReply,
removeAckReactionAfterReply,
shouldAckReaction,
shouldAckReactionForWhatsApp,
@@ -178,6 +180,48 @@ describe("shouldAckReactionForWhatsApp", () => {
});
});
describe("createAckReactionHandle", () => {
it("tracks a successful ack send", async () => {
const send = vi.fn().mockResolvedValue(undefined);
const remove = vi.fn().mockResolvedValue(undefined);
const handle = createAckReactionHandle({
ackReactionValue: " 👀 ",
send,
remove,
});
expect(handle).toMatchObject({ ackReactionValue: "👀", remove });
expect(send).toHaveBeenCalledTimes(1);
await expect(handle?.ackReactionPromise).resolves.toBe(true);
});
it("tracks a failed ack send without throwing", async () => {
const error = new Error("nope");
const onSendError = vi.fn();
const handle = createAckReactionHandle({
ackReactionValue: "👀",
send: vi.fn().mockRejectedValue(error),
remove: vi.fn().mockResolvedValue(undefined),
onSendError,
});
await expect(handle?.ackReactionPromise).resolves.toBe(false);
expect(onSendError).toHaveBeenCalledWith(error);
});
it("skips empty ack values", () => {
const handle = createAckReactionHandle({
ackReactionValue: " ",
send: vi.fn().mockResolvedValue(undefined),
remove: vi.fn().mockResolvedValue(undefined),
});
expect(handle).toBeNull();
});
});
describe("removeAckReactionAfterReply", () => {
it("removes only when ack succeeded", async () => {
const remove = vi.fn().mockResolvedValue(undefined);
@@ -206,3 +250,20 @@ describe("removeAckReactionAfterReply", () => {
expect(remove).not.toHaveBeenCalled();
});
});
describe("removeAckReactionHandleAfterReply", () => {
it("removes through an ack handle", async () => {
const remove = vi.fn().mockResolvedValue(undefined);
removeAckReactionHandleAfterReply({
removeAfterReply: true,
ackReaction: {
ackReactionPromise: Promise.resolve(true),
ackReactionValue: "👀",
remove,
},
});
await flushMicrotasks();
expect(remove).toHaveBeenCalledTimes(1);
});
});

View File

@@ -2,6 +2,12 @@ export type AckReactionScope = "all" | "direct" | "group-all" | "group-mentions"
export type WhatsAppAckReactionMode = "always" | "mentions" | "never";
export type AckReactionHandle = {
ackReactionPromise: Promise<boolean>;
ackReactionValue: string;
remove: () => Promise<void>;
};
export type AckReactionGateParams = {
scope: AckReactionScope | undefined;
isDirect: boolean;
@@ -78,6 +84,37 @@ export function shouldAckReactionForWhatsApp(params: {
});
}
export function createAckReactionHandle(params: {
ackReactionValue: string;
send: () => Promise<void>;
remove: () => Promise<void>;
onSendError?: (err: unknown) => void;
}): AckReactionHandle | null {
const ackReactionValue = params.ackReactionValue.trim();
if (!ackReactionValue) {
return null;
}
let sendPromise: Promise<void>;
try {
sendPromise = params.send();
} catch (err) {
sendPromise = Promise.reject(err);
}
return {
ackReactionPromise: sendPromise.then(
() => true,
(err) => {
params.onSendError?.(err);
return false;
},
),
ackReactionValue,
remove: params.remove,
};
}
export function removeAckReactionAfterReply(params: {
removeAfterReply: boolean;
ackReactionPromise: Promise<boolean> | null;
@@ -101,3 +138,17 @@ export function removeAckReactionAfterReply(params: {
params.remove().catch((err) => params.onError?.(err));
});
}
export function removeAckReactionHandleAfterReply(params: {
removeAfterReply: boolean;
ackReaction: AckReactionHandle | null | undefined;
onError?: (err: unknown) => void;
}) {
removeAckReactionAfterReply({
removeAfterReply: params.removeAfterReply,
ackReactionPromise: params.ackReaction?.ackReactionPromise ?? null,
ackReactionValue: params.ackReaction?.ackReactionValue ?? null,
remove: params.ackReaction?.remove ?? (async () => {}),
onError: params.onError,
});
}