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.
- blackbirdtalk-1.0.0/.gitignore +89 -0
- blackbirdtalk-1.0.0/LICENSE +21 -0
- blackbirdtalk-1.0.0/PKG-INFO +309 -0
- blackbirdtalk-1.0.0/README.md +277 -0
- blackbirdtalk-1.0.0/examples/echo.py +25 -0
- blackbirdtalk-1.0.0/examples/files.py +31 -0
- blackbirdtalk-1.0.0/examples/form.py +37 -0
- blackbirdtalk-1.0.0/examples/group_guard.py +46 -0
- blackbirdtalk-1.0.0/examples/menu.py +64 -0
- blackbirdtalk-1.0.0/examples/private_bot.py +55 -0
- blackbirdtalk-1.0.0/examples/sync_echo.py +28 -0
- blackbirdtalk-1.0.0/examples/webhook_asgi.py +37 -0
- blackbirdtalk-1.0.0/pyproject.toml +72 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/__init__.py +118 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/__main__.py +3 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/_core.py +175 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/_gen/__init__.py +1 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/_gen/constants.py +84 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/_gen/methods.py +783 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/_gen/types.py +541 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/_helpers.py +173 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/_object.py +76 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/_router.py +238 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/_types_in.py +64 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/_update_check.py +67 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/bot.py +446 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/cli.py +155 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/errors.py +77 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/markup.py +249 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/py.typed +0 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/sync.py +403 -0
- blackbirdtalk-1.0.0/src/blackbirdtalk/testing.py +168 -0
- blackbirdtalk-1.0.0/tests/live/test_live_server.py +185 -0
- blackbirdtalk-1.0.0/tests/test_core.py +192 -0
- blackbirdtalk-1.0.0/tests/test_dispatch.py +211 -0
- blackbirdtalk-1.0.0/tests/test_examples.py +142 -0
- blackbirdtalk-1.0.0/tests/test_markup.py +90 -0
- 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.
|