blackbirdtalk 1.0.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 (38) hide show
  1. blackbirdtalk-1.0.0/.gitignore +89 -0
  2. blackbirdtalk-1.0.0/LICENSE +21 -0
  3. blackbirdtalk-1.0.0/PKG-INFO +309 -0
  4. blackbirdtalk-1.0.0/README.md +277 -0
  5. blackbirdtalk-1.0.0/examples/echo.py +25 -0
  6. blackbirdtalk-1.0.0/examples/files.py +31 -0
  7. blackbirdtalk-1.0.0/examples/form.py +37 -0
  8. blackbirdtalk-1.0.0/examples/group_guard.py +46 -0
  9. blackbirdtalk-1.0.0/examples/menu.py +64 -0
  10. blackbirdtalk-1.0.0/examples/private_bot.py +55 -0
  11. blackbirdtalk-1.0.0/examples/sync_echo.py +28 -0
  12. blackbirdtalk-1.0.0/examples/webhook_asgi.py +37 -0
  13. blackbirdtalk-1.0.0/pyproject.toml +72 -0
  14. blackbirdtalk-1.0.0/src/blackbirdtalk/__init__.py +118 -0
  15. blackbirdtalk-1.0.0/src/blackbirdtalk/__main__.py +3 -0
  16. blackbirdtalk-1.0.0/src/blackbirdtalk/_core.py +175 -0
  17. blackbirdtalk-1.0.0/src/blackbirdtalk/_gen/__init__.py +1 -0
  18. blackbirdtalk-1.0.0/src/blackbirdtalk/_gen/constants.py +84 -0
  19. blackbirdtalk-1.0.0/src/blackbirdtalk/_gen/methods.py +783 -0
  20. blackbirdtalk-1.0.0/src/blackbirdtalk/_gen/types.py +541 -0
  21. blackbirdtalk-1.0.0/src/blackbirdtalk/_helpers.py +173 -0
  22. blackbirdtalk-1.0.0/src/blackbirdtalk/_object.py +76 -0
  23. blackbirdtalk-1.0.0/src/blackbirdtalk/_router.py +238 -0
  24. blackbirdtalk-1.0.0/src/blackbirdtalk/_types_in.py +64 -0
  25. blackbirdtalk-1.0.0/src/blackbirdtalk/_update_check.py +67 -0
  26. blackbirdtalk-1.0.0/src/blackbirdtalk/bot.py +446 -0
  27. blackbirdtalk-1.0.0/src/blackbirdtalk/cli.py +155 -0
  28. blackbirdtalk-1.0.0/src/blackbirdtalk/errors.py +77 -0
  29. blackbirdtalk-1.0.0/src/blackbirdtalk/markup.py +249 -0
  30. blackbirdtalk-1.0.0/src/blackbirdtalk/py.typed +0 -0
  31. blackbirdtalk-1.0.0/src/blackbirdtalk/sync.py +403 -0
  32. blackbirdtalk-1.0.0/src/blackbirdtalk/testing.py +168 -0
  33. blackbirdtalk-1.0.0/tests/live/test_live_server.py +185 -0
  34. blackbirdtalk-1.0.0/tests/test_core.py +192 -0
  35. blackbirdtalk-1.0.0/tests/test_dispatch.py +211 -0
  36. blackbirdtalk-1.0.0/tests/test_examples.py +142 -0
  37. blackbirdtalk-1.0.0/tests/test_markup.py +90 -0
  38. blackbirdtalk-1.0.0/tests/test_webhook.py +69 -0
