From 6454044748bb2a81898c88d723e57c9f12d60f6e Mon Sep 17 00:00:00 2001 From: Miki Palet <64255955+mikipalet@users.noreply.github.com> Date: Sat, 15 Aug 2026 06:18:48 +0000 Subject: [PATCH] fix: send platform message ids and unicode emoji when adding reactions Any addReaction on a webhook-received message failed: parseMessage used the Zernio internal message id, but the reactions endpoint forwards the id verbatim to the platform, which expects its native id. Message.id (and ReactionEvent.messageId) now carry the platform-native id, matching what REST fetches and postMessage already return. Passing an EmojiValue also failed on every platform: the adapter sent emoji.name ("thumbs_up") where the API expects the unicode character. Emoji are now normalized through the resolver, which passes raw unicode through. Fixes #9 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019c8DtXpqqSunXt4rWRQRTy --- README.md | 4 ++-- src/adapter.test.ts | 32 +++++++++++++++++++++++++++++--- src/adapter.ts | 24 +++++++++++++++++------- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 0ea897b..f3e72d5 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ const { accountId, conversationId } = adapter.decodeThreadId(threadId); | Open conversation by recipient | Yes | `openDM` / `openConversation` — cold-start a chat from a phone number ([see below](#opening-conversations)) | | Edit messages | Partial | Telegram only | | Delete messages | Partial | Telegram, X (full delete); Bluesky, Reddit (self-only) | -| Send reactions | Partial | Telegram and WhatsApp (add/remove emoji) | +| Send reactions | Partial | Telegram, WhatsApp, Instagram, Facebook Messenger (add/remove emoji) | | Receive reactions (`onReaction`) | Partial | WhatsApp, Telegram (via the `reaction.received` webhook) | | Typing indicators | Partial | Facebook Messenger, Instagram, and Telegram; WhatsApp (requires a recent inbound message in the conversation) | | AI streaming | Partial | Post+edit on Telegram; single post on others | @@ -157,7 +157,7 @@ const { accountId, conversationId } = adapter.decodeThreadId(threadId); | Templates / Flows | - | - | - | Y | - | - | - | | Typing | Y | Y | Y | Y | - | - | - | | Delete | - | - | Y | - | Y | Self | Self | -| Reactions | - | - | Y | Y | - | - | - | +| Reactions | Y | Y | Y | Y | - | - | - | | Media | Y | Y | Y | Y | Y | - | - | | Edit | - | - | Y | - | - | - | - | diff --git a/src/adapter.test.ts b/src/adapter.test.ts index 6dca960..450d45b 100644 --- a/src/adapter.test.ts +++ b/src/adapter.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from "vitest"; import { createHmac } from "node:crypto"; -import { Message } from "chat"; +import { Message, defaultEmojiResolver } from "chat"; import { ValidationError, AdapterError } from "@chat-adapter/shared"; import { ZernioAdapter } from "./adapter.js"; import type { ZernioRawMessage, ZernioWebhookPayload } from "./types.js"; @@ -200,11 +200,22 @@ describe("parseMessage", () => { const msg = adapter.parseMessage(raw); expect(msg).toBeInstanceOf(Message); - expect(msg.id).toBe("msg-123"); + // The platform-native id, NOT the Zernio internal id: every outbound + // message op (reactions, edit, delete) passes this id to the Zernio API, + // which forwards it verbatim to the platform. Using the internal id made + // every reaction on a webhook-received message fail (issue #9). It also + // matches the ids returned by REST fetches and postMessage. + expect(msg.id).toBe("ig-msg-789"); expect(msg.text).toBe("Hello from Instagram"); expect(msg.raw).toBe(raw); }); + it("falls back to the Zernio id when platformMessageId is missing", () => { + const raw = makeRawMessage({ platformMessageId: "" }); + const msg = adapter.parseMessage(raw); + expect(msg.id).toBe("msg-123"); + }); + it("maps author fields correctly", () => { const raw = makeRawMessage(); const msg = adapter.parseMessage(raw); @@ -499,7 +510,9 @@ describe("handleWebhook", () => { expect(event.threadId).toBe("zernio:acc-789:conv-456"); expect(event.added).toBe(true); expect(event.rawEmoji).toBe("👍"); - expect(event.messageId).toBe("msg-zernio-1"); + // Platform-native id, consistent with Message.id everywhere else in the + // adapter, so handlers can react/edit/delete using event.messageId. + expect(event.messageId).toBe("wamid.REACTED"); expect(event.user.userId).toBe("13866666863"); // Normalized to a known name via the unicode resolver. expect(event.emoji.name).toBe("thumbs_up"); @@ -602,6 +615,19 @@ describe("API-backed methods", () => { expect(body.accountId).toBe("acc-1"); }); + it("addReaction converts an EmojiValue to the unicode emoji, not its name (issue #9)", async () => { + // Regression: chat-sdk hands the adapter EmojiValue objects; sending + // emoji.name ("thumbs_up") made every platform reject the reaction. + vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( + new Response(JSON.stringify({ success: true }), { status: 200 }), + ); + const emojiValue = defaultEmojiResolver.fromGChat("👍"); + expect(emojiValue.name).toBe("thumbs_up"); + await adapter.addReaction("zernio:acc-1:conv-2", "msg-3", emojiValue); + const body = JSON.parse((fetch as any).mock.calls[0][1].body); + expect(body.emoji).toBe("👍"); + }); + it("removeReaction calls the API DELETE endpoint", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( new Response(JSON.stringify({ success: true }), { status: 200 }), diff --git a/src/adapter.ts b/src/adapter.ts index a3d9ee6..63fe875 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -269,9 +269,10 @@ export class ZernioAdapter implements Adapter added: reaction.action === "added", emoji: emojiValue, rawEmoji: reaction.emoji, - // The message that was reacted to. Prefer the Zernio id; fall back to the - // platform id (always present). - messageId: reaction.messageId ?? reaction.platformMessageId, + // The message that was reacted to: the platform-native id, consistent + // with Message.id everywhere else in the adapter, so handlers can pass + // it straight back to addReaction/editMessage/deleteMessage (issue #9). + messageId: reaction.platformMessageId || reaction.messageId || "", threadId, user: { userId: reaction.sender.id, @@ -334,7 +335,12 @@ export class ZernioAdapter implements Adapter const text = raw.text ?? ""; return new Message({ - id: raw.id, + // The platform-native id, not the Zernio internal id: message ops + // (reactions, edit, delete) send this id to the Zernio API, which + // forwards it verbatim to the platform. The internal id made every such + // op on a webhook-received message fail (issue #9), and REST fetches + + // postMessage already return platform ids. The Zernio id stays on raw.id. + id: raw.platformMessageId || raw.id, threadId: "", // Set by chat-sdk's processMessage text, formatted: this.converter.toAst(text), @@ -575,7 +581,7 @@ export class ZernioAdapter implements Adapter /** * Add a reaction to a message. - * Supported on: Telegram (emoji reactions), WhatsApp (emoji reactions). + * Supported on: Telegram, WhatsApp, Slack, Instagram, Facebook Messenger. * Unsupported on other platforms (API returns 400). */ async addReaction( @@ -584,13 +590,17 @@ export class ZernioAdapter implements Adapter emoji: EmojiValue | string, ): Promise { const { accountId, conversationId } = this.decodeThreadId(threadId); - const emojiStr = typeof emoji === "string" ? emoji : emoji.name; + // Zernio's wire format is the unicode character. toGChat maps an EmojiValue + // or normalized name ("thumbs_up") to unicode and passes through anything it + // doesn't know (an already-unicode "👍"). Sending emoji.name made every + // platform reject the reaction (issue #9). + const emojiStr = defaultEmojiResolver.toGChat(emoji); await this.api.addReaction(conversationId, messageId, accountId, emojiStr); } /** * Remove a reaction from a message. - * Supported on: Telegram (send empty reaction), WhatsApp (send empty emoji). + * Supported on: Telegram, WhatsApp, Slack, Instagram, Facebook Messenger. * Unsupported on other platforms (API returns 400). * * Note: The `emoji` parameter is required by the chat-sdk Adapter interface but