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

183 lines
5.2 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Zalo
description: Community Zalo Bot adapter for Chat SDK using the Zalo Bot Platform API.
packageName: chat-adapter-zalo
slug: zalo
type: platform
tagline: Community Zalo Bot adapter for Chat SDK using the Zalo Bot Platform API. Buffered streaming, auto-chunking, typing indicators, and webhook signature verification.
community: true
mdxBody: true
features:
postMessage: yes
editMessage: no
deleteMessage: no
fileUploads:
status: partial
label: Inbound only
streaming:
status: partial
label: Buffered
scheduledMessages: no
cardFormat: no
buttons: no
linkButtons: no
selectMenus: no
tables: no
fields: no
imagesInCards: no
modals: no
slashCommands: no
mentions: yes
addReactions: no
removeReactions: no
typingIndicator: yes
messageUpdatedEvents: no
messageDeletedEvents: no
directMessages: yes
ephemeralMessages: no
userLookup: no
customApiEndpoint: no
fetchMessages: no
fetchSingleMessage: no
fetchThreadInfo: yes
fetchChannelMessages: no
listThreads: no
fetchChannelInfo: yes
postChannelMessage: yes
---
## Install
<PackageInstall package="chat-adapter-zalo chat @chat-adapter/shared" />
## Quick start
```typescript title="lib/bot.ts" lineNumbers
import { Chat } from "chat";
import { createZaloAdapter } from "chat-adapter-zalo";
const bot = new Chat({
userName: "mybot",
adapters: {
zalo: createZaloAdapter(),
},
});
bot.onNewMention(async (thread, message) => {
await thread.post(`You said: ${message.text}`);
});
```
When called with no arguments, `createZaloAdapter()` reads its credentials from environment variables.
## Zalo Bot setup
### 1. Create a Zalo Bot
1. Go to [bot.zapps.me](https://bot.zapps.me) and sign in with your Zalo account.
2. Create a new bot and copy your **Bot Token** (format: `12345689:abc-xyz`).
3. In the **Webhooks** section, set your webhook URL.
### 2. Configure webhooks
1. In the Zalo Bot dashboard, navigate to **Webhooks**.
2. Set **Webhook URL** to `https://your-domain.com/api/webhooks/zalo`.
3. Set a **Secret Token** of your choice (8–256 characters) — this becomes `ZALO_WEBHOOK_SECRET`.
4. Subscribe to the message events you need (`message.text.received`, `message.image.received`, etc.).
### 3. Get credentials
Copy these values from the dashboard:
- **Bot Token** → `ZALO_BOT_TOKEN`
- The **Secret Token** you set in the webhook config → `ZALO_WEBHOOK_SECRET`
## Configuration
All options are auto-detected from environment variables when not provided.
<TypeTable
type={{
botToken: {
type: "string",
description:
"Zalo bot token. Auto-detected from `ZALO_BOT_TOKEN`. Required at runtime.",
},
webhookSecret: {
type: "string",
description:
"Secret token for webhook verification. Auto-detected from `ZALO_WEBHOOK_SECRET`. Required at runtime.",
},
userName: {
type: "string",
default: '"zalo-bot"',
description:
"Bot display name. Auto-detected from `ZALO_BOT_USERNAME` when omitted.",
},
logger: {
type: "Logger",
default: 'ConsoleLogger("info")',
description: "Logger instance.",
},
}}
/>
## Environment variables
```bash title=".env.local"
ZALO_BOT_TOKEN=12345689:abc-xyz # Bot token from Zalo Bot dashboard
ZALO_WEBHOOK_SECRET=your-secret # Secret token for X-Bot-Api-Secret-Token verification
ZALO_BOT_USERNAME=mybot # Optional, defaults to "zalo-bot"
```
## Webhook setup
```typescript title="app/api/webhooks/zalo/route.ts" lineNumbers
import { bot } from "@/lib/bot";
export async function POST(request: Request) {
return bot.webhooks.zalo(request);
}
```
Zalo delivers all events via POST requests with an `X-Bot-Api-Secret-Token` header. The adapter verifies this header using timing-safe comparison before processing any payload.
## Thread ID format
```
zalo:{chatId}
```
Example: `zalo:1234567890`
The `chatId` is the conversation ID from the Zalo webhook payload. For group chats it is the group ID; for private chats it is the user ID.
## Notes
- Zalo does not expose message history APIs to bots — `fetchMessages` returns an empty array.
- All formatting (bold, italic, code blocks) is stripped to plain text; Zalo renders no markdown.
- The bot token is embedded in the API URL path and is never logged.
- `isDM()` always returns `true` — Zalo thread IDs do not encode chat type.
- Streaming is buffered: tokens accumulate and the adapter auto-chunks at 2000 characters.
## Troubleshooting
### Webhook verification failing
- Confirm `ZALO_WEBHOOK_SECRET` matches the value you entered in the Zalo Bot dashboard.
- The adapter compares the `X-Bot-Api-Secret-Token` header using a timing-safe byte comparison — ensure the secret contains only ASCII characters and has no trailing whitespace.
### Messages not arriving
- Verify your webhook URL is reachable and returns `200 OK`.
- Check that the event types you need are subscribed in the Zalo Bot dashboard.
### "Zalo API error" on send
- Confirm `ZALO_BOT_TOKEN` is correct — it should be in `12345689:abc-xyz` format.
- The adapter calls `getMe` during `initialize()` to validate the token; check logs for initialization errors.
## Feature support
<FeatureSupport />