@@ -0,0 +1,89 @@
1
+ # ============================================================
2
+ # BLACKBIRD .gitignore — hardened for the handover push.
3
+ # Rule of thumb: if it's a secret, a build artifact, a binary
4
+ # blob or a per-machine config, it stays out.
5
+ # ============================================================
6
+
7
+ # ---------- SECRETS ----------
8
+ # Never commit these. HANDOVER_SECRETS.md lists what must travel
9
+ # through an encrypted channel instead (Signal / Keybase / pw mgr).
10
+ .env
11
+ .env.*
12
+ !.env.example
13
+ *.key
14
+ *.pem
15
+ *.p12
16
+ *.der
17
+ *.jks
18
+ *.keystore
19
+ *.asc
20
+ *.gpg
21
+ HANDOVER_SECRETS.md
22
+
23
+ # Firebase service accounts and Google client configs — identify a
24
+ # live project; leaking them lets anyone push notifications as us.
25
+ google-services.json
26
+ android/app/google-services.json
27
+ *service-account*.json
28
+ firebase-adminsdk*.json
29
+
30
+ # ---------- BACKUPS (local-only snapshots) ----------
31
+ backups/
32
+
33
+ # ---------- ANDROID ----------
34
+ # Signing + per-machine config.
35
+ android/keystore.jks
36
+ android/keystore.properties
37
+ android/local.properties
38
+ # Build output.
39
+ android/.gradle/
40
+ android/build/
41
+ android/app/build/
42
+ android/app/release/
43
+ android/captures/
44
+ # IDE.
45
+ android/.idea/
46
+ # Shipped artifacts.
47
+ *.apk
48
+ *.aab
49
+
50
+ # ---------- BACKEND ----------
51
+ # Compiled Go binaries + vendored deps + runtime scratch.
52
+ backend/dexchatd
53
+ backend/dexchatd-linux
54
+ backend/vendor/
55
+ backend/tmp/
56
+ backend/*.db
57
+ backend/*.db-journal
58
+ backend/*.sqlite
59
+ backend/*.sqlite-journal
60
+ backend/logs/
61
+ backend/uploads/
62
+
63
+ # ---------- PYTHON ----------
64
+ __pycache__/
65
+ *.pyc
66
+ *.pyo
67
+ scripts/.venv/
68
+ *.egg-info/
69
+ .pytest_cache/
70
+
71
+ # ---------- NODE / DISCOVERY WORKER ----------
72
+ discovery/node_modules/
73
+ discovery/.wrangler/
74
+ node_modules/
75
+
76
+ # ---------- IDE / OS ----------
77
+ .vscode/
78
+ .idea/
79
+ *.swp
80
+ *.swo
81
+ .DS_Store
82
+ Thumbs.db
83
+
84
+ # ---------- ARCHIVES (don't commit distribution bundles) ----------
85
+ *.tar.gz
86
+ *.tgz
87
+ *.zip
88
+ *.7z
89
+ *.rar
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DEX (Blackbird)
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,309 @@
1
+ Metadata-Version: 2.5
2
+ Name: blackbirdtalk
3
+ Version: 1.0.0
4
+ Summary: The official library for Blackbird bots: every Bot API method, buttons, polling, webhooks.
5
+ Project-URL: Homepage, https://blackbirdtalk.com/bots
6
+ Project-URL: Documentation, https://blackbirdtalk.com/bots
7
+ Author-email: Blackbird <buildsevaa@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: blackbird,bot,bot-api,chatbot,messenger
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Communications :: Chat
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: httpx<1,>=0.27
25
+ Provides-Extra: dev
26
+ Requires-Dist: mypy>=1.11; extra == 'dev'
27
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
28
+ Requires-Dist: pytest>=8; extra == 'dev'
29
+ Requires-Dist: ruff>=0.6; extra == 'dev'
30
+ Requires-Dist: uvicorn>=0.30; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # blackbirdtalk
34
+
35
+ The official Python library for [Blackbird](https://blackbirdtalk.com) bots.
36
+
37
+ ```bash
38
+ pip install blackbirdtalk
39
+ ```
40
+
41
+ Python 3.11 or newer. One dependency (httpx).
42
+
43
+ ```python
44
+ from blackbirdtalk import Bot, Button
45
+
46
+ bot = Bot() # reads BLACKBIRD_TOKEN; or Bot("bb_...")
47
+
48
+ @bot.command("start")
49
+ async def start(msg):
50
+ await msg.answer("Welcome!", buttons=[[Button("Pay ₹49", callback="pay_49", style="main", icon="card")]])
51
+
52
+ @bot.button("pay_49")
53
+ async def pay(tap):
54
+ await tap.answer("Payment started")
55
+
56
+ bot.run()
57
+ ```
58
+
59
+ Get the token in the Blackbird app: **Settings › Bot Studio › your bot › Overview**.
60
+ Set it in your terminal so it stays out of your code:
61
+
62
+ | | |
63
+ |---|---|
64
+ | macOS / Linux | `export BLACKBIRD_TOKEN=bb_...` |
65
+ | Windows PowerShell | `$env:BLACKBIRD_TOKEN="bb_..."` |
66
+ | Windows cmd | `set BLACKBIRD_TOKEN=bb_...` |
67
+
68
+ Or start from a ready project: `blackbirdtalk new mybot`. Check a token, its
69
+ webhook and commands any time with `blackbirdtalk check`.
70
+
71
+ ## Contents
72
+
73
+ - [Handlers](#handlers)
74
+ - [Buttons](#buttons)
75
+ - [Questions with ask()](#questions-with-ask)
76
+ - [Files](#files)
77
+ - [Groups](#groups)
78
+ - [Private bots](#private-bots)
79
+ - [Webhooks](#webhooks)
80
+ - [No async: SyncBot](#no-async-syncbot)
81
+ - [Testing your bot](#testing-your-bot)
82
+ - [Errors and retries](#errors-and-retries)
83
+ - [Every method](#every-method)
84
+ - [Updates and versions](#updates-and-versions)
85
+
86
+ ## Handlers
87
+
88
+ The first handler that matches an update runs, in the order you wrote them.
89
+ Messages from one chat are handled one at a time, in order; different chats
90
+ run in parallel.
91
+
92
+ ```python
93
+ @bot.command("quiz", "q") # /quiz physics -> msg.args == "physics"
94
+ async def quiz(msg): ...
95
+
96
+ @bot.message(regex=r"(\d+) apples") # msg.match[1] == "3"
97
+ async def apples(msg): ...
98
+
99
+ @bot.message(text=["hi", "hello"])
100
+ async def hi(msg): ...
101
+
102
+ @bot.message(kind="image") # image, video, voice, doc, text
103
+ async def photo(msg): ...
104
+
105
+ @bot.message(chat="group", when=lambda m: len(m.text or "") > 500)
106
+ async def long_text(msg): ...
107
+
108
+ @bot.message() # everything else
109
+ async def fallback(msg): ...
110
+
111
+ @bot.on_error
112
+ async def oops(error, update):
113
+ print("handler failed:", error)
114
+ ```
115
+
116
+ A message has `msg.answer()` (same chat), `msg.reply()` (quoting it),
117
+ `msg.answer_file()`, `msg.edit()`, `msg.delete()`, `msg.pin()`, `msg.typing()`,
118
+ `msg.ask()` and `msg.download()`.
119
+
120
+ ## Buttons
121
+
122
+ Blackbird buttons have styles, icons, colours, sizes, a confirm step and
123
+ copy-to-clipboard. The library checks every rule the server checks before it
124
+ sends, so a mistake fails on your machine with the button's row and position.
125
+
126
+ ```python
127
+ from blackbirdtalk import Button, Buttons
128
+
129
+ menu = Buttons(
130
+ [Button("Buy ₹49", callback="buy", style="success", icon="card", confirm="Pay ₹49?")],
131
+ [Button("Coupon", copy="SAVE20", icon="copy"), Button("Site", url="https://example.com", icon="link")],
132
+ [Button("Brand colour", callback="x", color="#5B3FD9", size="pill")],
133
+ )
134
+ print(menu.preview()) # see it in the terminal
135
+ await msg.answer("Pick one", buttons=menu)
136
+
137
+ @bot.button(prefix="buy") # also: exact data, a list, regex=, when=
138
+ async def buy(tap):
139
+ await tap.answer("Done!") # a short note (alert=True for a dialog)
140
+ await tap.edit("Paid ✓", buttons=[]) # change the message in place
141
+ ```
142
+
143
+ `style`: main, normal, success, warning, danger. `size`: pill, normal, full.
144
+ `icon`: one of 40 (`blackbirdtalk.BUTTON_ICONS`). Up to 8 rows, 8 per row.
145
+ Taps you don't answer are answered for you, so the spinner never hangs.
146
+
147
+ Keyboard buttons (under the text box) send their text; they can also ask for
148
+ the person's location or contact:
149
+
150
+ ```python
151
+ from blackbirdtalk import Keyboard, RemoveKeyboard
152
+
153
+ await msg.answer("When?", keyboard=Keyboard([["Today", "Tomorrow"],
154
+ [Button("Share location", request="location")]], one_time=True))
155
+ await msg.answer("Thanks", reply_markup=RemoveKeyboard())
156
+ ```
157
+
158
+ ## Questions with ask()
159
+
160
+ Write a conversation top to bottom. Each person's answers come back to their
161
+ own question, even with many people at once.
162
+
163
+ ```python
164
+ from blackbirdtalk import AskTimeout
165
+
166
+ @bot.command("register")
167
+ async def register(msg):
168
+ try:
169
+ name = await msg.ask("Your name?", placeholder="Full name")
170
+ tap = await msg.ask("Class?", buttons=[[Button("10", callback="10"), Button("12", callback="12")]])
171
+ except AskTimeout: # default 5 minutes; timeout=...
172
+ return await msg.answer("Stopped. Send /register again.")
173
+ await msg.answer(f"Saved {name.text}, class {tap.data}")
174
+ ```
175
+
176
+ A text answer comes back as a Message, a tap as a Callback.
177
+
178
+ ## Files
179
+
180
+ ```python
181
+ await msg.answer_file("report.pdf", caption="Your report") # a path
182
+ await msg.answer_file(InputFile(data, filename="chart.png")) # bytes made in memory
183
+ await bot.send_document(chat_id, file_id=old.file.file_id) # send again, no upload
184
+
185
+ @bot.message(kind="doc")
186
+ async def got(msg):
187
+ data = await msg.download() # bytes; or msg.download("saved.pdf")
188
+ ```
189
+
190
+ ## Groups
191
+
192
+ Add the bot to a group and give it the powers it needs (approve, remove,
193
+ delete, pin ...); `msg.chat.my_powers` lists what it has.
194
+
195
+ ```python
196
+ @bot.on_join_request()
197
+ async def check(req):
198
+ await (req.approve() if "blackbird" in (req.answer or "").lower() else req.decline())
199
+
200
+ @bot.on_member(lambda m: m.event == "joined")
201
+ async def welcome(change):
202
+ await change.chat.send("Welcome " + ", ".join(u.name for u in change.users))
203
+
204
+ @bot.on_my_membership(lambda m: m.status == "added")
205
+ async def added(change): ...
206
+ ```
207
+
208
+ ## Private bots
209
+
210
+ Choose who can use the bot (`everyone`, `owner`, or `chosen` people) from the
211
+ code or from Bot Studio. People can ask for access; you decide.
212
+
213
+ ```python
214
+ await bot.set_access(audience="chosen", message="For our class only", allow=[user_id])
215
+
216
+ @bot.on_access_request()
217
+ async def asked(req):
218
+ await req.approve() # or req.decline()
219
+ ```
220
+
221
+ ## Webhooks
222
+
223
+ Polling (`bot.run()`) needs nothing but the token. On a server with HTTPS you
224
+ can use a webhook instead:
225
+
226
+ ```python
227
+ app = bot.webhook_app(secret="long-random-text", path="/hook") # ASGI: uvicorn mybot:app
228
+ await bot.set_webhook("https://your.domain/hook", secret_token="long-random-text") # once
229
+ ```
230
+
231
+ FastAPI, aiohttp or anything else:
232
+
233
+ ```python
234
+ @api.post("/hook")
235
+ async def hook(request: Request):
236
+ status = await bot.handle_webhook(await request.body(), request.headers, secret=SECRET)
237
+ return Response(status_code=status)
238
+ ```
239
+
240
+ `SyncBot.wsgi_app()` does the same for gunicorn, Flask and Django. The secret
241
+ header is checked in constant time; requests without it get 401.
242
+
243
+ ## No async: SyncBot
244
+
245
+ Same handlers and helpers with plain functions:
246
+
247
+ ```python
248
+ from blackbirdtalk import SyncBot
249
+
250
+ bot = SyncBot()
251
+
252
+ @bot.command("start")
253
+ def start(msg):
254
+ msg.answer("Hi!")
255
+
256
+ bot.run()
257
+ ```
258
+
259
+ ## Testing your bot
260
+
261
+ `FakeBlackbird` stands in for the server: no token, no network.
262
+
263
+ ```python
264
+ from blackbirdtalk.testing import FakeBlackbird
265
+
266
+ async def test_pay():
267
+ fake = FakeBlackbird(bot)
268
+ await fake.send("/start")
269
+ assert fake.last("sendMessage")["text"] == "Welcome!"
270
+ await fake.tap(fake.last_message(), "pay_49")
271
+ assert fake.last("answerCallback")["text"] == "Payment started"
272
+ ```
273
+
274
+ ## Errors and retries
275
+
276
+ Every error is a `BlackbirdError` with `.code`, `.description` and `.method`:
277
+ `BadRequest`, `Unauthorized` (token wrong or replaced), `Forbidden` (the person
278
+ hasn't started the bot, or blocked it), `NotFound`, `Conflict` (another copy
279
+ is polling with this token), `RateLimited` (`.retry_after`), `ServerError`,
280
+ `NetworkError`, and `MarkupError` for a button mistake caught before sending.
281
+
282
+ "Too many requests" is waited out by itself (up to `max_retry_wait`), and
283
+ server errors and lost connections are retried with back-off. Retries never
284
+ send a message twice: each send carries an id the server uses to drop
285
+ repeats.
286
+
287
+ ## Every method
288
+
289
+ All 25 Bot API methods are on the bot with snake_case names
290
+ (`send_message`, `edit_message`, `answer_callback`, `set_commands`,
291
+ `get_chat`, `remove_member`, `set_access` ...), typed, with docs in your
292
+ editor. Telegram-style names are accepted by the server too, so code ported
293
+ from a Telegram bot mostly works.
294
+
295
+ New fields from the server show up on objects at once (`obj["new_field"]`, or
296
+ `obj.to_dict()`), and a method or parameter newer than your library can still
297
+ be used: `await bot.send_message(chat_id, "hi", new_param=1)`. The full
298
+ reference: <https://blackbirdtalk.com/bots>.
299
+
300
+ ## Updates and versions
301
+
302
+ The library prints one line a day when a newer version is out (turn it off
303
+ with `Bot(check_updates=False)` or `BLACKBIRD_NO_UPDATE_CHECK=1`). Update with
304
+ `pip install -U blackbirdtalk`. Versions: a new minor (1.**2**.0) adds things,
305
+ a patch (1.2.**1**) fixes things; your code keeps working across both. The
306
+ JavaScript library [@blackbirdtalk/bot](https://www.npmjs.com/package/@blackbirdtalk/bot)
307
+ has the same version number and features.
308
+
309
+ MIT licence.
@@ -0,0 +1,277 @@
1
+ # blackbirdtalk
2
+
3
+ The official Python library for [Blackbird](https://blackbirdtalk.com) bots.
4
+
5
+ ```bash
6
+ pip install blackbirdtalk
7
+ ```
8
+
9
+ Python 3.11 or newer. One dependency (httpx).
10
+
11
+ ```python
12
+ from blackbirdtalk import Bot, Button
13
+
14
+ bot = Bot() # reads BLACKBIRD_TOKEN; or Bot("bb_...")
15
+
16
+ @bot.command("start")
17
+ async def start(msg):
18
+ await msg.answer("Welcome!", buttons=[[Button("Pay ₹49", callback="pay_49", style="main", icon="card")]])
19
+
20
+ @bot.button("pay_49")
21
+ async def pay(tap):
22
+ await tap.answer("Payment started")
23
+
24
+ bot.run()
25
+ ```
26
+
27
+ Get the token in the Blackbird app: **Settings › Bot Studio › your bot › Overview**.
28
+ Set it in your terminal so it stays out of your code:
29
+
30
+ | | |
31
+ |---|---|
32
+ | macOS / Linux | `export BLACKBIRD_TOKEN=bb_...` |
33
+ | Windows PowerShell | `$env:BLACKBIRD_TOKEN="bb_..."` |
34
+ | Windows cmd | `set BLACKBIRD_TOKEN=bb_...` |
35
+
36
+ Or start from a ready project: `blackbirdtalk new mybot`. Check a token, its
37
+ webhook and commands any time with `blackbirdtalk check`.
38
+
39
+ ## Contents
40
+
41
+ - [Handlers](#handlers)
42
+ - [Buttons](#buttons)
43
+ - [Questions with ask()](#questions-with-ask)
44
+ - [Files](#files)
45
+ - [Groups](#groups)
46
+ - [Private bots](#private-bots)
47
+ - [Webhooks](#webhooks)
48
+ - [No async: SyncBot](#no-async-syncbot)
49
+ - [Testing your bot](#testing-your-bot)
50
+ - [Errors and retries](#errors-and-retries)
51
+ - [Every method](#every-method)
52
+ - [Updates and versions](#updates-and-versions)
53
+
54
+ ## Handlers
55
+
56
+ The first handler that matches an update runs, in the order you wrote them.
57
+ Messages from one chat are handled one at a time, in order; different chats
58
+ run in parallel.
59
+
60
+ ```python
61
+ @bot.command("quiz", "q") # /quiz physics -> msg.args == "physics"
62
+ async def quiz(msg): ...
63
+
64
+ @bot.message(regex=r"(\d+) apples") # msg.match[1] == "3"
65
+ async def apples(msg): ...
66
+
67
+ @bot.message(text=["hi", "hello"])
68
+ async def hi(msg): ...
69
+
70
+ @bot.message(kind="image") # image, video, voice, doc, text
71
+ async def photo(msg): ...
72
+
73
+ @bot.message(chat="group", when=lambda m: len(m.text or "") > 500)
74
+ async def long_text(msg): ...
75
+
76
+ @bot.message() # everything else
77
+ async def fallback(msg): ...
78
+
79
+ @bot.on_error
80
+ async def oops(error, update):
81
+ print("handler failed:", error)
82
+ ```
83
+
84
+ A message has `msg.answer()` (same chat), `msg.reply()` (quoting it),
85
+ `msg.answer_file()`, `msg.edit()`, `msg.delete()`, `msg.pin()`, `msg.typing()`,
86
+ `msg.ask()` and `msg.download()`.
87
+
88
+ ## Buttons
89
+
90
+ Blackbird buttons have styles, icons, colours, sizes, a confirm step and
91
+ copy-to-clipboard. The library checks every rule the server checks before it
92
+ sends, so a mistake fails on your machine with the button's row and position.
93
+
94
+ ```python
95
+ from blackbirdtalk import Button, Buttons
96
+
97
+ menu = Buttons(
98
+ [Button("Buy ₹49", callback="buy", style="success", icon="card", confirm="Pay ₹49?")],
99
+ [Button("Coupon", copy="SAVE20", icon="copy"), Button("Site", url="https://example.com", icon="link")],
100
+ [Button("Brand colour", callback="x", color="#5B3FD9", size="pill")],
101
+ )
102
+ print(menu.preview()) # see it in the terminal
103
+ await msg.answer("Pick one", buttons=menu)
104
+
105
+ @bot.button(prefix="buy") # also: exact data, a list, regex=, when=
106
+ async def buy(tap):
107
+ await tap.answer("Done!") # a short note (alert=True for a dialog)
108
+ await tap.edit("Paid ✓", buttons=[]) # change the message in place
109
+ ```
110
+
111
+ `style`: main, normal, success, warning, danger. `size`: pill, normal, full.
112
+ `icon`: one of 40 (`blackbirdtalk.BUTTON_ICONS`). Up to 8 rows, 8 per row.
113
+ Taps you don't answer are answered for you, so the spinner never hangs.
114
+
115
+ Keyboard buttons (under the text box) send their text; they can also ask for
116
+ the person's location or contact:
117
+
118
+ ```python
119
+ from blackbirdtalk import Keyboard, RemoveKeyboard
120
+
121
+ await msg.answer("When?", keyboard=Keyboard([["Today", "Tomorrow"],
122
+ [Button("Share location", request="location")]], one_time=True))
123
+ await msg.answer("Thanks", reply_markup=RemoveKeyboard())
124
+ ```
125
+
126
+ ## Questions with ask()
127
+
128
+ Write a conversation top to bottom. Each person's answers come back to their
129
+ own question, even with many people at once.
130
+
131
+ ```python
132
+ from blackbirdtalk import AskTimeout
133
+
134
+ @bot.command("register")
135
+ async def register(msg):
136
+ try:
137
+ name = await msg.ask("Your name?", placeholder="Full name")
138
+ tap = await msg.ask("Class?", buttons=[[Button("10", callback="10"), Button("12", callback="12")]])
139
+ except AskTimeout: # default 5 minutes; timeout=...
140
+ return await msg.answer("Stopped. Send /register again.")
141
+ await msg.answer(f"Saved {name.text}, class {tap.data}")
142
+ ```
143
+
144
+ A text answer comes back as a Message, a tap as a Callback.
145
+
146
+ ## Files
147
+
148
+ ```python
149
+ await msg.answer_file("report.pdf", caption="Your report") # a path
150
+ await msg.answer_file(InputFile(data, filename="chart.png")) # bytes made in memory
151
+ await bot.send_document(chat_id, file_id=old.file.file_id) # send again, no upload
152
+
153
+ @bot.message(kind="doc")
154
+ async def got(msg):
155
+ data = await msg.download() # bytes; or msg.download("saved.pdf")
156
+ ```
157
+
158
+ ## Groups
159
+
160
+ Add the bot to a group and give it the powers it needs (approve, remove,
161
+ delete, pin ...); `msg.chat.my_powers` lists what it has.
162
+
163
+ ```python
164
+ @bot.on_join_request()
165
+ async def check(req):
166
+ await (req.approve() if "blackbird" in (req.answer or "").lower() else req.decline())
167
+
168
+ @bot.on_member(lambda m: m.event == "joined")
169
+ async def welcome(change):
170
+ await change.chat.send("Welcome " + ", ".join(u.name for u in change.users))
171
+
172
+ @bot.on_my_membership(lambda m: m.status == "added")
173
+ async def added(change): ...
174
+ ```
175
+
176
+ ## Private bots
177
+
178
+ Choose who can use the bot (`everyone`, `owner`, or `chosen` people) from the
179
+ code or from Bot Studio. People can ask for access; you decide.
180
+
181
+ ```python
182
+ await bot.set_access(audience="chosen", message="For our class only", allow=[user_id])
183
+
184
+ @bot.on_access_request()
185
+ async def asked(req):
186
+ await req.approve() # or req.decline()
187
+ ```
188
+
189
+ ## Webhooks
190
+
191
+ Polling (`bot.run()`) needs nothing but the token. On a server with HTTPS you
192
+ can use a webhook instead:
193
+
194
+ ```python
195
+ app = bot.webhook_app(secret="long-random-text", path="/hook") # ASGI: uvicorn mybot:app
196
+ await bot.set_webhook("https://your.domain/hook", secret_token="long-random-text") # once
197
+ ```
198
+
199
+ FastAPI, aiohttp or anything else:
200
+
201
+ ```python
202
+ @api.post("/hook")
203
+ async def hook(request: Request):
204
+ status = await bot.handle_webhook(await request.body(), request.headers, secret=SECRET)
205
+ return Response(status_code=status)
206
+ ```
207
+
208
+ `SyncBot.wsgi_app()` does the same for gunicorn, Flask and Django. The secret
209
+ header is checked in constant time; requests without it get 401.
210
+
211
+ ## No async: SyncBot
212
+
213
+ Same handlers and helpers with plain functions:
214
+
215
+ ```python
216
+ from blackbirdtalk import SyncBot
217
+
218
+ bot = SyncBot()
219
+
220
+ @bot.command("start")
221
+ def start(msg):
222
+ msg.answer("Hi!")
223
+
224
+ bot.run()
225
+ ```
226
+
227
+ ## Testing your bot
228
+
229
+ `FakeBlackbird` stands in for the server: no token, no network.
230
+
231
+ ```python
232
+ from blackbirdtalk.testing import FakeBlackbird
233
+
234
+ async def test_pay():
235
+ fake = FakeBlackbird(bot)
236
+ await fake.send("/start")
237
+ assert fake.last("sendMessage")["text"] == "Welcome!"
238
+ await fake.tap(fake.last_message(), "pay_49")
239
+ assert fake.last("answerCallback")["text"] == "Payment started"
240
+ ```
241
+
242
+ ## Errors and retries
243
+
244
+ Every error is a `BlackbirdError` with `.code`, `.description` and `.method`:
245
+ `BadRequest`, `Unauthorized` (token wrong or replaced), `Forbidden` (the person
246
+ hasn't started the bot, or blocked it), `NotFound`, `Conflict` (another copy
247
+ is polling with this token), `RateLimited` (`.retry_after`), `ServerError`,
248
+ `NetworkError`, and `MarkupError` for a button mistake caught before sending.
249
+
250
+ "Too many requests" is waited out by itself (up to `max_retry_wait`), and
251
+ server errors and lost connections are retried with back-off. Retries never
252
+ send a message twice: each send carries an id the server uses to drop
253
+ repeats.
254
+
255
+ ## Every method
256
+
257
+ All 25 Bot API methods are on the bot with snake_case names
258
+ (`send_message`, `edit_message`, `answer_callback`, `set_commands`,
259
+ `get_chat`, `remove_member`, `set_access` ...), typed, with docs in your
260
+ editor. Telegram-style names are accepted by the server too, so code ported
261
+ from a Telegram bot mostly works.
262
+
263
+ New fields from the server show up on objects at once (`obj["new_field"]`, or
264
+ `obj.to_dict()`), and a method or parameter newer than your library can still
265
+ be used: `await bot.send_message(chat_id, "hi", new_param=1)`. The full
266
+ reference: <https://blackbirdtalk.com/bots>.
267
+
268
+ ## Updates and versions
269
+
270
+ The library prints one line a day when a newer version is out (turn it off
271
+ with `Bot(check_updates=False)` or `BLACKBIRD_NO_UPDATE_CHECK=1`). Update with
272
+ `pip install -U blackbirdtalk`. Versions: a new minor (1.**2**.0) adds things,
273
+ a patch (1.2.**1**) fixes things; your code keeps working across both. The
274
+ JavaScript library [@blackbirdtalk/bot](https://www.npmjs.com/package/@blackbirdtalk/bot)
275
+ has the same version number and features.
276
+
277
+ MIT licence.