telekit 2.2.0a3__tar.gz → 2.3.0b1__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.
- {telekit-2.2.0a3/telekit.egg-info → telekit-2.3.0b1}/PKG-INFO +76 -38
- {telekit-2.2.0a3 → telekit-2.3.0b1}/README.md +2 -1
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/__init__.py +2 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_handler.py +7 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_inline_buttons.py +1 -1
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_version.py +1 -1
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_handlers/entry.py +5 -1
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_handlers/text_document.py +6 -2
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/senders.py +130 -39
- telekit-2.3.0b1/telekit/traits/__init__.py +2 -0
- telekit-2.3.0b1/telekit/traits/paginated_choice.py +109 -0
- telekit-2.3.0b1/telekit/traits/track_handoff_origin.py +99 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/utils.py +116 -14
- {telekit-2.2.0a3 → telekit-2.3.0b1/telekit.egg-info}/PKG-INFO +76 -38
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit.egg-info/SOURCES.txt +4 -1
- {telekit-2.2.0a3 → telekit-2.3.0b1}/LICENSE +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/setup.cfg +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/setup.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_buildtext/__init__.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_buildtext/formatter.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_buildtext/styles.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_callback_query_handler.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_chain.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_chain_base.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_chain_entry_logic.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_chain_inline_keyboards_logic.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_chapters/__init__.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_chapters/chapters.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_init.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_input_handler.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_logger.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_on.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_snapvault/__init__.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_snapvault/snapcode.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_snapvault/snapvault.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_state.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_telekit_dsl/__init__.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_telekit_dsl/mixin.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_telekit_dsl/parser/__init__.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_telekit_dsl/parser/builder.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_telekit_dsl/parser/canvas_parser.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_telekit_dsl/parser/lexer.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_telekit_dsl/parser/nodes.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_telekit_dsl/parser/parser.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_telekit_dsl/parser/token.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_telekit_dsl/telekit_dsl.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_telekit_dsl/telekit_orm.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_timeout.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/_user.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/dices.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/__init__.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_handlers/__init__.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_handlers/complete_hotel.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_handlers/counter.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_handlers/dsl.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_handlers/faq.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_handlers/hotel.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_handlers/on_text.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_handlers/pages.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_handlers/pyapi.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_handlers/qr.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_handlers/quiz.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_handlers/spells.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_handlers/start.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/example/example_server.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/inline_buttons.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/parameters.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/server.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/styles.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit/types.py +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit.egg-info/dependency_links.txt +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit.egg-info/requires.txt +0 -0
- {telekit-2.2.0a3 → telekit-2.3.0b1}/telekit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: telekit
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0b1
|
|
4
4
|
Summary: Declarative, developer-friendly library for building Telegram bots
|
|
5
5
|
Home-page: https://github.com/Romashkaa/telekit
|
|
6
6
|
Author: romashka
|
|
@@ -101,7 +101,8 @@ Even in its beta stage, Telekit accelerates bot development, offering typed comm
|
|
|
101
101
|
- [Dialogue](https://github.com/Romashkaa/telekit/blob/main/docs/examples/dialogue.md)
|
|
102
102
|
- [Risk Game](https://github.com/Romashkaa/telekit/blob/main/docs/examples/risk_game.md)
|
|
103
103
|
- [Counter](https://github.com/Romashkaa/telekit/blob/main/docs/examples/counter.md)
|
|
104
|
-
- [Quiz (
|
|
104
|
+
- [Quiz (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/quiz.md)
|
|
105
|
+
- [Hotel (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/complete_hotel.md)
|
|
105
106
|
- [More...](https://github.com/Romashkaa/telekit/blob/main/docs/examples/examples.md)
|
|
106
107
|
|
|
107
108
|
## Overview
|
|
@@ -378,49 +379,86 @@ It tries to make Telegram bot development easier.
|
|
|
378
379
|
|
|
379
380
|
---
|
|
380
381
|
|
|
381
|
-
# Changes in version 2.
|
|
382
|
+
# Changes in version 2.3.0b1
|
|
382
383
|
|
|
383
|
-
##
|
|
384
|
+
## Traits
|
|
384
385
|
|
|
385
|
-
| **Name**
|
|
386
|
-
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
| `InvokeButton` | A callback button that calls the object method. |
|
|
386
|
+
| **Name** | **Description** |
|
|
387
|
+
| -------------------------- | --------------------------------------------------------------------- |
|
|
388
|
+
| `TrackHandoffOrigin` | Tracks which handler transferred control to this one via `handoff()`. |
|
|
389
|
+
| `PaginatedChoice` | Adds a paginated inline keyboard for choosing from a list of items. |
|
|
390
390
|
|
|
391
|
-
|
|
391
|
+
### TrackHandoffOrigin
|
|
392
392
|
|
|
393
|
-
|
|
394
|
-
| ---------------------- | --------------------------------------------------------- |
|
|
395
|
-
| `bio` | Bio of the user or description of the chat. |
|
|
396
|
-
| `birthdate` | Birthdate of the user, if set and visible. |
|
|
397
|
-
| `description` | Description of the group or channel. |
|
|
398
|
-
| `mention` | `tg://user?id=` deep link, works even without a username. |
|
|
399
|
-
| `is_private` | Whether the message was sent in a private chat. |
|
|
400
|
-
| `is_group` | Whether the message was sent in a group. |
|
|
401
|
-
| `is_supergroup` | Whether the message was sent in a supergroup. |
|
|
402
|
-
| `is_channel` | Whether the message was sent in a channel. |
|
|
403
|
-
| `avatar` | File ID of the user's most recent profile photo. |
|
|
404
|
-
| `profile_photos_count` | Total number of profile photos the user has set. |
|
|
393
|
+
`TrackHandoffOrigin` adds three members to any handler that inherits it:
|
|
405
394
|
|
|
406
|
-
|
|
407
|
-
|
|
395
|
+
| **Member** | **Description** |
|
|
396
|
+
| ------------------- | -------------------------------------------------------------------------------- |
|
|
397
|
+
| `handoff_origin` | The handler instance that handed off to this one, or `None` if invoked directly. |
|
|
398
|
+
| `is_handed_off` | `True` if this handler was reached via `handoff()`, `False` otherwise. |
|
|
399
|
+
| `handoff_back()` | Transfer control back to the origin handler via `self.handoff_origin.handle()` |
|
|
400
|
+
| `handoff_back_or(handler)` | Like `handoff_back()`, but falls back to `handler` on fail. |
|
|
408
401
|
|
|
409
|
-
|
|
402
|
+
Set `TRACK_HANDOFF_ORIGIN = False` on any subclass to opt out of tracking.
|
|
410
403
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
404
|
+
```python
|
|
405
|
+
class MyHandler(TrackHandoffOrigin, telekit.Handler):
|
|
406
|
+
|
|
407
|
+
def handle(self):
|
|
408
|
+
...
|
|
409
|
+
self.chain.set_inline_keyboard({
|
|
410
|
+
"« Back": self.handoff_back_or(StartHandler)
|
|
411
|
+
})
|
|
412
|
+
self.chain.edit()
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### PaginatedChoice
|
|
419
416
|
|
|
420
|
-
|
|
417
|
+
Renders a paginated inline keyboard from any dict or iterable.
|
|
418
|
+
Navigation buttons (`« Back`, `Next »`) are added automatically.
|
|
421
419
|
|
|
422
|
-
|
|
420
|
+
| **Member** | **Description** |
|
|
421
|
+
| -------------------- | ---------------------------------------------------------------------------- |
|
|
422
|
+
| `paginated_choice(choices, on_choice, on_update, row_width)` | Display a paginated choice keyboard. |
|
|
423
423
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
424
|
+
```python
|
|
425
|
+
self.chain.sender.set_title("🔤 What is your initial?")
|
|
426
|
+
self.chain.sender.set_message("Pick the first letter of your name")
|
|
427
|
+
self.chain.sender.set_remove_text(False)
|
|
428
|
+
|
|
429
|
+
self.paginated_choice(
|
|
430
|
+
choices="ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
431
|
+
on_choice=self.handle_letter,
|
|
432
|
+
row_width=5
|
|
433
|
+
)
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
<details>
|
|
437
|
+
<summary>(Click to see the result)</summary>
|
|
438
|
+
<table>
|
|
439
|
+
<tr>
|
|
440
|
+
<td><img src="https://github.com/Romashkaa/telekit/blob/main/docs/images/paginated_choice.png?raw=true" alt="Example" width="300"></td>
|
|
441
|
+
</tr>
|
|
442
|
+
</table>
|
|
443
|
+
</details>
|
|
444
|
+
|
|
445
|
+
`choices` accepts a `dict[str, T]`, or any `Iterable[T]`.
|
|
446
|
+
If only one item is present, `on_choice` is called immediately without rendering a keyboard.
|
|
447
|
+
|
|
448
|
+
## Utils
|
|
449
|
+
|
|
450
|
+
| **Name** | **Description** |
|
|
451
|
+
| ---------------- | ------------------------------------------------------------------ |
|
|
452
|
+
| `load_env` | Load all key-value pairs from a `.env` file into a dictionary. |
|
|
453
|
+
| `read_env_var` | Read a single variable by name from a `.env` file. |
|
|
454
|
+
|
|
455
|
+
- `read_token` and `read_canvas_path` now support reading from `.env` files.
|
|
456
|
+
Pass `".env"` to use the default key, or `".env:KEY"` to specify a custom one:
|
|
457
|
+
|
|
458
|
+
```python
|
|
459
|
+
read_token(".env") # reads TOKEN
|
|
460
|
+
read_token(".env:BOT_TOKEN") # reads BOT_TOKEN
|
|
461
|
+
|
|
462
|
+
read_canvas_path(".env") # reads CANVAS_PATH
|
|
463
|
+
read_canvas_path(".env:MY_CANVAS") # reads MY_CANVAS
|
|
464
|
+
```
|
|
@@ -65,7 +65,8 @@ Even in its beta stage, Telekit accelerates bot development, offering typed comm
|
|
|
65
65
|
- [Dialogue](https://github.com/Romashkaa/telekit/blob/main/docs/examples/dialogue.md)
|
|
66
66
|
- [Risk Game](https://github.com/Romashkaa/telekit/blob/main/docs/examples/risk_game.md)
|
|
67
67
|
- [Counter](https://github.com/Romashkaa/telekit/blob/main/docs/examples/counter.md)
|
|
68
|
-
- [Quiz (
|
|
68
|
+
- [Quiz (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/quiz.md)
|
|
69
|
+
- [Hotel (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/complete_hotel.md)
|
|
69
70
|
- [More...](https://github.com/Romashkaa/telekit/blob/main/docs/examples/examples.md)
|
|
70
71
|
|
|
71
72
|
## Overview
|
|
@@ -35,12 +35,14 @@ from . import parameters
|
|
|
35
35
|
from . import inline_buttons
|
|
36
36
|
from . import dices
|
|
37
37
|
from . import utils
|
|
38
|
+
from . import traits
|
|
38
39
|
|
|
39
40
|
Styles = styles.Styles
|
|
40
41
|
|
|
41
42
|
from ._version import __version__
|
|
42
43
|
|
|
43
44
|
__all__ = [
|
|
45
|
+
"traits",
|
|
44
46
|
"utils",
|
|
45
47
|
"senders",
|
|
46
48
|
"types",
|
|
@@ -225,9 +225,16 @@ class Handler:
|
|
|
225
225
|
|
|
226
226
|
handler_instance = handler(self.message)
|
|
227
227
|
handler_instance.chain._set_previous_message(self.chain.get_previous_message())
|
|
228
|
+
handler_instance._on_handoff(self)
|
|
228
229
|
|
|
229
230
|
return handler_instance
|
|
230
231
|
|
|
232
|
+
|
|
233
|
+
def _on_handoff(self, origin: "Handler") -> None:
|
|
234
|
+
"""Called when this handler is reached via handoff(). Override to customize."""
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
|
|
231
238
|
def freeze(self, func, *args):
|
|
232
239
|
"""
|
|
233
240
|
Return a zero-argument callback that invokes the given function
|
|
@@ -44,8 +44,12 @@ class EntryHandler(telekit.Handler):
|
|
|
44
44
|
# Handling Logic
|
|
45
45
|
# ------------------------------------------
|
|
46
46
|
|
|
47
|
+
def __init__(self, *args, **kwargs):
|
|
48
|
+
super().__init__(*args, **kwargs)
|
|
49
|
+
|
|
50
|
+
self._user_data = UserData(self.user.id)
|
|
51
|
+
|
|
47
52
|
def handle(self) -> None:
|
|
48
|
-
self._user_data = UserData(self.message.chat.id)
|
|
49
53
|
self.chain.disable_timeout_warnings()
|
|
50
54
|
self.entry_name()
|
|
51
55
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
from telebot.types import Message
|
|
2
1
|
from telekit.types import (
|
|
3
2
|
TextDocument, CopyTextButton, ParseMode
|
|
4
3
|
)
|
|
5
4
|
from telekit.styles import Quote, Escape
|
|
6
5
|
import telekit
|
|
7
6
|
|
|
8
|
-
class TextDocumentHandler(telekit.Handler):
|
|
7
|
+
class TextDocumentHandler(telekit.traits.TrackHandoffOrigin, telekit.Handler):
|
|
9
8
|
|
|
10
9
|
@classmethod
|
|
11
10
|
def init_handler(cls) -> None:
|
|
@@ -26,6 +25,11 @@ class TextDocumentHandler(telekit.Handler):
|
|
|
26
25
|
self.handle_text_document,
|
|
27
26
|
allowed_extensions=(".txt", ".py", ".md", ".html")
|
|
28
27
|
)
|
|
28
|
+
self.chain.set_inline_keyboard(
|
|
29
|
+
{
|
|
30
|
+
"« Back": self.handoff_back_or("StartHandler")
|
|
31
|
+
}
|
|
32
|
+
)
|
|
29
33
|
|
|
30
34
|
self.chain.disable_timeout_warnings()
|
|
31
35
|
self.chain.edit()
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
# along with Telekit. If not, see <https://www.gnu.org/licenses/>.
|
|
18
18
|
#
|
|
19
19
|
|
|
20
|
-
from typing import Any, Literal
|
|
20
|
+
from typing import Any, Literal, NoReturn
|
|
21
21
|
import textwrap
|
|
22
22
|
import io
|
|
23
23
|
|
|
@@ -27,7 +27,7 @@ from telebot.types import (
|
|
|
27
27
|
ReplyParameters, LinkPreviewOptions,
|
|
28
28
|
InputMediaPhoto, InputFile,
|
|
29
29
|
InputMediaAudio, InputMediaDocument,
|
|
30
|
-
InputMediaVideo,
|
|
30
|
+
InputMediaVideo, InputMediaAnimation
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
from telekit.styles import TextEntity, Escape, Raw, Group, Bold, Italic
|
|
@@ -90,6 +90,9 @@ class TempMessageStore:
|
|
|
90
90
|
# Base Sender
|
|
91
91
|
# ---------------------------------------------------------------------------------
|
|
92
92
|
|
|
93
|
+
class _FallbackToSend(Exception):
|
|
94
|
+
"""Raised when editing is not supported — silently falls back to delete + send."""
|
|
95
|
+
|
|
93
96
|
class BaseSender:
|
|
94
97
|
|
|
95
98
|
bot: TeleBot
|
|
@@ -109,6 +112,15 @@ class BaseSender:
|
|
|
109
112
|
|
|
110
113
|
parse_mode: Literal["html", "markdown"] | None
|
|
111
114
|
|
|
115
|
+
def _get_parse_mode(self):
|
|
116
|
+
match self.parse_mode:
|
|
117
|
+
case "html":
|
|
118
|
+
return "HTML"
|
|
119
|
+
case "markdown":
|
|
120
|
+
return "MarkdownV2"
|
|
121
|
+
case _:
|
|
122
|
+
return None
|
|
123
|
+
|
|
112
124
|
def __init__(
|
|
113
125
|
self,
|
|
114
126
|
chat_id: int,
|
|
@@ -872,6 +884,25 @@ class BaseSender:
|
|
|
872
884
|
# Internal send dispatcher
|
|
873
885
|
# --------------------------------------------------------
|
|
874
886
|
|
|
887
|
+
def _edit_or_send(self) -> tuple[Message | None, bool]:
|
|
888
|
+
if self.edit_message_id:
|
|
889
|
+
|
|
890
|
+
try:
|
|
891
|
+
return self._edit(), True
|
|
892
|
+
except _FallbackToSend:
|
|
893
|
+
# silently delete and resend
|
|
894
|
+
self._delete_message(self.edit_message_id)
|
|
895
|
+
except Exception as exception:
|
|
896
|
+
if "Bad Request: there is no text in the message to edit" not in str(exception):
|
|
897
|
+
library.warning(f"Failed to edit message {self.edit_message_id}, sending new one instead. Exception: {exception}")
|
|
898
|
+
self._delete_message(self.edit_message_id)
|
|
899
|
+
|
|
900
|
+
return self._send(), False
|
|
901
|
+
|
|
902
|
+
# --------------------------------------------------------
|
|
903
|
+
# Internal methods for sending messages
|
|
904
|
+
# --------------------------------------------------------
|
|
905
|
+
|
|
875
906
|
def _send(self) -> Message | None:
|
|
876
907
|
if self.photo:
|
|
877
908
|
message = self._send_photo()
|
|
@@ -982,40 +1013,33 @@ class BaseSender:
|
|
|
982
1013
|
)
|
|
983
1014
|
|
|
984
1015
|
# --------------------------------------------------------
|
|
985
|
-
# Internal methods for
|
|
1016
|
+
# Internal methods for editing messages
|
|
986
1017
|
# --------------------------------------------------------
|
|
987
1018
|
|
|
988
|
-
def _get_parse_mode(self):
|
|
989
|
-
match self.parse_mode:
|
|
990
|
-
case "html":
|
|
991
|
-
return "HTML"
|
|
992
|
-
case "markdown":
|
|
993
|
-
return "MarkdownV2"
|
|
994
|
-
case _:
|
|
995
|
-
return None
|
|
996
|
-
|
|
997
1019
|
def _edit(self) -> Message | None:
|
|
998
1020
|
if not self.edit_message_id:
|
|
999
1021
|
raise ValueError("edit_message_id is None: Unable to edit message without a valid message ID.")
|
|
1000
|
-
|
|
1001
|
-
configs = self._get_edit_params()
|
|
1002
|
-
|
|
1022
|
+
|
|
1003
1023
|
if self.photo:
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
)
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
)
|
|
1024
|
+
message = self._edit_photo()
|
|
1025
|
+
elif self.document:
|
|
1026
|
+
message = self._edit_document()
|
|
1027
|
+
elif self.video:
|
|
1028
|
+
message = self._edit_video()
|
|
1029
|
+
elif self.animation:
|
|
1030
|
+
message = self._edit_animation()
|
|
1031
|
+
elif self.audio:
|
|
1032
|
+
message = self._edit_audio()
|
|
1033
|
+
elif self.voice:
|
|
1034
|
+
message = self._edit_voice()
|
|
1035
|
+
elif self.video_note:
|
|
1036
|
+
message = self._edit_video_note()
|
|
1037
|
+
elif self.venue:
|
|
1038
|
+
message = self._edit_venue()
|
|
1039
|
+
elif self.media:
|
|
1040
|
+
message = self._edit_media()
|
|
1013
1041
|
else:
|
|
1014
|
-
message = self.
|
|
1015
|
-
text=self.text,
|
|
1016
|
-
parse_mode=self._get_parse_mode(),
|
|
1017
|
-
**configs
|
|
1018
|
-
)
|
|
1042
|
+
message = self._edit_text()
|
|
1019
1043
|
|
|
1020
1044
|
self._reset_after_send()
|
|
1021
1045
|
|
|
@@ -1023,17 +1047,84 @@ class BaseSender:
|
|
|
1023
1047
|
self.sent_message = message
|
|
1024
1048
|
return message
|
|
1025
1049
|
|
|
1026
|
-
def
|
|
1027
|
-
|
|
1050
|
+
def _edit_text(self) -> Message | bool:
|
|
1051
|
+
configs: dict = self._get_edit_params()
|
|
1052
|
+
return self.bot.edit_message_text(
|
|
1053
|
+
text=self.text,
|
|
1054
|
+
parse_mode=self._get_parse_mode(),
|
|
1055
|
+
link_preview_options=self.link_preview_options,
|
|
1056
|
+
**configs,
|
|
1057
|
+
)
|
|
1028
1058
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
return self.
|
|
1059
|
+
def _edit_document(self) -> Message | bool:
|
|
1060
|
+
configs: dict = self._get_edit_params()
|
|
1061
|
+
media = InputMediaDocument(
|
|
1062
|
+
media=self.document,
|
|
1063
|
+
caption=self.text,
|
|
1064
|
+
parse_mode=self._get_parse_mode(),
|
|
1065
|
+
)
|
|
1066
|
+
return self.bot.edit_message_media(media=media, **configs)
|
|
1067
|
+
|
|
1068
|
+
def _edit_video(self) -> Message | bool:
|
|
1069
|
+
configs: dict = self._get_edit_params()
|
|
1070
|
+
media = InputMediaVideo(
|
|
1071
|
+
media=self.video,
|
|
1072
|
+
caption=self.text,
|
|
1073
|
+
show_caption_above_media=self.show_caption_above_media,
|
|
1074
|
+
parse_mode=self._get_parse_mode(),
|
|
1075
|
+
)
|
|
1076
|
+
return self.bot.edit_message_media(media=media, **configs)
|
|
1077
|
+
|
|
1078
|
+
def _edit_animation(self) -> Message | bool:
|
|
1079
|
+
configs: dict = self._get_edit_params()
|
|
1080
|
+
media = InputMediaAnimation(
|
|
1081
|
+
media=self.animation,
|
|
1082
|
+
caption=self.text,
|
|
1083
|
+
show_caption_above_media=self.show_caption_above_media,
|
|
1084
|
+
parse_mode=self._get_parse_mode(),
|
|
1085
|
+
)
|
|
1086
|
+
return self.bot.edit_message_media(media=media, **configs)
|
|
1087
|
+
|
|
1088
|
+
def _edit_audio(self) -> Message | bool:
|
|
1089
|
+
configs: dict = self._get_edit_params()
|
|
1090
|
+
media = InputMediaAudio(
|
|
1091
|
+
media=self.audio,
|
|
1092
|
+
caption=self.text,
|
|
1093
|
+
performer=self.audio_performer,
|
|
1094
|
+
title=self.audio_title,
|
|
1095
|
+
parse_mode=self._get_parse_mode(),
|
|
1096
|
+
)
|
|
1097
|
+
return self.bot.edit_message_media(media=media, **configs)
|
|
1098
|
+
|
|
1099
|
+
def _edit_photo(self) -> Message | bool:
|
|
1100
|
+
configs: dict = self._get_edit_params()
|
|
1101
|
+
|
|
1102
|
+
media = InputMediaPhoto(
|
|
1103
|
+
media=self.photo,
|
|
1104
|
+
caption=self.text,
|
|
1105
|
+
show_caption_above_media=self.show_caption_above_media,
|
|
1106
|
+
parse_mode=self._get_parse_mode()
|
|
1107
|
+
)
|
|
1108
|
+
return self.bot.edit_message_media(
|
|
1109
|
+
media=media,
|
|
1110
|
+
**configs
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1113
|
+
def _edit_voice(self) -> NoReturn:
|
|
1114
|
+
# Voice messages cannot be edited.
|
|
1115
|
+
raise _FallbackToSend() # fallback to _edit_or_send
|
|
1116
|
+
|
|
1117
|
+
def _edit_video_note(self) -> NoReturn:
|
|
1118
|
+
# Video notes cannot be edited.
|
|
1119
|
+
raise _FallbackToSend() # fallback to _edit_or_send
|
|
1120
|
+
|
|
1121
|
+
def _edit_venue(self) -> NoReturn:
|
|
1122
|
+
# Venues cannot be edited.
|
|
1123
|
+
raise _FallbackToSend() # fallback to _edit_or_send
|
|
1124
|
+
|
|
1125
|
+
def _edit_media(self) -> NoReturn:
|
|
1126
|
+
# Media groups cannot be edited.
|
|
1127
|
+
raise _FallbackToSend() # fallback to _edit_or_send
|
|
1037
1128
|
|
|
1038
1129
|
# --------------------------------------------------------
|
|
1039
1130
|
# Methods for deleting messages
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from typing import Callable, Any, Iterable
|
|
2
|
+
|
|
3
|
+
import telekit
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class _NavButton:
|
|
8
|
+
start_index: int
|
|
9
|
+
|
|
10
|
+
class PaginatedChoice(telekit.Handler):
|
|
11
|
+
|
|
12
|
+
# ── entry point ───────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
def __init__(self, *args, **kwargs):
|
|
15
|
+
super().__init__(*args, **kwargs)
|
|
16
|
+
|
|
17
|
+
def paginated_choice[T](self, choices: dict[str, T] | Iterable[T], on_choice: Callable[[T], Any], on_update: Callable[[], Any] | None = None, row_width: int = 1, page_size: int = 10) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Display a paginated inline keyboard for choosing from a list of items.
|
|
20
|
+
|
|
21
|
+
Navigation buttons (``« Back``, ``Next »``) are added automatically
|
|
22
|
+
when the item count exceeds ``page_size``. If only one item is present,
|
|
23
|
+
``on_choice`` is called immediately without rendering a keyboard.
|
|
24
|
+
|
|
25
|
+
:param choices: Items to display. Accepts a ``dict[str, T]`` (label: value),
|
|
26
|
+
or any ``Iterable[T]``.
|
|
27
|
+
:type choices: ``dict[str, T] | Iterable[T]``
|
|
28
|
+
:param on_choice: Callback invoked with the selected value.
|
|
29
|
+
:type on_choice: ``Callable[[T], Any]``
|
|
30
|
+
:param on_update: Optional callback invoked before each page render.
|
|
31
|
+
Useful for updating entry text.
|
|
32
|
+
:type on_update: ``Callable[[], Any] | None``
|
|
33
|
+
:param row_width: Number of choice buttons per row. Defaults to ``1``.
|
|
34
|
+
:type row_width: ``int``
|
|
35
|
+
:param page_size: Maximum number of items per page. Defaults to ``10``.
|
|
36
|
+
:type page_size: ``int``
|
|
37
|
+
|
|
38
|
+
Example::
|
|
39
|
+
|
|
40
|
+
self.paginated_choice(
|
|
41
|
+
choices="ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
42
|
+
on_choice=self.handle_letter,
|
|
43
|
+
on_update=lambda: self.chain.set_entry_text(self.handle_letter),
|
|
44
|
+
row_width=5,
|
|
45
|
+
)
|
|
46
|
+
"""
|
|
47
|
+
if isinstance(choices, dict):
|
|
48
|
+
_choices: dict[str, T] = choices.copy() # pyright: ignore[reportAssignmentType]
|
|
49
|
+
else:
|
|
50
|
+
_choices: dict[str, T] = {str(c): c for c in choices}
|
|
51
|
+
|
|
52
|
+
if len(_choices) == 1:
|
|
53
|
+
on_choice(next(iter(_choices.values())))
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
self._paginated_choice(0, _choices, on_choice, on_update, row_width, page_size)
|
|
57
|
+
|
|
58
|
+
def _paginated_choice[T](self, start: int, choices: dict[str, T], on_choice: Callable[[T], Any], on_update: Callable[[], Any] | None, row_width: int, page_size: int) -> None:
|
|
59
|
+
total = len(choices)
|
|
60
|
+
|
|
61
|
+
page = start // page_size + 1
|
|
62
|
+
pages = (total + page_size - 1) // page_size
|
|
63
|
+
page_items = list(choices.items())[start : start + page_size]
|
|
64
|
+
|
|
65
|
+
if on_update is not None:
|
|
66
|
+
on_update()
|
|
67
|
+
|
|
68
|
+
keyboard: dict[str, T | _NavButton] = {}
|
|
69
|
+
keyboard.update(page_items)
|
|
70
|
+
|
|
71
|
+
# navigation row
|
|
72
|
+
has_back = start - page_size >= 0
|
|
73
|
+
has_next = start + page_size < total
|
|
74
|
+
|
|
75
|
+
nav: dict[str, T | _NavButton] = {}
|
|
76
|
+
|
|
77
|
+
if has_back:
|
|
78
|
+
nav["« Back"] = _NavButton(start - page_size)
|
|
79
|
+
if has_back and has_next:
|
|
80
|
+
nav[f"({page}/{pages})" ] = _NavButton(start)
|
|
81
|
+
if has_next:
|
|
82
|
+
nav["Next »"] = _NavButton(start + page_size)
|
|
83
|
+
|
|
84
|
+
# each choice on its own row, nav row at the end
|
|
85
|
+
choices_count = len(keyboard)
|
|
86
|
+
nav_count = len(nav)
|
|
87
|
+
|
|
88
|
+
full_rows = choices_count // row_width
|
|
89
|
+
remainder = choices_count % row_width
|
|
90
|
+
|
|
91
|
+
if remainder > 0:
|
|
92
|
+
_row_width = (*([row_width] * full_rows), remainder, nav_count)
|
|
93
|
+
else:
|
|
94
|
+
_row_width = (*([row_width] * full_rows), nav_count)
|
|
95
|
+
|
|
96
|
+
keyboard.update(nav)
|
|
97
|
+
|
|
98
|
+
def _on_choice(choice: T | _NavButton):
|
|
99
|
+
if isinstance(choice, _NavButton):
|
|
100
|
+
self._paginated_choice(choice.start_index, choices, on_choice, on_update, row_width, page_size)
|
|
101
|
+
else:
|
|
102
|
+
on_choice(choice)
|
|
103
|
+
|
|
104
|
+
self.chain.set_inline_choice(
|
|
105
|
+
_on_choice,
|
|
106
|
+
keyboard,
|
|
107
|
+
row_width=_row_width,
|
|
108
|
+
)
|
|
109
|
+
self.chain.edit()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
import telekit
|
|
4
|
+
|
|
5
|
+
class TrackHandoffOrigin(telekit.Handler):
|
|
6
|
+
|
|
7
|
+
TRACK_HANDOFF_ORIGIN: bool = True
|
|
8
|
+
|
|
9
|
+
def __init__(self, *args, **kwargs):
|
|
10
|
+
super().__init__(*args, **kwargs)
|
|
11
|
+
self.handoff_origin: telekit.Handler | None = None
|
|
12
|
+
|
|
13
|
+
def _on_handoff(self, origin: telekit.Handler) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Called when this handler is reached via :meth:`handoff`.
|
|
16
|
+
Override to customize handoff behaviour.
|
|
17
|
+
"""
|
|
18
|
+
if self.TRACK_HANDOFF_ORIGIN:
|
|
19
|
+
self.handoff_origin = origin
|
|
20
|
+
|
|
21
|
+
super()._on_handoff(origin)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def is_handed_off(self) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Whether this handler was reached via :meth:`handoff`.
|
|
27
|
+
|
|
28
|
+
- ``True`` if a previous handler transferred control here,
|
|
29
|
+
- ``False`` if this handler was invoked directly.
|
|
30
|
+
"""
|
|
31
|
+
return self.handoff_origin is not None
|
|
32
|
+
|
|
33
|
+
def handoff_back(self, *args, **kwargs):
|
|
34
|
+
"""
|
|
35
|
+
Transfer control back to the handler that handed off to this one.
|
|
36
|
+
|
|
37
|
+
Useful for implementing "« Back" buttons without hardcoding the
|
|
38
|
+
previous handler class:
|
|
39
|
+
|
|
40
|
+
.. code-block:: python
|
|
41
|
+
self.chain.set_inline_keyboard({
|
|
42
|
+
"« Back": self.handoff_back
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
Any positional or keyword arguments are forwarded directly to
|
|
46
|
+
``handoff_origin.handle()``:
|
|
47
|
+
|
|
48
|
+
.. code-block:: python
|
|
49
|
+
self.handoff_back("some_arg", key="value")
|
|
50
|
+
# equivalent to: handoff_origin.handle("some_arg", key="value")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
:raises RuntimeError: If this handler was not reached via :meth:`handoff`
|
|
54
|
+
(i.e. ``handoff_origin`` is ``None``).
|
|
55
|
+
"""
|
|
56
|
+
if self.handoff_origin is None:
|
|
57
|
+
raise RuntimeError(
|
|
58
|
+
f"{type(self).__name__}().handoff_back() called, but this handler "
|
|
59
|
+
"was not reached via handoff() - handoff_origin is None."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.handoff_origin.chain._set_previous_message(self.chain.get_previous_message())
|
|
63
|
+
self.handoff_origin.handle(*args, **kwargs)
|
|
64
|
+
|
|
65
|
+
def handoff_back_or(self, handler: type[telekit.Handler] | str) -> Callable[..., None]:
|
|
66
|
+
"""
|
|
67
|
+
Return a callable that transfers control back to the origin handler,
|
|
68
|
+
or falls back to ``handler`` if this handler was invoked directly.
|
|
69
|
+
|
|
70
|
+
Useful for ``« Back`` buttons in handlers that can be reached both
|
|
71
|
+
via :meth:`handoff` and directly:
|
|
72
|
+
|
|
73
|
+
.. code-block:: python
|
|
74
|
+
self.chain.set_inline_keyboard({
|
|
75
|
+
"« Back": self.handoff_back_or(StartHandler)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
Any positional or keyword arguments passed to the returned callable
|
|
79
|
+
are forwarded to ``handle()`` of whichever handler is invoked:
|
|
80
|
+
|
|
81
|
+
.. code-block:: python
|
|
82
|
+
back = self.handoff_back_or(StartHandler)
|
|
83
|
+
back("some_arg", key="value")
|
|
84
|
+
# if handed off: handoff_origin.handle("some_arg", key="value")
|
|
85
|
+
# if direct: StartHandler.handle("some_arg", key="value")
|
|
86
|
+
|
|
87
|
+
:param handler: Fallback handler class (or name) to transfer control to when
|
|
88
|
+
``handoff_origin`` is ``None``.
|
|
89
|
+
:type handler: ``type[Handler]`` | ``str``
|
|
90
|
+
:return: A zero-argument callable that performs the handoff.
|
|
91
|
+
:rtype: ``Callable[..., None]``
|
|
92
|
+
"""
|
|
93
|
+
def handoff_invoker(*args, **kwargs):
|
|
94
|
+
if self.handoff_origin is None:
|
|
95
|
+
self.handoff(handler).handle(*args, **kwargs)
|
|
96
|
+
else:
|
|
97
|
+
self.handoff_back(*args, **kwargs)
|
|
98
|
+
|
|
99
|
+
return handoff_invoker
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import re
|
|
2
3
|
|
|
3
4
|
from pathlib import Path
|
|
@@ -6,6 +7,10 @@ from typing import Literal
|
|
|
6
7
|
|
|
7
8
|
ROOT_DIR = Path(__file__).resolve().parent # telekit/
|
|
8
9
|
|
|
10
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
11
|
+
# Checks
|
|
12
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
13
|
+
|
|
9
14
|
def is_valid_callback_data(callback_data: str) -> bool:
|
|
10
15
|
"""
|
|
11
16
|
Check whether a callback data string is valid for Telegram inline buttons.
|
|
@@ -31,6 +36,9 @@ def is_valid_callback_data(callback_data: str) -> bool:
|
|
|
31
36
|
byte_size = len(callback_data.encode('utf-8'))
|
|
32
37
|
return 1 <= byte_size <= 64
|
|
33
38
|
|
|
39
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
40
|
+
# Formatting
|
|
41
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
34
42
|
|
|
35
43
|
def format_file_size(size: int, precision: int = 1) -> str:
|
|
36
44
|
"""
|
|
@@ -105,46 +113,140 @@ def format_file_size(size: int, precision: int = 1) -> str:
|
|
|
105
113
|
|
|
106
114
|
return f"{formatted} {units[index]}"
|
|
107
115
|
|
|
108
|
-
|
|
116
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
117
|
+
# Environment
|
|
118
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
119
|
+
|
|
120
|
+
def _split_env_path(path: str, default: str) -> tuple[str, str]:
|
|
121
|
+
if ":" in path:
|
|
122
|
+
path, name = path.split(":", 1)
|
|
123
|
+
else:
|
|
124
|
+
path, name = path, default
|
|
125
|
+
|
|
126
|
+
return path, name
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def load_env(path=".env") -> dict[str, str]:
|
|
130
|
+
"""
|
|
131
|
+
Load all key-value pairs from a ``.env`` file.
|
|
132
|
+
|
|
133
|
+
Lines starting with ``#`` and empty lines are ignored.
|
|
134
|
+
|
|
135
|
+
:param path: Path to the ``.env`` file. Defaults to ``".env"``.
|
|
136
|
+
:type path: ``str``
|
|
137
|
+
:return: Dictionary of all key-value pairs found in the file,
|
|
138
|
+
or an empty dict if the file does not exist.
|
|
139
|
+
:rtype: ``dict[str, str]``
|
|
140
|
+
"""
|
|
141
|
+
if not os.path.exists(path):
|
|
142
|
+
return {}
|
|
143
|
+
|
|
144
|
+
env: dict[str, str] = {}
|
|
145
|
+
|
|
146
|
+
with open(path, "r") as f:
|
|
147
|
+
for line in f:
|
|
148
|
+
line = line.strip()
|
|
149
|
+
if not line or line.startswith("#"):
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
key, value = line.split("=", 1)
|
|
153
|
+
env[key.strip()] = value.strip()
|
|
154
|
+
|
|
155
|
+
return env
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def read_envar(path: str, name: str) -> str:
|
|
159
|
+
"""
|
|
160
|
+
Read a single environment variable from a ``.env`` file.
|
|
161
|
+
|
|
162
|
+
:param path: Path to the ``.env`` file.
|
|
163
|
+
:type path: ``str``
|
|
164
|
+
:param name: Name of the key to read.
|
|
165
|
+
:type name: ``str``
|
|
166
|
+
:return: Value of the key.
|
|
167
|
+
:rtype: ``str``
|
|
168
|
+
:raises KeyError: If ``name`` is not found in the file.
|
|
109
169
|
"""
|
|
110
|
-
|
|
170
|
+
env: dict[str, str] = load_env(path)
|
|
111
171
|
|
|
112
|
-
|
|
113
|
-
|
|
172
|
+
if name not in env:
|
|
173
|
+
raise KeyError(f"{name} not found in {path}")
|
|
114
174
|
|
|
115
|
-
|
|
116
|
-
is ignored::
|
|
175
|
+
return env[name]
|
|
117
176
|
|
|
177
|
+
|
|
178
|
+
def read_token(path: str = "token.txt") -> str:
|
|
179
|
+
"""
|
|
180
|
+
Read the bot token from a file or ``.env``.
|
|
181
|
+
|
|
182
|
+
**Plain text file** (default)::
|
|
183
|
+
|
|
184
|
+
# token.txt
|
|
118
185
|
123456789:BotSecretToken Main production bot
|
|
119
186
|
987654321:AnotherToken Backup bot
|
|
120
187
|
|
|
121
|
-
|
|
188
|
+
Reads only the first line. Inline comments (everything after the first
|
|
189
|
+
whitespace) are ignored. Multiple tokens can be stored and swapped by
|
|
190
|
+
reordering lines.
|
|
191
|
+
|
|
192
|
+
**Environment file** (``.env``)::
|
|
193
|
+
|
|
194
|
+
read_token(".env") # reads the key named TOKEN
|
|
195
|
+
read_token(".env:TOKEN") # same, explicit key
|
|
196
|
+
read_token(".env:BOT_KEY") # reads a custom key
|
|
197
|
+
|
|
198
|
+
:param path: Path to a token file, or ``".env"`` / ``".env:KEY"`` for
|
|
199
|
+
environment files. Defaults to ``"token.txt"``.
|
|
122
200
|
:type path: ``str``
|
|
123
|
-
:return: Bot token string
|
|
201
|
+
:return: Bot token string.
|
|
124
202
|
:rtype: ``str``
|
|
203
|
+
:raises KeyError: If the key is not found in the ``.env`` file.
|
|
204
|
+
:raises FileNotFoundError: If the file does not exist.
|
|
125
205
|
"""
|
|
206
|
+
if path.endswith(".env") or ".env:" in path:
|
|
207
|
+
return read_envar(*_split_env_path(path, "TOKEN"))
|
|
208
|
+
|
|
126
209
|
with open(path) as f:
|
|
127
210
|
first_line: str = f.readline().strip()
|
|
128
211
|
token, *_ = first_line.split()
|
|
129
212
|
return token
|
|
213
|
+
|
|
130
214
|
|
|
131
215
|
def read_canvas_path(path: str = "canvas_path.txt") -> str:
|
|
132
216
|
"""
|
|
133
|
-
Read the ``.canvas`` file path from a file
|
|
217
|
+
Read the ``.canvas`` file path from a file or ``.env``.
|
|
218
|
+
|
|
219
|
+
**Plain text file** (default)::
|
|
134
220
|
|
|
135
|
-
|
|
136
|
-
|
|
221
|
+
# canvas_path.txt
|
|
222
|
+
/home/user/project/main.canvas Production canvas
|
|
223
|
+
/home/user/project/test.canvas Test canvas
|
|
137
224
|
|
|
138
|
-
|
|
225
|
+
Reads only the first line. Multiple paths can be stored and swapped by
|
|
226
|
+
reordering lines.
|
|
227
|
+
|
|
228
|
+
**Environment file** (``.env``)::
|
|
229
|
+
|
|
230
|
+
read_canvas_path(".env") # reads the key named CANVAS_PATH
|
|
231
|
+
read_canvas_path(".env:CANVAS_PATH") # same, explicit key
|
|
232
|
+
read_canvas_path(".env:MY_CANVAS") # reads a custom key
|
|
233
|
+
|
|
234
|
+
:param path: Path to a canvas path file, or ``".env"`` / ``".env:KEY"`` for
|
|
235
|
+
environment files. Defaults to ``"canvas_path.txt"``.
|
|
139
236
|
:type path: ``str``
|
|
140
|
-
:return: Path to the ``.canvas`` file
|
|
237
|
+
:return: Path to the ``.canvas`` file.
|
|
141
238
|
:rtype: ``str``
|
|
239
|
+
:raises KeyError: If the key is not found in the ``.env`` file.
|
|
240
|
+
:raises FileNotFoundError: If the file does not exist.
|
|
142
241
|
"""
|
|
242
|
+
if path.endswith(".env") or ".env:" in path:
|
|
243
|
+
return read_envar(*_split_env_path(path, "CANVAS_PATH"))
|
|
244
|
+
|
|
143
245
|
with open(path) as f:
|
|
144
246
|
return f.readline().strip()
|
|
145
247
|
|
|
146
248
|
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
147
|
-
# Link
|
|
249
|
+
# Link Generating
|
|
148
250
|
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
149
251
|
|
|
150
252
|
def make_bot_link(botname: str, start: str | None = None) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: telekit
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0b1
|
|
4
4
|
Summary: Declarative, developer-friendly library for building Telegram bots
|
|
5
5
|
Home-page: https://github.com/Romashkaa/telekit
|
|
6
6
|
Author: romashka
|
|
@@ -101,7 +101,8 @@ Even in its beta stage, Telekit accelerates bot development, offering typed comm
|
|
|
101
101
|
- [Dialogue](https://github.com/Romashkaa/telekit/blob/main/docs/examples/dialogue.md)
|
|
102
102
|
- [Risk Game](https://github.com/Romashkaa/telekit/blob/main/docs/examples/risk_game.md)
|
|
103
103
|
- [Counter](https://github.com/Romashkaa/telekit/blob/main/docs/examples/counter.md)
|
|
104
|
-
- [Quiz (
|
|
104
|
+
- [Quiz (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/quiz.md)
|
|
105
|
+
- [Hotel (DSL)](https://github.com/Romashkaa/telekit/blob/main/docs/examples/complete_hotel.md)
|
|
105
106
|
- [More...](https://github.com/Romashkaa/telekit/blob/main/docs/examples/examples.md)
|
|
106
107
|
|
|
107
108
|
## Overview
|
|
@@ -378,49 +379,86 @@ It tries to make Telegram bot development easier.
|
|
|
378
379
|
|
|
379
380
|
---
|
|
380
381
|
|
|
381
|
-
# Changes in version 2.
|
|
382
|
+
# Changes in version 2.3.0b1
|
|
382
383
|
|
|
383
|
-
##
|
|
384
|
+
## Traits
|
|
384
385
|
|
|
385
|
-
| **Name**
|
|
386
|
-
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
| `InvokeButton` | A callback button that calls the object method. |
|
|
386
|
+
| **Name** | **Description** |
|
|
387
|
+
| -------------------------- | --------------------------------------------------------------------- |
|
|
388
|
+
| `TrackHandoffOrigin` | Tracks which handler transferred control to this one via `handoff()`. |
|
|
389
|
+
| `PaginatedChoice` | Adds a paginated inline keyboard for choosing from a list of items. |
|
|
390
390
|
|
|
391
|
-
|
|
391
|
+
### TrackHandoffOrigin
|
|
392
392
|
|
|
393
|
-
|
|
394
|
-
| ---------------------- | --------------------------------------------------------- |
|
|
395
|
-
| `bio` | Bio of the user or description of the chat. |
|
|
396
|
-
| `birthdate` | Birthdate of the user, if set and visible. |
|
|
397
|
-
| `description` | Description of the group or channel. |
|
|
398
|
-
| `mention` | `tg://user?id=` deep link, works even without a username. |
|
|
399
|
-
| `is_private` | Whether the message was sent in a private chat. |
|
|
400
|
-
| `is_group` | Whether the message was sent in a group. |
|
|
401
|
-
| `is_supergroup` | Whether the message was sent in a supergroup. |
|
|
402
|
-
| `is_channel` | Whether the message was sent in a channel. |
|
|
403
|
-
| `avatar` | File ID of the user's most recent profile photo. |
|
|
404
|
-
| `profile_photos_count` | Total number of profile photos the user has set. |
|
|
393
|
+
`TrackHandoffOrigin` adds three members to any handler that inherits it:
|
|
405
394
|
|
|
406
|
-
|
|
407
|
-
|
|
395
|
+
| **Member** | **Description** |
|
|
396
|
+
| ------------------- | -------------------------------------------------------------------------------- |
|
|
397
|
+
| `handoff_origin` | The handler instance that handed off to this one, or `None` if invoked directly. |
|
|
398
|
+
| `is_handed_off` | `True` if this handler was reached via `handoff()`, `False` otherwise. |
|
|
399
|
+
| `handoff_back()` | Transfer control back to the origin handler via `self.handoff_origin.handle()` |
|
|
400
|
+
| `handoff_back_or(handler)` | Like `handoff_back()`, but falls back to `handler` on fail. |
|
|
408
401
|
|
|
409
|
-
|
|
402
|
+
Set `TRACK_HANDOFF_ORIGIN = False` on any subclass to opt out of tracking.
|
|
410
403
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
404
|
+
```python
|
|
405
|
+
class MyHandler(TrackHandoffOrigin, telekit.Handler):
|
|
406
|
+
|
|
407
|
+
def handle(self):
|
|
408
|
+
...
|
|
409
|
+
self.chain.set_inline_keyboard({
|
|
410
|
+
"« Back": self.handoff_back_or(StartHandler)
|
|
411
|
+
})
|
|
412
|
+
self.chain.edit()
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### PaginatedChoice
|
|
419
416
|
|
|
420
|
-
|
|
417
|
+
Renders a paginated inline keyboard from any dict or iterable.
|
|
418
|
+
Navigation buttons (`« Back`, `Next »`) are added automatically.
|
|
421
419
|
|
|
422
|
-
|
|
420
|
+
| **Member** | **Description** |
|
|
421
|
+
| -------------------- | ---------------------------------------------------------------------------- |
|
|
422
|
+
| `paginated_choice(choices, on_choice, on_update, row_width)` | Display a paginated choice keyboard. |
|
|
423
423
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
424
|
+
```python
|
|
425
|
+
self.chain.sender.set_title("🔤 What is your initial?")
|
|
426
|
+
self.chain.sender.set_message("Pick the first letter of your name")
|
|
427
|
+
self.chain.sender.set_remove_text(False)
|
|
428
|
+
|
|
429
|
+
self.paginated_choice(
|
|
430
|
+
choices="ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
431
|
+
on_choice=self.handle_letter,
|
|
432
|
+
row_width=5
|
|
433
|
+
)
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
<details>
|
|
437
|
+
<summary>(Click to see the result)</summary>
|
|
438
|
+
<table>
|
|
439
|
+
<tr>
|
|
440
|
+
<td><img src="https://github.com/Romashkaa/telekit/blob/main/docs/images/paginated_choice.png?raw=true" alt="Example" width="300"></td>
|
|
441
|
+
</tr>
|
|
442
|
+
</table>
|
|
443
|
+
</details>
|
|
444
|
+
|
|
445
|
+
`choices` accepts a `dict[str, T]`, or any `Iterable[T]`.
|
|
446
|
+
If only one item is present, `on_choice` is called immediately without rendering a keyboard.
|
|
447
|
+
|
|
448
|
+
## Utils
|
|
449
|
+
|
|
450
|
+
| **Name** | **Description** |
|
|
451
|
+
| ---------------- | ------------------------------------------------------------------ |
|
|
452
|
+
| `load_env` | Load all key-value pairs from a `.env` file into a dictionary. |
|
|
453
|
+
| `read_env_var` | Read a single variable by name from a `.env` file. |
|
|
454
|
+
|
|
455
|
+
- `read_token` and `read_canvas_path` now support reading from `.env` files.
|
|
456
|
+
Pass `".env"` to use the default key, or `".env:KEY"` to specify a custom one:
|
|
457
|
+
|
|
458
|
+
```python
|
|
459
|
+
read_token(".env") # reads TOKEN
|
|
460
|
+
read_token(".env:BOT_TOKEN") # reads BOT_TOKEN
|
|
461
|
+
|
|
462
|
+
read_canvas_path(".env") # reads CANVAS_PATH
|
|
463
|
+
read_canvas_path(".env:MY_CANVAS") # reads MY_CANVAS
|
|
464
|
+
```
|
|
@@ -66,4 +66,7 @@ telekit/example/example_handlers/qr.py
|
|
|
66
66
|
telekit/example/example_handlers/quiz.py
|
|
67
67
|
telekit/example/example_handlers/spells.py
|
|
68
68
|
telekit/example/example_handlers/start.py
|
|
69
|
-
telekit/example/example_handlers/text_document.py
|
|
69
|
+
telekit/example/example_handlers/text_document.py
|
|
70
|
+
telekit/traits/__init__.py
|
|
71
|
+
telekit/traits/paginated_choice.py
|
|
72
|
+
telekit/traits/track_handoff_origin.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|