mirror of
https://github.com/vercel/chat.git
synced 2026-09-14 18:32:29 +08:00
4ac0455134
## 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>
177 lines
7.2 KiB
Plaintext
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/)
|