Files
clawdbot/extensions/bluebubbles/src/reactions.ts

115 lines
3.4 KiB
TypeScript

import { resolveBlueBubblesAccount } from "./accounts.js";
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js";
export type BlueBubblesReactionOpts = {
serverUrl?: string;
password?: string;
accountId?: string;
timeoutMs?: number;
cfg?: ClawdbotConfig;
};
const REACTION_TYPES = new Set([
"love",
"like",
"dislike",
"laugh",
"emphasize",
"question",
]);
const REACTION_ALIASES = new Map<string, string>([
["heart", "love"],
["thumbs_up", "like"],
["thumbs-down", "dislike"],
["thumbs_down", "dislike"],
["haha", "laugh"],
["lol", "laugh"],
["emphasis", "emphasize"],
["exclaim", "emphasize"],
["question", "question"],
]);
const REACTION_EMOJIS = new Map<string, string>([
["❤️", "love"],
["❤", "love"],
["♥️", "love"],
["😍", "love"],
["👍", "like"],
["👎", "dislike"],
["😂", "laugh"],
["🤣", "laugh"],
["😆", "laugh"],
["‼️", "emphasize"],
["‼", "emphasize"],
["❗", "emphasize"],
["❓", "question"],
["❔", "question"],
]);
function resolveAccount(params: BlueBubblesReactionOpts) {
const account = resolveBlueBubblesAccount({
cfg: params.cfg ?? {},
accountId: params.accountId,
});
const baseUrl = params.serverUrl?.trim() || account.config.serverUrl?.trim();
const password = params.password?.trim() || account.config.password?.trim();
if (!baseUrl) throw new Error("BlueBubbles serverUrl is required");
if (!password) throw new Error("BlueBubbles password is required");
return { baseUrl, password };
}
export function normalizeBlueBubblesReactionInput(emoji: string, remove?: boolean): string {
const trimmed = emoji.trim();
if (!trimmed) throw new Error("BlueBubbles reaction requires an emoji or name.");
let raw = trimmed.toLowerCase();
if (raw.startsWith("-")) raw = raw.slice(1);
const aliased = REACTION_ALIASES.get(raw) ?? raw;
const mapped = REACTION_EMOJIS.get(trimmed) ?? REACTION_EMOJIS.get(raw) ?? aliased;
if (!REACTION_TYPES.has(mapped)) {
throw new Error(`Unsupported BlueBubbles reaction: ${trimmed}`);
}
return remove ? `-${mapped}` : mapped;
}
export async function sendBlueBubblesReaction(params: {
chatGuid: string;
messageGuid: string;
emoji: string;
remove?: boolean;
partIndex?: number;
opts?: BlueBubblesReactionOpts;
}): Promise<void> {
const chatGuid = params.chatGuid.trim();
const messageGuid = params.messageGuid.trim();
if (!chatGuid) throw new Error("BlueBubbles reaction requires chatGuid.");
if (!messageGuid) throw new Error("BlueBubbles reaction requires messageGuid.");
const reaction = normalizeBlueBubblesReactionInput(params.emoji, params.remove);
const { baseUrl, password } = resolveAccount(params.opts ?? {});
const url = buildBlueBubblesApiUrl({
baseUrl,
path: "/api/v1/message/react",
password,
});
const payload = {
chatGuid,
selectedMessageGuid: messageGuid,
reaction,
partIndex: typeof params.partIndex === "number" ? params.partIndex : 0,
};
const res = await blueBubblesFetchWithTimeout(
url,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
},
params.opts?.timeoutMs,
);
if (!res.ok) {
const errorText = await res.text();
throw new Error(`BlueBubbles reaction failed (${res.status}): ${errorText || "unknown"}`);
}
}