zalobot-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. zalobot_sdk-0.1.0/CHANGELOG.md +28 -0
  2. zalobot_sdk-0.1.0/LICENSE +21 -0
  3. zalobot_sdk-0.1.0/PKG-INFO +226 -0
  4. zalobot_sdk-0.1.0/README.md +193 -0
  5. zalobot_sdk-0.1.0/docs/api.md +35 -0
  6. zalobot_sdk-0.1.0/docs/changelog.md +3 -0
  7. zalobot_sdk-0.1.0/docs/errors.md +48 -0
  8. zalobot_sdk-0.1.0/docs/getting-started.md +64 -0
  9. zalobot_sdk-0.1.0/docs/handlers.md +81 -0
  10. zalobot_sdk-0.1.0/docs/index.md +29 -0
  11. zalobot_sdk-0.1.0/docs/polling-webhooks.md +73 -0
  12. zalobot_sdk-0.1.0/docs/rich-text.md +48 -0
  13. zalobot_sdk-0.1.0/docs/testing.md +32 -0
  14. zalobot_sdk-0.1.0/examples/__init__.py +0 -0
  15. zalobot_sdk-0.1.0/examples/echo_async.py +17 -0
  16. zalobot_sdk-0.1.0/examples/echo_polling.py +26 -0
  17. zalobot_sdk-0.1.0/examples/rich_text.py +24 -0
  18. zalobot_sdk-0.1.0/examples/webhook_fastapi.py +36 -0
  19. zalobot_sdk-0.1.0/examples/webhook_flask.py +36 -0
  20. zalobot_sdk-0.1.0/mkdocs.yml +50 -0
  21. zalobot_sdk-0.1.0/pyproject.toml +143 -0
  22. zalobot_sdk-0.1.0/pyproject.toml.orig +92 -0
  23. zalobot_sdk-0.1.0/src/zalobot/__init__.py +79 -0
  24. zalobot_sdk-0.1.0/src/zalobot/_core.py +309 -0
  25. zalobot_sdk-0.1.0/src/zalobot/client.py +270 -0
  26. zalobot_sdk-0.1.0/src/zalobot/dispatcher.py +527 -0
  27. zalobot_sdk-0.1.0/src/zalobot/errors.py +80 -0
  28. zalobot_sdk-0.1.0/src/zalobot/filters.py +90 -0
  29. zalobot_sdk-0.1.0/src/zalobot/py.typed +0 -0
  30. zalobot_sdk-0.1.0/src/zalobot/testing.py +398 -0
  31. zalobot_sdk-0.1.0/src/zalobot/text.py +209 -0
  32. zalobot_sdk-0.1.0/src/zalobot/types.py +234 -0
  33. zalobot_sdk-0.1.0/src/zalobot/webhook.py +110 -0
  34. zalobot_sdk-0.1.0/tests/__init__.py +0 -0
  35. zalobot_sdk-0.1.0/tests/conftest.py +48 -0
  36. zalobot_sdk-0.1.0/tests/test_client.py +427 -0
  37. zalobot_sdk-0.1.0/tests/test_dispatcher.py +440 -0
  38. zalobot_sdk-0.1.0/tests/test_examples.py +122 -0
  39. zalobot_sdk-0.1.0/tests/test_live.py +182 -0
  40. zalobot_sdk-0.1.0/tests/test_regressions.py +426 -0
  41. zalobot_sdk-0.1.0/tests/test_smoke.py +26 -0
  42. zalobot_sdk-0.1.0/tests/test_testing.py +113 -0
  43. zalobot_sdk-0.1.0/tests/test_text.py +138 -0
  44. zalobot_sdk-0.1.0/tests/test_webhook.py +94 -0
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes are listed here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
+ and versions follow [Semantic Versioning](https://semver.org/). Before 1.0 a minor version may change the API.
5
+
6
+ ## [Unreleased]
7
+
8
+ ## [0.1.0] - 2026-11-08
9
+
10
+ ### Added
11
+
12
+ - `Bot` (blocking) and `AsyncBot` (asyncio) with every Zalo Bot API method: `get_me`, `get_updates`,
13
+ `set_webhook`, `test_webhook`, `delete_webhook`, `get_webhook_info`, `send_message`, `send_photo`,
14
+ `send_sticker`, `send_voice` and `send_chat_action`, plus `send_long_message`.
15
+ - A typed dataclass for every answer, keeping the raw JSON in `.raw`.
16
+ - An exception for each documented error code, plus `NetworkError`. The bot token is removed from errors,
17
+ `repr()` and httpx log lines.
18
+ - Checks against the documented limits before sending, and an opt-in `RetryPolicy` for quota and network errors.
19
+ - `Text`, a builder for `text_styles` with UTF-16 offsets, and `split_text`.
20
+ - `Dispatcher` and `Router` with command and message handlers, filters `F` (combinable with `&`, `|`, `~`),
21
+ fallback and error hooks, and duplicate-message protection.
22
+ - Long polling (`run_polling`, `start_polling`, `run`) with backoff, a clean stop, and a refusal to start while a
23
+ webhook is set.
24
+ - Webhook helpers that work with any framework: `verify_secret`, `parse_update`, `WebhookHandler` and
25
+ `AsyncWebhookHandler`.
26
+ - `zalobot.testing.FakeZalo`, an in-memory Bot API for tests. It answers like the real API where the real API
27
+ differs from the docs.
28
+ - `InvalidChat` (410) and `BotInfo.display_name`, both seen from the real API but not in the docs.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tâm Ngô
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.4
2
+ Name: zalobot-sdk
3
+ Version: 0.1.0
4
+ Summary: Typed Python client and bot framework for the Zalo Bot Platform (sync and async).
5
+ Keywords: zalo,zalobot,bot,chatbot,vietnam,asyncio,webhook
6
+ Author: Tâm Ngô
7
+ Author-email: Tâm Ngô <ngothtam.me@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Natural Language :: English
14
+ Classifier: Natural Language :: Vietnamese
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Communications :: Chat
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Dist: httpx>=0.27,<1
26
+ Requires-Python: >=3.11
27
+ Project-URL: Homepage, https://github.com/nttams/zalobot
28
+ Project-URL: Source, https://github.com/nttams/zalobot
29
+ Project-URL: Issues, https://github.com/nttams/zalobot/issues
30
+ Project-URL: Changelog, https://github.com/nttams/zalobot/blob/main/CHANGELOG.md
31
+ Project-URL: Zalo Bot API, https://bot.zaloplatforms.com/docs/
32
+ Description-Content-Type: text/markdown
33
+
34
+ # zalobot
35
+
36
+ A typed Python library for the [Zalo Bot Platform](https://bot.zaloplatforms.com/docs/) with blocking and asyncio
37
+ clients, handlers, long polling and webhooks.
38
+
39
+ - **The whole Bot API**: all 11 methods, including `sendVoice`, `testWebhook` and the webhook check `setWebhook`
40
+ now returns.
41
+ - **Typed**: every answer is a dataclass, and the package ships `py.typed` (it passes `mypy --strict`).
42
+ - **Blocking or asyncio**: `Bot` and `AsyncBot` have the same methods.
43
+ - **Rich text that lines up**: `Text` builds Zalo's `text_styles`. Their offsets are counted in UTF-16 units, so
44
+ Vietnamese accents and emoji land in the right place.
45
+ - **Handlers like you know them**: `@dp.command("start")` and `@dp.message(F.photo)`, plus filters, an error hook
46
+ and duplicate-message protection.
47
+ - **Safe by default**: the bot token never shows up in errors, `repr()` or httpx's request logs. Webhook secrets are
48
+ compared in constant time.
49
+ - **Testable offline**: `zalobot.testing.FakeZalo` is an in-memory Bot API you can point a bot at in your own
50
+ tests.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ uv add zalobot-sdk # or: pip install zalobot-sdk
56
+ ```
57
+
58
+ The package is `zalobot-sdk` on PyPI, and you import it as `zalobot`. Python 3.11 or newer. The only dependency is [httpx](https://www.python-httpx.org/).
59
+
60
+ ## Quickstart
61
+
62
+ Create a bot in the Zalo app: open the official account **Zalo Bot Manager**, tap **Tạo bot**, and give it a name
63
+ starting with "Bot". Zalo then sends you the token in a message.
64
+
65
+ ```python
66
+ import os
67
+
68
+ from zalobot import Bot, Dispatcher, F
69
+
70
+ dp = Dispatcher()
71
+
72
+
73
+ @dp.message(F.text)
74
+ def echo(ctx):
75
+ ctx.reply_text(ctx.text)
76
+
77
+
78
+ dp.run_polling(Bot(os.environ["ZALO_BOT_TOKEN"]))
79
+ ```
80
+
81
+ Message your bot in Zalo, and it answers with the same text.
82
+
83
+ ## Commands, filters, replies
84
+
85
+ ```python
86
+ from zalobot import Context, Dispatcher, F
87
+
88
+ dp = Dispatcher()
89
+
90
+ @dp.command("add") # "/add 50k coffee" (any case)
91
+ def add(ctx: Context) -> None:
92
+ ctx.reply_text(f"Added: {ctx.args}") # "50k coffee"
93
+
94
+ @dp.message(F.photo & F.private)
95
+ def photo(ctx: Context) -> None:
96
+ ctx.reply_text(f"Got your photo: {ctx.message.photo_url}")
97
+
98
+ @dp.fallback
99
+ def other(ctx: Context) -> None:
100
+ ctx.reply_sticker("bfe458bf64fa8da4d4eb")
101
+
102
+ @dp.error
103
+ def on_error(ctx: Context, exc: Exception) -> None:
104
+ ctx.reply_text("Sorry, something went wrong.")
105
+ ```
106
+
107
+ Filters: `F.text`, `F.photo`, `F.sticker`, `F.voice`, `F.unsupported`, `F.private`, `F.group`,
108
+ `F.command(...)`, `F.regex(...)`, `F.from_user(...)` and `F.chat(...)`. Combine them with `&`, `|` and `~`. The first
109
+ matching handler wins. A `Router` groups handlers, and `dp.include(router)` adds the group to the dispatcher.
110
+
111
+ Replies: `ctx.reply_text`, `ctx.reply_long` (splits text over 2,000 characters at line breaks), `ctx.reply_photo`,
112
+ `ctx.reply_sticker`, `ctx.reply_voice` and `ctx.typing()`.
113
+
114
+ ## asyncio
115
+
116
+ ```python
117
+ from zalobot import AsyncBot, AsyncContext, Dispatcher, F
118
+
119
+ dp = Dispatcher()
120
+
121
+ @dp.message(F.text)
122
+ async def echo(ctx: AsyncContext) -> None:
123
+ await ctx.typing()
124
+ await ctx.reply_text(ctx.text)
125
+
126
+ dp.run(AsyncBot(token)) # or: await dp.start_polling(bot)
127
+ ```
128
+
129
+ With an `AsyncBot`, plain `def` handlers still work: they run in a worker thread, and their replies go through
130
+ the same bot.
131
+
132
+ ## Polling or webhook
133
+
134
+ Zalo delivers messages in one of two ways, and you can't use both at once:
135
+
136
+ | | Long polling | Webhook |
137
+ |---|---|---|
138
+ | How | `dp.run_polling(bot)` | Zalo POSTs to your HTTPS URL |
139
+ | Good for | development, small bots, no public server | production (Zalo's advice) |
140
+ | Setup | nothing | `bot.set_webhook(url, secret)` once |
141
+
142
+ While a webhook is set, Zalo turns `getUpdates` off. `run_polling` checks for this and tells you to call
143
+ `bot.delete_webhook()`.
144
+
145
+ Webhook with FastAPI (see `examples/webhook_flask.py` for Flask):
146
+
147
+ ```python
148
+ from fastapi import FastAPI, Request, Response
149
+ from zalobot import AsyncBot, AsyncWebhookHandler
150
+
151
+ handler = AsyncWebhookHandler(dp, AsyncBot(token), secret="a-long-random-secret")
152
+ app = FastAPI()
153
+
154
+ @app.post("/zalo/webhook")
155
+ async def webhook(request: Request) -> Response:
156
+ status, body = await handler.handle(request.headers, await request.body())
157
+ return Response(body, status_code=status, media_type="application/json")
158
+ ```
159
+
160
+ The handler checks the `X-Bot-Api-Secret-Token` header (403 if it's wrong) and the body (400 if it isn't an
161
+ update), then runs your handlers. `bot.set_webhook()` and `bot.test_webhook()` return Zalo's check of your URL, such
162
+ as `outcome="webhook.http.403"` with a hint about what to fix.
163
+
164
+ ## Rich text
165
+
166
+ ```python
167
+ from zalobot import Color, Size, Text
168
+
169
+ msg = (Text().size(Size.LARGE, "Đơn hàng #1024").plain("\n")
170
+ .bullets(["Cà phê sữa đá: 29,000đ", "Bánh mì: 25,000đ"])
171
+ .bold("Tổng: ").plain("54,000đ ").color(Color.GREEN, "✅ đã thanh toán"))
172
+ bot.send_message(chat_id, msg)
173
+ ```
174
+
175
+ You can also let Zalo parse the markup: `bot.send_message(chat_id, "**bold** {red}red{/red}", parse_mode="markdown")`
176
+ or `parse_mode="html"`.
177
+
178
+ ## Errors and retries
179
+
180
+ Each documented error code has its own exception, all subclasses of `zalobot.ZaloError`:
181
+
182
+ | Code | Exception |
183
+ |---|---|
184
+ | 400 | `BadRequest` |
185
+ | 401 | `Unauthorized` (token expired or wrong) |
186
+ | 403 | `Forbidden` |
187
+ | 404 | `NotFound` |
188
+ | 408 | `TimedOut` |
189
+ | 410 | `InvalidChat` (no such chat for this bot; not in Zalo's table) |
190
+ | 429 | `QuotaExceeded` |
191
+ | network failure | `NetworkError` |
192
+
193
+ The real API answers errors with HTTP 200 and the code in the body; zalobot reads the body either way.
194
+
195
+ Retries are off by default. Turn them on with `Bot(token, retry=RetryPolicy(attempts=3))`: quota errors and
196
+ network errors are retried with doubling waits, while 401 never is. A send (`send_message`, `send_photo` ...) is
197
+ retried after a network error only if the request never left (connection refused or timed out): after a read
198
+ timeout Zalo may already have delivered it, and a retry would send it twice. Inputs that break the documented limits raise
199
+ `ValueError` before any request is sent: text over 2,000 characters (counted like Zalo's server, where most emoji
200
+ take 2), a secret shorter than 8, or a voice file that isn't `.aac`.
201
+
202
+ ## Testing your bot
203
+
204
+ ```python
205
+ from zalobot.testing import FakeZalo
206
+
207
+ def test_echo():
208
+ fake = FakeZalo()
209
+ fake.push_text("hello") # a user writes to the bot
210
+ with fake.bot() as bot:
211
+ for update in bot.get_updates():
212
+ dp.feed(bot, update)
213
+ assert fake.sent[0].text == "hello" # what the bot sent back
214
+ ```
215
+
216
+ `FakeZalo` answers with the shapes from the official docs. It can fail on demand (`fake.fail(429)`), and it can
217
+ deliver queued updates to a webhook handler (`fake.deliver_webhook(handler)`).
218
+
219
+ ## Links
220
+
221
+ - Source and issues: https://github.com/nttams/zalobot
222
+ - Zalo Bot Platform docs: https://bot.zaloplatforms.com/docs/
223
+ - Changelog: [CHANGELOG.md](https://github.com/nttams/zalobot/blob/main/CHANGELOG.md)
224
+ - Contributing: [CONTRIBUTING.md](https://github.com/nttams/zalobot/blob/main/CONTRIBUTING.md)
225
+
226
+ zalobot is an independent project, not made or endorsed by Zalo or VNG. MIT licensed.
@@ -0,0 +1,193 @@
1
+ # zalobot
2
+
3
+ A typed Python library for the [Zalo Bot Platform](https://bot.zaloplatforms.com/docs/) with blocking and asyncio
4
+ clients, handlers, long polling and webhooks.
5
+
6
+ - **The whole Bot API**: all 11 methods, including `sendVoice`, `testWebhook` and the webhook check `setWebhook`
7
+ now returns.
8
+ - **Typed**: every answer is a dataclass, and the package ships `py.typed` (it passes `mypy --strict`).
9
+ - **Blocking or asyncio**: `Bot` and `AsyncBot` have the same methods.
10
+ - **Rich text that lines up**: `Text` builds Zalo's `text_styles`. Their offsets are counted in UTF-16 units, so
11
+ Vietnamese accents and emoji land in the right place.
12
+ - **Handlers like you know them**: `@dp.command("start")` and `@dp.message(F.photo)`, plus filters, an error hook
13
+ and duplicate-message protection.
14
+ - **Safe by default**: the bot token never shows up in errors, `repr()` or httpx's request logs. Webhook secrets are
15
+ compared in constant time.
16
+ - **Testable offline**: `zalobot.testing.FakeZalo` is an in-memory Bot API you can point a bot at in your own
17
+ tests.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ uv add zalobot-sdk # or: pip install zalobot-sdk
23
+ ```
24
+
25
+ The package is `zalobot-sdk` on PyPI, and you import it as `zalobot`. Python 3.11 or newer. The only dependency is [httpx](https://www.python-httpx.org/).
26
+
27
+ ## Quickstart
28
+
29
+ Create a bot in the Zalo app: open the official account **Zalo Bot Manager**, tap **Tạo bot**, and give it a name
30
+ starting with "Bot". Zalo then sends you the token in a message.
31
+
32
+ ```python
33
+ import os
34
+
35
+ from zalobot import Bot, Dispatcher, F
36
+
37
+ dp = Dispatcher()
38
+
39
+
40
+ @dp.message(F.text)
41
+ def echo(ctx):
42
+ ctx.reply_text(ctx.text)
43
+
44
+
45
+ dp.run_polling(Bot(os.environ["ZALO_BOT_TOKEN"]))
46
+ ```
47
+
48
+ Message your bot in Zalo, and it answers with the same text.
49
+
50
+ ## Commands, filters, replies
51
+
52
+ ```python
53
+ from zalobot import Context, Dispatcher, F
54
+
55
+ dp = Dispatcher()
56
+
57
+ @dp.command("add") # "/add 50k coffee" (any case)
58
+ def add(ctx: Context) -> None:
59
+ ctx.reply_text(f"Added: {ctx.args}") # "50k coffee"
60
+
61
+ @dp.message(F.photo & F.private)
62
+ def photo(ctx: Context) -> None:
63
+ ctx.reply_text(f"Got your photo: {ctx.message.photo_url}")
64
+
65
+ @dp.fallback
66
+ def other(ctx: Context) -> None:
67
+ ctx.reply_sticker("bfe458bf64fa8da4d4eb")
68
+
69
+ @dp.error
70
+ def on_error(ctx: Context, exc: Exception) -> None:
71
+ ctx.reply_text("Sorry, something went wrong.")
72
+ ```
73
+
74
+ Filters: `F.text`, `F.photo`, `F.sticker`, `F.voice`, `F.unsupported`, `F.private`, `F.group`,
75
+ `F.command(...)`, `F.regex(...)`, `F.from_user(...)` and `F.chat(...)`. Combine them with `&`, `|` and `~`. The first
76
+ matching handler wins. A `Router` groups handlers, and `dp.include(router)` adds the group to the dispatcher.
77
+
78
+ Replies: `ctx.reply_text`, `ctx.reply_long` (splits text over 2,000 characters at line breaks), `ctx.reply_photo`,
79
+ `ctx.reply_sticker`, `ctx.reply_voice` and `ctx.typing()`.
80
+
81
+ ## asyncio
82
+
83
+ ```python
84
+ from zalobot import AsyncBot, AsyncContext, Dispatcher, F
85
+
86
+ dp = Dispatcher()
87
+
88
+ @dp.message(F.text)
89
+ async def echo(ctx: AsyncContext) -> None:
90
+ await ctx.typing()
91
+ await ctx.reply_text(ctx.text)
92
+
93
+ dp.run(AsyncBot(token)) # or: await dp.start_polling(bot)
94
+ ```
95
+
96
+ With an `AsyncBot`, plain `def` handlers still work: they run in a worker thread, and their replies go through
97
+ the same bot.
98
+
99
+ ## Polling or webhook
100
+
101
+ Zalo delivers messages in one of two ways, and you can't use both at once:
102
+
103
+ | | Long polling | Webhook |
104
+ |---|---|---|
105
+ | How | `dp.run_polling(bot)` | Zalo POSTs to your HTTPS URL |
106
+ | Good for | development, small bots, no public server | production (Zalo's advice) |
107
+ | Setup | nothing | `bot.set_webhook(url, secret)` once |
108
+
109
+ While a webhook is set, Zalo turns `getUpdates` off. `run_polling` checks for this and tells you to call
110
+ `bot.delete_webhook()`.
111
+
112
+ Webhook with FastAPI (see `examples/webhook_flask.py` for Flask):
113
+
114
+ ```python
115
+ from fastapi import FastAPI, Request, Response
116
+ from zalobot import AsyncBot, AsyncWebhookHandler
117
+
118
+ handler = AsyncWebhookHandler(dp, AsyncBot(token), secret="a-long-random-secret")
119
+ app = FastAPI()
120
+
121
+ @app.post("/zalo/webhook")
122
+ async def webhook(request: Request) -> Response:
123
+ status, body = await handler.handle(request.headers, await request.body())
124
+ return Response(body, status_code=status, media_type="application/json")
125
+ ```
126
+
127
+ The handler checks the `X-Bot-Api-Secret-Token` header (403 if it's wrong) and the body (400 if it isn't an
128
+ update), then runs your handlers. `bot.set_webhook()` and `bot.test_webhook()` return Zalo's check of your URL, such
129
+ as `outcome="webhook.http.403"` with a hint about what to fix.
130
+
131
+ ## Rich text
132
+
133
+ ```python
134
+ from zalobot import Color, Size, Text
135
+
136
+ msg = (Text().size(Size.LARGE, "Đơn hàng #1024").plain("\n")
137
+ .bullets(["Cà phê sữa đá: 29,000đ", "Bánh mì: 25,000đ"])
138
+ .bold("Tổng: ").plain("54,000đ ").color(Color.GREEN, "✅ đã thanh toán"))
139
+ bot.send_message(chat_id, msg)
140
+ ```
141
+
142
+ You can also let Zalo parse the markup: `bot.send_message(chat_id, "**bold** {red}red{/red}", parse_mode="markdown")`
143
+ or `parse_mode="html"`.
144
+
145
+ ## Errors and retries
146
+
147
+ Each documented error code has its own exception, all subclasses of `zalobot.ZaloError`:
148
+
149
+ | Code | Exception |
150
+ |---|---|
151
+ | 400 | `BadRequest` |
152
+ | 401 | `Unauthorized` (token expired or wrong) |
153
+ | 403 | `Forbidden` |
154
+ | 404 | `NotFound` |
155
+ | 408 | `TimedOut` |
156
+ | 410 | `InvalidChat` (no such chat for this bot; not in Zalo's table) |
157
+ | 429 | `QuotaExceeded` |
158
+ | network failure | `NetworkError` |
159
+
160
+ The real API answers errors with HTTP 200 and the code in the body; zalobot reads the body either way.
161
+
162
+ Retries are off by default. Turn them on with `Bot(token, retry=RetryPolicy(attempts=3))`: quota errors and
163
+ network errors are retried with doubling waits, while 401 never is. A send (`send_message`, `send_photo` ...) is
164
+ retried after a network error only if the request never left (connection refused or timed out): after a read
165
+ timeout Zalo may already have delivered it, and a retry would send it twice. Inputs that break the documented limits raise
166
+ `ValueError` before any request is sent: text over 2,000 characters (counted like Zalo's server, where most emoji
167
+ take 2), a secret shorter than 8, or a voice file that isn't `.aac`.
168
+
169
+ ## Testing your bot
170
+
171
+ ```python
172
+ from zalobot.testing import FakeZalo
173
+
174
+ def test_echo():
175
+ fake = FakeZalo()
176
+ fake.push_text("hello") # a user writes to the bot
177
+ with fake.bot() as bot:
178
+ for update in bot.get_updates():
179
+ dp.feed(bot, update)
180
+ assert fake.sent[0].text == "hello" # what the bot sent back
181
+ ```
182
+
183
+ `FakeZalo` answers with the shapes from the official docs. It can fail on demand (`fake.fail(429)`), and it can
184
+ deliver queued updates to a webhook handler (`fake.deliver_webhook(handler)`).
185
+
186
+ ## Links
187
+
188
+ - Source and issues: https://github.com/nttams/zalobot
189
+ - Zalo Bot Platform docs: https://bot.zaloplatforms.com/docs/
190
+ - Changelog: [CHANGELOG.md](https://github.com/nttams/zalobot/blob/main/CHANGELOG.md)
191
+ - Contributing: [CONTRIBUTING.md](https://github.com/nttams/zalobot/blob/main/CONTRIBUTING.md)
192
+
193
+ zalobot is an independent project, not made or endorsed by Zalo or VNG. MIT licensed.
@@ -0,0 +1,35 @@
1
+ # API reference
2
+
3
+ ## Clients
4
+
5
+ ::: zalobot.client.Bot
6
+
7
+ ::: zalobot.client.AsyncBot
8
+
9
+ ::: zalobot._core.RetryPolicy
10
+
11
+ ## Types
12
+
13
+ ::: zalobot.types
14
+
15
+ ## Handlers and polling
16
+
17
+ ::: zalobot.dispatcher
18
+
19
+ ::: zalobot.filters
20
+
21
+ ## Webhooks
22
+
23
+ ::: zalobot.webhook
24
+
25
+ ## Rich text
26
+
27
+ ::: zalobot.text
28
+
29
+ ## Errors
30
+
31
+ ::: zalobot.errors
32
+
33
+ ## Testing
34
+
35
+ ::: zalobot.testing
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+
3
+ --8<-- "CHANGELOG.md:3"
@@ -0,0 +1,48 @@
1
+ # Errors and retries
2
+
3
+ Every failed call raises a subclass of `zalobot.ZaloError`, with `.code` (Zalo's `error_code`), `.description`
4
+ and `.method`:
5
+
6
+ | Code | Exception | Usually |
7
+ |---|---|---|
8
+ | 400 | `BadRequest` | invalid fields, wrong method |
9
+ | 401 | `Unauthorized` | the token expired or is wrong (retrying won't help) |
10
+ | 403 | `Forbidden` | Zalo's table says "internal server error" |
11
+ | 404 | `NotFound` | unknown method (Zalo's table says 400; the real API answers 404) |
12
+ | 408 | `TimedOut` | the request took too long (`get_updates` turns it into `[]`) |
13
+ | 410 | `InvalidChat` | no chat with that id for this bot (the real API's code; not in Zalo's table) |
14
+ | 429 | `QuotaExceeded` | over the API quota: slow down |
15
+ | none | `NetworkError` | DNS, TLS, connection or timeout before Zalo answered |
16
+
17
+ ## Checks before sending
18
+
19
+ These raise `ValueError` without making a request:
20
+
21
+ - `text` and `caption` must be 1 to 2,000 characters, counted in UTF-16 units as Zalo's Java server counts them
22
+ (most emoji take 2).
23
+ - `secret_token` must be 8 to 256 characters.
24
+ - The webhook URL must use `https://`.
25
+ - `photo` and `voice_url` must be `http(s)` URLs, and `voice_url` must end in `.aac`.
26
+
27
+ ## Retries
28
+
29
+ Retries are off by default. Turn them on per bot:
30
+
31
+ ```python
32
+ from zalobot import Bot, RetryPolicy
33
+
34
+ bot = Bot(token, retry=RetryPolicy(attempts=4, base_delay=1, max_delay=30))
35
+ ```
36
+
37
+ `QuotaExceeded` and `NetworkError` are retried, waiting 1 s, then 2 s, then 4 s. Other errors are raised at once.
38
+ Set `retry_network=False` to retry quota errors only.
39
+
40
+ Sends are the exception: after a network error, `send_message`, `send_photo`, `send_sticker` and `send_voice` are
41
+ retried only when the request never left (`NetworkError.request_sent` is False: connection refused, connect or pool
42
+ timeout). After a read timeout or a dropped connection, Zalo may already have delivered the message, and a retry
43
+ would send it twice. Reads (`get_me`, `get_webhook_info` ...) are always safe to retry.
44
+
45
+ ## The token stays secret
46
+
47
+ The token is part of every request URL. zalobot removes it from error messages and `repr(bot)`, and filters it out
48
+ of httpx's request log lines (which log URLs at INFO level). `bot.token` still gives it to you when you need it.
@@ -0,0 +1,64 @@
1
+ # Getting started
2
+
3
+ ## 1. Make a bot
4
+
5
+ In the Zalo app, open the official account **Zalo Bot Manager** and tap **Tạo bot**. Name the bot (the name must
6
+ start with "Bot") and confirm. Zalo then sends you the bot **token** in a message. Keep it secret: anyone with the
7
+ token controls the bot.
8
+
9
+ ## 2. Install
10
+
11
+ ```bash
12
+ uv add zalobot-sdk # or: pip install zalobot-sdk
13
+ ```
14
+
15
+ The package is `zalobot-sdk` on PyPI; in code it is `import zalobot`.
16
+
17
+ ## 3. Check the token
18
+
19
+ ```python
20
+ from zalobot import Bot
21
+
22
+ with Bot("YOUR_TOKEN") as bot:
23
+ me = bot.get_me()
24
+ print(me.id, me.account_name)
25
+ ```
26
+
27
+ A wrong or expired token raises `zalobot.Unauthorized`.
28
+
29
+ ## 4. Answer messages
30
+
31
+ ```python
32
+ from zalobot import Bot, Context, Dispatcher, F
33
+
34
+ dp = Dispatcher()
35
+
36
+ @dp.command("start")
37
+ def start(ctx: Context) -> None:
38
+ ctx.reply_text(f"Xin chào {ctx.message.from_user.display_name}!")
39
+
40
+ @dp.message(F.text)
41
+ def echo(ctx: Context) -> None:
42
+ ctx.reply_text(ctx.text or "")
43
+
44
+ dp.run_polling(Bot("YOUR_TOKEN"))
45
+ ```
46
+
47
+ Message your bot in Zalo. A bot can only answer people who wrote to it: there is no API for starting a
48
+ conversation.
49
+
50
+ ## 5. Or with asyncio
51
+
52
+ ```python
53
+ from zalobot import AsyncBot, AsyncContext, Dispatcher, F
54
+
55
+ dp = Dispatcher()
56
+
57
+ @dp.message(F.text)
58
+ async def echo(ctx: AsyncContext) -> None:
59
+ await ctx.reply_text(ctx.text or "")
60
+
61
+ dp.run(AsyncBot("YOUR_TOKEN"))
62
+ ```
63
+
64
+ Next: [handlers and filters](handlers.md), then [polling or webhooks](polling-webhooks.md).