xtr-telegram-notifier 5.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.
- xtr_telegram_notifier-5.0.0/LICENSE +22 -0
- xtr_telegram_notifier-5.0.0/PKG-INFO +190 -0
- xtr_telegram_notifier-5.0.0/README.md +167 -0
- xtr_telegram_notifier-5.0.0/pyproject.toml +162 -0
- xtr_telegram_notifier-5.0.0/pyproject.toml.orig +164 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/.agents/skills/xtr-telegram-notifier/SKILL.md +118 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/__init__.py +15 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/_markdown_v2.py +24 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/_payload.py +186 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/py.typed +0 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/recipe/__init__.py +15 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/recipe/manifest.toml +23 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/__init__.py +5 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/__init__.py +17 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/abstract_telegram_reply_markup.py +25 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/button/__init__.py +9 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/button/abstract_keyboard_button.py +23 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/button/inline_keyboard_button.py +64 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/button/keyboard_button.py +34 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/force_reply.py +26 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/inline_keyboard_markup.py +30 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/reply_keyboard_markup.py +45 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/reply/markup/reply_keyboard_remove.py +26 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/telegram_options.py +235 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/telegram_transport.py +196 -0
- xtr_telegram_notifier-5.0.0/src/xtr_telegram_notifier/telegram_transport_factory.py +69 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xterr
|
|
4
|
+
Copyright (c) 2019-present Fabien Potencier
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xtr-telegram-notifier
|
|
3
|
+
Version: 5.0.0
|
|
4
|
+
Summary: Telegram for xtr-notifier — chat messages posted, edited and answered through a bot, with keyboards, media and file uploads, configured by DSN.
|
|
5
|
+
Keywords: notifier,notification,telegram,chat,bot,dsn,asyncio
|
|
6
|
+
Author: Xterr
|
|
7
|
+
Author-email: Xterr <me@xterr.dev>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Dist: typing-extensions>=4.4
|
|
19
|
+
Requires-Dist: xtr-notifier>=5.0,<6
|
|
20
|
+
Requires-Dist: xtr-http-client-contracts>=5.0,<6
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
<div align="center">
|
|
25
|
+
|
|
26
|
+
# xtr-telegram-notifier
|
|
27
|
+
|
|
28
|
+
**Telegram for xtr-notifier — chat messages posted, edited and answered through a bot, with keyboards, media and file uploads, configured by DSN.**
|
|
29
|
+
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Why?
|
|
35
|
+
|
|
36
|
+
[xtr-notifier](../xtr-notifier) sends chat messages through whatever provider a DSN names. This
|
|
37
|
+
library is the Telegram provider: installed beside it, a `telegram://` DSN is served with no
|
|
38
|
+
wiring, and a message can carry everything the bot API offers — a keyboard, a photo, a location,
|
|
39
|
+
an edit of a message already sent, the answer to a button's callback.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
uv add xtr-telegram-notifier
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
It needs an HTTP client implementing
|
|
48
|
+
[xtr-http-client-contracts](../xtr-http-client-contracts) — usually
|
|
49
|
+
[xtr-http-client](../xtr-http-client).
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from xtr_http_client import HttpClient
|
|
55
|
+
from xtr_notifier import Chatter, Transport
|
|
56
|
+
from xtr_notifier.message import ChatMessage
|
|
57
|
+
from xtr_notifier.transport import TransportFactoryDiscovery
|
|
58
|
+
|
|
59
|
+
discovery = TransportFactoryDiscovery(
|
|
60
|
+
TransportFactoryDiscovery.CHATTER_GROUP, http_client=HttpClient.create()
|
|
61
|
+
)
|
|
62
|
+
transport = Transport(discovery=discovery).from_string(
|
|
63
|
+
"telegram://123456:ABC-DEF@default?channel=-1001234567890"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
await Chatter(transport).send(ChatMessage("Deploy *finished* in 42s."))
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## The DSN
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
telegram://BOT_ID:SECRET@default?channel=CHAT_ID
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `BOT_ID:SECRET` — the bot's token, as the bot API hands it out; a DSN without it, or with half
|
|
76
|
+
of it, is refused with `Missing token.` or `Malformed token.`
|
|
77
|
+
- `default` — `api.telegram.org`; name another host (and port) to reach a local bot API server.
|
|
78
|
+
- `channel` — the chat a message naming none goes to. To find a chat's id, message the bot from
|
|
79
|
+
that chat and read `https://api.telegram.org/bot<token>/getUpdates`.
|
|
80
|
+
- `ssl=false` — reach the host over plain HTTP; `sslmode=disable` is the older spelling.
|
|
81
|
+
|
|
82
|
+
The transport's string form — what errors, events and `debug:config` show — is
|
|
83
|
+
`telegram://api.telegram.org?channel=CHAT_ID`: never the token.
|
|
84
|
+
|
|
85
|
+
## Options
|
|
86
|
+
|
|
87
|
+
`TelegramOptions` builds what a message carries beyond its text, one call at a time;
|
|
88
|
+
`to_options()` renders it for the message:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from xtr_notifier.message import ChatMessage
|
|
92
|
+
from xtr_telegram_notifier import TelegramOptions
|
|
93
|
+
from xtr_telegram_notifier.reply.markup import InlineKeyboardMarkup
|
|
94
|
+
from xtr_telegram_notifier.reply.markup.button import InlineKeyboardButton
|
|
95
|
+
|
|
96
|
+
options = (
|
|
97
|
+
TelegramOptions()
|
|
98
|
+
.chat_id("-1001234567890")
|
|
99
|
+
.disable_notification(True)
|
|
100
|
+
.reply_markup(
|
|
101
|
+
InlineKeyboardMarkup().inline_keyboard(
|
|
102
|
+
[InlineKeyboardButton("Open").url("https://example.com/deploys/42")]
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
await chatter.send(ChatMessage("Deploy finished.", options.to_options()))
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
What the message is follows from the option set, and only one of these may be:
|
|
110
|
+
|
|
111
|
+
| Option | Method | The text goes in |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| none | `sendMessage` | `text` |
|
|
114
|
+
| `photo` / `upload_photo` | `sendPhoto` | `caption` |
|
|
115
|
+
| `video`, `audio`, `document`, `animation` (and `upload_*`) | `sendVideo`, … | `caption` |
|
|
116
|
+
| `sticker` / `upload_sticker` | `sendSticker` | not sent |
|
|
117
|
+
| `location`, `venue`, `contact` | `sendLocation`, `sendVenue`, `sendContact` | not sent |
|
|
118
|
+
| `edit(message_id)` | `editMessageText`, or `editMessageMedia` with one media by URL or file id | `text`, or the media's caption |
|
|
119
|
+
| `edit_caption(message_id)` | `editMessageCaption` | `caption` |
|
|
120
|
+
| `answer_callback_query(id, show_alert, cache_time)` | `answerCallbackQuery` | `text` |
|
|
121
|
+
|
|
122
|
+
Setting two of them raises `MultipleExclusiveOptionsUsedError`; editing a message's media with a
|
|
123
|
+
file to upload raises `LogicError` — the bot API takes a URL or a file id there.
|
|
124
|
+
|
|
125
|
+
The text is sent as MarkdownV2 unless `parse_mode(...)` picks `TelegramOptions.PARSE_MODE_HTML`
|
|
126
|
+
or `PARSE_MODE_MARKDOWN`. It is escaped first: the characters the bot API rejects unescaped
|
|
127
|
+
(`.`, `!`, `#`, `+`, `-`, `=`, `|`, `{`, `}`, `>`) are escaped, while markup written on purpose —
|
|
128
|
+
`*bold*`, `_italic_`, `` `code` ``, `~strike~`, `||spoiler||`, `[link](https://example.com)`, block quotations,
|
|
129
|
+
custom emojis — and characters already escaped are kept.
|
|
130
|
+
|
|
131
|
+
Keyboards: `InlineKeyboardMarkup` with `InlineKeyboardButton`s (link, login URL, callback data,
|
|
132
|
+
inline query, payment), `ReplyKeyboardMarkup` with `KeyboardButton`s (text, contact, location,
|
|
133
|
+
poll), `ReplyKeyboardRemove` and `ForceReply`.
|
|
134
|
+
|
|
135
|
+
## Uploads
|
|
136
|
+
|
|
137
|
+
`upload_photo(path)` and its siblings keep the file's **path**, so a message stays plain data and
|
|
138
|
+
can be queued on a message bus. The transport opens each file when it sends the message, sends
|
|
139
|
+
the fields as a form body — which the HTTP client sends as `multipart/form-data`, the file named
|
|
140
|
+
after its path — and closes the files once the request is made, whether it succeeded or not.
|
|
141
|
+
A field holding an object or a list, such as a reply markup, goes in that body as JSON text, the
|
|
142
|
+
way the bot API reads it. A message without an upload is sent as JSON.
|
|
143
|
+
|
|
144
|
+
## Errors
|
|
145
|
+
|
|
146
|
+
| Error | When |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `IncompleteDsnError` | The DSN carries no token (`Missing token.`) or half of one (`Malformed token.`) |
|
|
149
|
+
| `UnsupportedSchemeError` | The factory was handed a DSN that is not `telegram://` |
|
|
150
|
+
| `UnsupportedMessageTypeError` | The transport was handed a message that is not a `ChatMessage` |
|
|
151
|
+
| `MultipleExclusiveOptionsUsedError` | The options pick more than one kind of message |
|
|
152
|
+
| `LogicError` | An edit replaces a media with a file to upload |
|
|
153
|
+
| `TransportError` | `Unable to post the Telegram message: <description> (code <n>).` (or `edit`, `answer callback query`), or `Could not reach the remote Telegram server.` |
|
|
154
|
+
|
|
155
|
+
Every one of them is xtr-notifier's; `TransportError` is what failover moves on from.
|
|
156
|
+
|
|
157
|
+
## Use in an application
|
|
158
|
+
|
|
159
|
+
Everything adding this package to an application takes — and, read backwards, what removing it
|
|
160
|
+
undoes. It ships no bundle: the notifier's bundle finds its factory by the DSN scheme.
|
|
161
|
+
|
|
162
|
+
- **Install** — `uv add xtr-telegram-notifier`, beside `xtr-notifier` set up as its
|
|
163
|
+
[*Use in an application*](../xtr-notifier/README.md#use-in-an-application) says.
|
|
164
|
+
- **Recipe** — `uv run xtr-recipes recipes:sync` does the *Environment* step below: it writes
|
|
165
|
+
`TELEGRAM_DSN` in `.env`. It prints the step adding the transport to
|
|
166
|
+
`<app>/config/notifier.py`, which a recipe cannot make for you.
|
|
167
|
+
- **Configure** — add `telegram=env("TELEGRAM_DSN")` to `chatter_transports` in
|
|
168
|
+
`<app>/config/notifier.py`.
|
|
169
|
+
- **Environment** — `TELEGRAM_DSN`, `telegram://TOKEN@default?channel=CHAT_ID` with the bot's
|
|
170
|
+
token and the chat; read when a message is first sent.
|
|
171
|
+
- **Remove** — drop the `chatter_transports` entry and `TELEGRAM_DSN`, then
|
|
172
|
+
`uv remove xtr-telegram-notifier`.
|
|
173
|
+
- **Check** — `debug:config notifier` lists `telegram` under `chatter_transports`.
|
|
174
|
+
|
|
175
|
+
Everything else — the bundle, the channel policy, queuing through the message bus — is
|
|
176
|
+
xtr-notifier's.
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
uv run pytest
|
|
182
|
+
uv run ruff check
|
|
183
|
+
uv run ruff format --check
|
|
184
|
+
uv run basedpyright
|
|
185
|
+
uv run ty check
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# xtr-telegram-notifier
|
|
4
|
+
|
|
5
|
+
**Telegram for xtr-notifier — chat messages posted, edited and answered through a bot, with keyboards, media and file uploads, configured by DSN.**
|
|
6
|
+
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why?
|
|
12
|
+
|
|
13
|
+
[xtr-notifier](../xtr-notifier) sends chat messages through whatever provider a DSN names. This
|
|
14
|
+
library is the Telegram provider: installed beside it, a `telegram://` DSN is served with no
|
|
15
|
+
wiring, and a message can carry everything the bot API offers — a keyboard, a photo, a location,
|
|
16
|
+
an edit of a message already sent, the answer to a button's callback.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
uv add xtr-telegram-notifier
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
It needs an HTTP client implementing
|
|
25
|
+
[xtr-http-client-contracts](../xtr-http-client-contracts) — usually
|
|
26
|
+
[xtr-http-client](../xtr-http-client).
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from xtr_http_client import HttpClient
|
|
32
|
+
from xtr_notifier import Chatter, Transport
|
|
33
|
+
from xtr_notifier.message import ChatMessage
|
|
34
|
+
from xtr_notifier.transport import TransportFactoryDiscovery
|
|
35
|
+
|
|
36
|
+
discovery = TransportFactoryDiscovery(
|
|
37
|
+
TransportFactoryDiscovery.CHATTER_GROUP, http_client=HttpClient.create()
|
|
38
|
+
)
|
|
39
|
+
transport = Transport(discovery=discovery).from_string(
|
|
40
|
+
"telegram://123456:ABC-DEF@default?channel=-1001234567890"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
await Chatter(transport).send(ChatMessage("Deploy *finished* in 42s."))
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## The DSN
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
telegram://BOT_ID:SECRET@default?channel=CHAT_ID
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- `BOT_ID:SECRET` — the bot's token, as the bot API hands it out; a DSN without it, or with half
|
|
53
|
+
of it, is refused with `Missing token.` or `Malformed token.`
|
|
54
|
+
- `default` — `api.telegram.org`; name another host (and port) to reach a local bot API server.
|
|
55
|
+
- `channel` — the chat a message naming none goes to. To find a chat's id, message the bot from
|
|
56
|
+
that chat and read `https://api.telegram.org/bot<token>/getUpdates`.
|
|
57
|
+
- `ssl=false` — reach the host over plain HTTP; `sslmode=disable` is the older spelling.
|
|
58
|
+
|
|
59
|
+
The transport's string form — what errors, events and `debug:config` show — is
|
|
60
|
+
`telegram://api.telegram.org?channel=CHAT_ID`: never the token.
|
|
61
|
+
|
|
62
|
+
## Options
|
|
63
|
+
|
|
64
|
+
`TelegramOptions` builds what a message carries beyond its text, one call at a time;
|
|
65
|
+
`to_options()` renders it for the message:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from xtr_notifier.message import ChatMessage
|
|
69
|
+
from xtr_telegram_notifier import TelegramOptions
|
|
70
|
+
from xtr_telegram_notifier.reply.markup import InlineKeyboardMarkup
|
|
71
|
+
from xtr_telegram_notifier.reply.markup.button import InlineKeyboardButton
|
|
72
|
+
|
|
73
|
+
options = (
|
|
74
|
+
TelegramOptions()
|
|
75
|
+
.chat_id("-1001234567890")
|
|
76
|
+
.disable_notification(True)
|
|
77
|
+
.reply_markup(
|
|
78
|
+
InlineKeyboardMarkup().inline_keyboard(
|
|
79
|
+
[InlineKeyboardButton("Open").url("https://example.com/deploys/42")]
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
await chatter.send(ChatMessage("Deploy finished.", options.to_options()))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
What the message is follows from the option set, and only one of these may be:
|
|
87
|
+
|
|
88
|
+
| Option | Method | The text goes in |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| none | `sendMessage` | `text` |
|
|
91
|
+
| `photo` / `upload_photo` | `sendPhoto` | `caption` |
|
|
92
|
+
| `video`, `audio`, `document`, `animation` (and `upload_*`) | `sendVideo`, … | `caption` |
|
|
93
|
+
| `sticker` / `upload_sticker` | `sendSticker` | not sent |
|
|
94
|
+
| `location`, `venue`, `contact` | `sendLocation`, `sendVenue`, `sendContact` | not sent |
|
|
95
|
+
| `edit(message_id)` | `editMessageText`, or `editMessageMedia` with one media by URL or file id | `text`, or the media's caption |
|
|
96
|
+
| `edit_caption(message_id)` | `editMessageCaption` | `caption` |
|
|
97
|
+
| `answer_callback_query(id, show_alert, cache_time)` | `answerCallbackQuery` | `text` |
|
|
98
|
+
|
|
99
|
+
Setting two of them raises `MultipleExclusiveOptionsUsedError`; editing a message's media with a
|
|
100
|
+
file to upload raises `LogicError` — the bot API takes a URL or a file id there.
|
|
101
|
+
|
|
102
|
+
The text is sent as MarkdownV2 unless `parse_mode(...)` picks `TelegramOptions.PARSE_MODE_HTML`
|
|
103
|
+
or `PARSE_MODE_MARKDOWN`. It is escaped first: the characters the bot API rejects unescaped
|
|
104
|
+
(`.`, `!`, `#`, `+`, `-`, `=`, `|`, `{`, `}`, `>`) are escaped, while markup written on purpose —
|
|
105
|
+
`*bold*`, `_italic_`, `` `code` ``, `~strike~`, `||spoiler||`, `[link](https://example.com)`, block quotations,
|
|
106
|
+
custom emojis — and characters already escaped are kept.
|
|
107
|
+
|
|
108
|
+
Keyboards: `InlineKeyboardMarkup` with `InlineKeyboardButton`s (link, login URL, callback data,
|
|
109
|
+
inline query, payment), `ReplyKeyboardMarkup` with `KeyboardButton`s (text, contact, location,
|
|
110
|
+
poll), `ReplyKeyboardRemove` and `ForceReply`.
|
|
111
|
+
|
|
112
|
+
## Uploads
|
|
113
|
+
|
|
114
|
+
`upload_photo(path)` and its siblings keep the file's **path**, so a message stays plain data and
|
|
115
|
+
can be queued on a message bus. The transport opens each file when it sends the message, sends
|
|
116
|
+
the fields as a form body — which the HTTP client sends as `multipart/form-data`, the file named
|
|
117
|
+
after its path — and closes the files once the request is made, whether it succeeded or not.
|
|
118
|
+
A field holding an object or a list, such as a reply markup, goes in that body as JSON text, the
|
|
119
|
+
way the bot API reads it. A message without an upload is sent as JSON.
|
|
120
|
+
|
|
121
|
+
## Errors
|
|
122
|
+
|
|
123
|
+
| Error | When |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `IncompleteDsnError` | The DSN carries no token (`Missing token.`) or half of one (`Malformed token.`) |
|
|
126
|
+
| `UnsupportedSchemeError` | The factory was handed a DSN that is not `telegram://` |
|
|
127
|
+
| `UnsupportedMessageTypeError` | The transport was handed a message that is not a `ChatMessage` |
|
|
128
|
+
| `MultipleExclusiveOptionsUsedError` | The options pick more than one kind of message |
|
|
129
|
+
| `LogicError` | An edit replaces a media with a file to upload |
|
|
130
|
+
| `TransportError` | `Unable to post the Telegram message: <description> (code <n>).` (or `edit`, `answer callback query`), or `Could not reach the remote Telegram server.` |
|
|
131
|
+
|
|
132
|
+
Every one of them is xtr-notifier's; `TransportError` is what failover moves on from.
|
|
133
|
+
|
|
134
|
+
## Use in an application
|
|
135
|
+
|
|
136
|
+
Everything adding this package to an application takes — and, read backwards, what removing it
|
|
137
|
+
undoes. It ships no bundle: the notifier's bundle finds its factory by the DSN scheme.
|
|
138
|
+
|
|
139
|
+
- **Install** — `uv add xtr-telegram-notifier`, beside `xtr-notifier` set up as its
|
|
140
|
+
[*Use in an application*](../xtr-notifier/README.md#use-in-an-application) says.
|
|
141
|
+
- **Recipe** — `uv run xtr-recipes recipes:sync` does the *Environment* step below: it writes
|
|
142
|
+
`TELEGRAM_DSN` in `.env`. It prints the step adding the transport to
|
|
143
|
+
`<app>/config/notifier.py`, which a recipe cannot make for you.
|
|
144
|
+
- **Configure** — add `telegram=env("TELEGRAM_DSN")` to `chatter_transports` in
|
|
145
|
+
`<app>/config/notifier.py`.
|
|
146
|
+
- **Environment** — `TELEGRAM_DSN`, `telegram://TOKEN@default?channel=CHAT_ID` with the bot's
|
|
147
|
+
token and the chat; read when a message is first sent.
|
|
148
|
+
- **Remove** — drop the `chatter_transports` entry and `TELEGRAM_DSN`, then
|
|
149
|
+
`uv remove xtr-telegram-notifier`.
|
|
150
|
+
- **Check** — `debug:config notifier` lists `telegram` under `chatter_transports`.
|
|
151
|
+
|
|
152
|
+
Everything else — the bundle, the channel policy, queuing through the message bus — is
|
|
153
|
+
xtr-notifier's.
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
uv run pytest
|
|
159
|
+
uv run ruff check
|
|
160
|
+
uv run ruff format --check
|
|
161
|
+
uv run basedpyright
|
|
162
|
+
uv run ty check
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "xtr-telegram-notifier"
|
|
3
|
+
version = "5.0.0"
|
|
4
|
+
description = "Telegram for xtr-notifier — chat messages posted, edited and answered through a bot, with keyboards, media and file uploads, configured by DSN."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"notifier",
|
|
11
|
+
"notification",
|
|
12
|
+
"telegram",
|
|
13
|
+
"chat",
|
|
14
|
+
"bot",
|
|
15
|
+
"dsn",
|
|
16
|
+
"asyncio",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Framework :: AsyncIO",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"typing-extensions>=4.4",
|
|
30
|
+
"xtr-notifier>=5.0,<6",
|
|
31
|
+
"xtr-http-client-contracts>=5.0,<6",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[[project.authors]]
|
|
35
|
+
name = "Xterr"
|
|
36
|
+
email = "me@xterr.dev"
|
|
37
|
+
|
|
38
|
+
[project.entry-points."xtr_notifier.chatter_transport_factories"]
|
|
39
|
+
telegram = "xtr_telegram_notifier:TelegramTransportFactory"
|
|
40
|
+
|
|
41
|
+
[project.entry-points.xtr_recipes]
|
|
42
|
+
telegram = "xtr_telegram_notifier.recipe"
|
|
43
|
+
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = [
|
|
46
|
+
"basedpyright>=1.21",
|
|
47
|
+
"ruff>=0.8",
|
|
48
|
+
"pytest>=8",
|
|
49
|
+
"pytest-cov>=5",
|
|
50
|
+
"anyio>=4.0",
|
|
51
|
+
"ty>=0.0.83",
|
|
52
|
+
"xtr-http-client>=5.0,<6",
|
|
53
|
+
"xtr-recipes>=5.0,<6",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[build-system]
|
|
57
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
58
|
+
build-backend = "uv_build"
|
|
59
|
+
|
|
60
|
+
[tool.basedpyright]
|
|
61
|
+
typeCheckingMode = "all"
|
|
62
|
+
pythonVersion = "3.11"
|
|
63
|
+
pythonPlatform = "All"
|
|
64
|
+
include = [
|
|
65
|
+
"src",
|
|
66
|
+
"tests",
|
|
67
|
+
]
|
|
68
|
+
exclude = [
|
|
69
|
+
"**/__pycache__",
|
|
70
|
+
"**/.venv",
|
|
71
|
+
"**/build",
|
|
72
|
+
"**/dist",
|
|
73
|
+
".tmp",
|
|
74
|
+
]
|
|
75
|
+
reportUnusedCallResult = "warning"
|
|
76
|
+
reportUnnecessaryTypeIgnoreComment = "error"
|
|
77
|
+
reportUnusedVariable = "error"
|
|
78
|
+
reportMissingParameterType = "error"
|
|
79
|
+
reportPrivateUsage = "error"
|
|
80
|
+
reportImplicitStringConcatenation = "none"
|
|
81
|
+
|
|
82
|
+
[[tool.basedpyright.executionEnvironments]]
|
|
83
|
+
root = "tests"
|
|
84
|
+
extraPaths = ["."]
|
|
85
|
+
reportPrivateUsage = "none"
|
|
86
|
+
|
|
87
|
+
[tool.ruff]
|
|
88
|
+
target-version = "py311"
|
|
89
|
+
line-length = 100
|
|
90
|
+
src = [
|
|
91
|
+
"src",
|
|
92
|
+
"tests",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
[tool.ruff.lint]
|
|
96
|
+
select = ["ALL"]
|
|
97
|
+
ignore = [
|
|
98
|
+
"COM812",
|
|
99
|
+
"ISC001",
|
|
100
|
+
"D203",
|
|
101
|
+
"D213",
|
|
102
|
+
"CPY001",
|
|
103
|
+
"FBT001",
|
|
104
|
+
"FBT002",
|
|
105
|
+
"TD002",
|
|
106
|
+
"TD003",
|
|
107
|
+
"FIX002",
|
|
108
|
+
"TRY003",
|
|
109
|
+
"EM101",
|
|
110
|
+
"EM102",
|
|
111
|
+
]
|
|
112
|
+
fixable = ["ALL"]
|
|
113
|
+
unfixable = []
|
|
114
|
+
|
|
115
|
+
[tool.ruff.lint.per-file-ignores]
|
|
116
|
+
"tests/**/*.py" = [
|
|
117
|
+
"S101",
|
|
118
|
+
"ARG",
|
|
119
|
+
"PLR2004",
|
|
120
|
+
"SLF001",
|
|
121
|
+
"D",
|
|
122
|
+
"FBT003",
|
|
123
|
+
"S105",
|
|
124
|
+
"S106",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
[tool.ruff.lint.pydocstyle]
|
|
128
|
+
convention = "google"
|
|
129
|
+
|
|
130
|
+
[tool.ruff.format]
|
|
131
|
+
quote-style = "double"
|
|
132
|
+
indent-style = "space"
|
|
133
|
+
docstring-code-format = true
|
|
134
|
+
docstring-code-line-length = "dynamic"
|
|
135
|
+
|
|
136
|
+
[tool.ty.src]
|
|
137
|
+
include = [
|
|
138
|
+
"src",
|
|
139
|
+
"tests",
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
[tool.pytest.ini_options]
|
|
143
|
+
minversion = "8.0"
|
|
144
|
+
testpaths = ["tests"]
|
|
145
|
+
addopts = [
|
|
146
|
+
"-ra",
|
|
147
|
+
"--strict-config",
|
|
148
|
+
"--strict-markers",
|
|
149
|
+
]
|
|
150
|
+
filterwarnings = ["error"]
|
|
151
|
+
|
|
152
|
+
[tool.coverage.run]
|
|
153
|
+
source = ["src"]
|
|
154
|
+
branch = true
|
|
155
|
+
|
|
156
|
+
[tool.coverage.report]
|
|
157
|
+
fail_under = 95
|
|
158
|
+
exclude_lines = [
|
|
159
|
+
"pragma: no cover",
|
|
160
|
+
"if TYPE_CHECKING:",
|
|
161
|
+
"raise NotImplementedError",
|
|
162
|
+
]
|