Files
josh 4ac0455134 feat(chat): add message update and delete lifecycle callbacks (#788)
## summary

adds `onMessageUpdated` and `onMessageDeleted`, so a bot can react when
a message is edited or removed. Slack dispatches both today; other
adapters can opt in later

supersedes #549, which was verified there against real Slack webhooks.
reopened from a branch in this repo with the original commits preserved
and signed

```typescript
bot.onMessageUpdated(async (thread, message, previousMessage) => {
  await mirror.update(message.id, message.text);
});

bot.onMessageDeleted(async (event) => {
  await mirror.remove(event.messageId);
});
```

both are lifecycle events: they never route through `onNewMessage`,
`onNewMention`, or `onSubscribedMessage`, and the concurrency strategies
do not apply

### notes

- **the bot's own edits are filtered.** slack sends a `message_changed`
for every `chat.update`, and post-and-edit streaming calls it once per
delta, so without this a single streamed reply would call the handler
back repeatedly on its own message
- **`previousMessage` is forwarded on edits.** slack sends the pre-edit
message and it was being dropped. an edit handler usually needs the
before to know what changed, so it is the optional third argument
- **the two shapes differ deliberately.** an edit carries a full
replacement message, so it gets `(thread, message, previousMessage?)`. a
delete has no message, only the id of what was removed, so it gets an
event. use `chat.thread(event.threadId)` when a delete handler needs one
- **one thread id helper** now serves message, edit, and delete, so an
edit cannot resolve to a different thread than the message it edits

## test plan

core:

- an edit dispatches to `onMessageUpdated` and not to the normal message
handlers
- the handler receives the pre-edit message as its third argument
- the bot's own edits are skipped
- a delete dispatches with normalized event data
- both run inside the active conversation, so read tools built in these
handlers stay scoped

slack:

- `message_changed` dispatches as an update, `message_deleted` as a
delete
- `previous_message` is forwarded, and left undefined when slack omits
it
- hidden unfurl updates stay ignored, hidden real edits still dispatch
- message, edit, and delete resolve to one thread id in a flat DM and in
a threaded `agent_view` DM

verified against a real slack workspace over socket mode: editing and
deleting a DM both routed to the same thread id as the original message

---------

Co-authored-by: Miłosz Lenczewski <m.lenczewski@tidio.net>
2026-08-05 13:22:54 +01:00

177 lines
7.2 KiB
Plaintext

---
title: Zaileys (WhatsApp)
description: Community WhatsApp adapter for Chat SDK powered by Zaileys, a batteries-included wrapper around the unofficial WhatsApp Web API. Self-hosted via WebSocket with QR / pairing-code auth, real message history, native buttons from Cards, decrypted poll votes, and scheduled messages.
packageName: chat-adapter-zaileys
slug: zaileys
type: platform
tagline: WhatsApp adapter powered by Zaileys — real fetchMessages history, Cards rendered as native WhatsApp buttons, natively decrypted poll votes, scheduling, and rich media. Auth, reconnection, and session persistence handled for you.
community: true
mdxBody: true
features:
postMessage: yes
editMessage: yes
deleteMessage: yes
fileUploads: yes
streaming:
status: partial
label: Post + edit fallback
scheduledMessages: yes
cardFormat:
status: partial
label: Buttons native, body as text
buttons: yes
linkButtons: no
selectMenus: no
tables: no
fields: no
imagesInCards:
status: partial
label: Header image only
modals: no
slashCommands:
status: partial
label: Opt-in prefix routing
mentions: yes
addReactions: yes
removeReactions: yes
typingIndicator: yes
messageUpdatedEvents: no
messageDeletedEvents: no
directMessages: yes
ephemeralMessages: no
userLookup: yes
customApiEndpoint: no
fetchMessages:
status: yes
label: Via the Zaileys store
fetchSingleMessage: yes
fetchThreadInfo: yes
fetchChannelMessages:
status: yes
label: Same as fetchMessages
listThreads: no
fetchChannelInfo: yes
postChannelMessage: yes
---
<Callout type="warn">
This adapter is powered by Zaileys, which builds on Baileys — a third-party unofficial WhatsApp Web API. It is **not** an official WhatsApp/Meta API and may break when WhatsApp changes internal protocols. WhatsApp may also suspend or ban numbers that use unofficial automation. Use at your own risk and evaluate compliance requirements before production use.
</Callout>
## Install
<PackageInstall package="chat-adapter-zaileys zaileys chat @chat-adapter/state-memory" />
## Quick start
Zaileys handles the WhatsApp lifecycle — QR / pairing-code auth, session persistence, and reconnection — so there is nothing to wire beyond `connect()`. Register handlers first, then connect:
```typescript title="bot.ts" lineNumbers
import { Chat } from "chat";
import { createMemoryState } from "@chat-adapter/state-memory";
import { createZaileysAdapter } from "chat-adapter-zaileys";
const whatsapp = createZaileysAdapter({
session: { sessionId: "main" }, // QR prints to the terminal on first run
});
const bot = new Chat({
userName: "mybot",
state: createMemoryState(),
adapters: { whatsapp },
});
bot.onNewMention(async (thread, message) => {
await thread.subscribe();
await thread.post(`Hello, ${message.author.fullName}!`);
});
await bot.initialize();
await whatsapp.connect();
```
Prefer a pairing code over QR scanning:
```typescript
const whatsapp = createZaileysAdapter({
session: { sessionId: "main", authType: "pairing", phoneNumber: "12345678901" },
});
```
## Transport
This adapter uses a persistent WebSocket, not HTTP webhooks — `handleWebhook` always returns `501`. Call `adapter.connect()` after registering handlers to start receiving messages.
## Message history
`thread.fetchMessages` returns real history backed by the Zaileys message store, with backward cursor pagination. Give the client a durable store (SQLite, Postgres, Redis, or Convex) for history that survives restarts:
```typescript
import { Client, SqliteMessageStore } from "zaileys";
import { createZaileysAdapter } from "chat-adapter-zaileys";
const client = new Client({
sessionId: "main",
store: new SqliteMessageStore({ database: "./wa.db" }),
});
const whatsapp = createZaileysAdapter({ client });
```
Attachments survive the SDK's `queue` / `debounce` concurrency strategies: the adapter implements `rehydrateAttachment`, re-downloading media by message key from the store.
## Cards and buttons
Cards render as native WhatsApp reply buttons. Button taps round-trip to `chat.onAction` with `actionId` and `value` intact. Link buttons, selects, and modals have no WhatsApp equivalent — card bodies fall back to formatted text.
## Polls
WhatsApp poll votes are end-to-end encrypted; Zaileys decrypts them natively with no `messageSecret` bookkeeping, and it works across restarts:
```typescript
import { requireZaileysAdapter } from "chat-adapter-zaileys";
const wa = requireZaileysAdapter(thread);
const poll = await wa.sendPoll({ threadId: thread.id, question: "Lunch?", options: ["A", "B"] });
wa.onPollVote(poll.id, (vote) => {
console.log(vote.voter.userName, vote.selectedOptions);
});
```
## WhatsApp-native extensions
Narrow a `Thread` or `Channel` with `requireZaileysAdapter` for quoted replies (`reply`), read receipts (`markRead`), presence (`setPresence`, `startTyping`, `startRecording`), locations, stickers (including animated Lottie), voice notes, contacts, message forwarding, pinning, disappearing messages, and group participant listing. `adapter.native(threadId)` exposes the entire Zaileys message builder (albums, carousels, lists, view-once media, mentions), and `adapter.client` exposes the full Zaileys client (groups, communities, newsletters, privacy, broadcast, plugins).
Every live inbound message also carries the full Zaileys `MessageContext` — `zaileysContext(message)` returns 20+ decoded flags, lazy media, and quoted-message decoding.
## Configuration
<TypeTable
type={{
client: { type: "Client", description: "An existing Zaileys client. Mutually exclusive with `session`." },
session: { type: "ClientOptions", description: "Zaileys client options used to construct a client — sessionId, authType, phoneNumber, storage adapters, plugins." },
adapterName: { type: "string", description: "Adapter identity and thread-ID prefix. Set a unique value per account for multi-account deployments. Defaults to `zaileys`." },
userName: { type: "string", description: "Bot display name. Defaults to `zaileys-bot`." },
forwardPollVotes: { type: "boolean", description: "Also deliver poll votes to message handlers with the selected options as text. Defaults to `true`." },
autoMarkRead: { type: "boolean", description: "Send read receipts for every inbound message. Defaults to `false`." },
richMessages: { type: "boolean", description: "Render markdown / AST posts through Zaileys AIRich rich bubbles instead of plain WhatsApp markup. Defaults to `false`." },
slashCommands: { type: "boolean", description: "Route prefixed messages (`/cmd args`) to `chat.onSlashCommand` using the Zaileys client's command prefixes. Defaults to `false`." },
}}
/>
## Feature support
<FeatureSupport />
## Limitations
- No modals or ephemeral messages — WhatsApp has no equivalent (`postEphemeral` falls back to DM).
- History depth equals what the Zaileys store has seen; forward pagination is not supported.
- `editMessage` edits text content; media edits require the native builder.
## Links
- [Documentation](https://zeative.github.io/chat-adapter-zaileys/)
- [GitHub](https://github.com/zeative/chat-adapter-zaileys)
- [npm](https://www.npmjs.com/package/chat-adapter-zaileys)
- [Zaileys documentation](https://zeative.github.io/zaileys/)