telekit 2.3.0b2__tar.gz → 2.3.0b3__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.3.0b2/telekit.egg-info → telekit-2.3.0b3}/PKG-INFO +3 -145
- {telekit-2.3.0b2 → telekit-2.3.0b3}/setup.py +1 -2
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/__init__.py +4 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_chain.py +2 -1
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_chain_base.py +3 -3
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_init.py +1 -1
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_inline_buttons.py +11 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_state.py +15 -2
- telekit-2.3.0b3/telekit/_trait.py +10 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_version.py +1 -1
- telekit-2.3.0b3/telekit/debug.py +8 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_handlers/__init__.py +2 -1
- telekit-2.3.0b3/telekit/example/example_handlers/calendar.py +223 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_handlers/start.py +2 -1
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/senders.py +3 -1
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/server.py +4 -2
- telekit-2.3.0b3/telekit/traits/__init__.py +3 -0
- telekit-2.3.0b3/telekit/traits/calendar_pick.py +807 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/traits/paginated_choice.py +1 -1
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/traits/track_handoff_origin.py +1 -1
- {telekit-2.3.0b2 → telekit-2.3.0b3/telekit.egg-info}/PKG-INFO +3 -145
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit.egg-info/SOURCES.txt +4 -0
- telekit-2.3.0b2/telekit/traits/__init__.py +0 -2
- {telekit-2.3.0b2 → telekit-2.3.0b3}/LICENSE +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/README.md +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/setup.cfg +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_buildtext/__init__.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_buildtext/formatter.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_buildtext/styles.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_callback_query_handler.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_chain_entry_logic.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_chain_inline_keyboards_logic.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_chapters/__init__.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_chapters/chapters.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_handler.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_input_handler.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_logger.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_on.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_snapvault/__init__.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_snapvault/snapcode.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_snapvault/snapvault.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_telekit_dsl/__init__.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_telekit_dsl/mixin.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_telekit_dsl/parser/__init__.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_telekit_dsl/parser/builder.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_telekit_dsl/parser/canvas_parser.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_telekit_dsl/parser/lexer.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_telekit_dsl/parser/nodes.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_telekit_dsl/parser/parser.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_telekit_dsl/parser/token.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_telekit_dsl/telekit_dsl.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_telekit_dsl/telekit_orm.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_timeout.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/_user.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/dices.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/__init__.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_handlers/complete_hotel.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_handlers/counter.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_handlers/dsl.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_handlers/entry.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_handlers/faq.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_handlers/hotel.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_handlers/on_text.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_handlers/pages.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_handlers/pyapi.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_handlers/qr.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_handlers/quiz.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_handlers/spells.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_handlers/text_document.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/example/example_server.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/inline_buttons.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/parameters.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/styles.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/types.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit/utils.py +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit.egg-info/dependency_links.txt +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/telekit.egg-info/requires.txt +0 -0
- {telekit-2.3.0b2 → telekit-2.3.0b3}/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.
|
|
3
|
+
Version: 2.3.0b3
|
|
4
4
|
Summary: Declarative, developer-friendly library for building Telegram bots
|
|
5
5
|
Home-page: https://github.com/Romashkaa/telekit
|
|
6
6
|
Author: romashka
|
|
@@ -379,148 +379,6 @@ It tries to make Telegram bot development easier.
|
|
|
379
379
|
|
|
380
380
|
---
|
|
381
381
|
|
|
382
|
-
# Changes in version 2.3.
|
|
382
|
+
# Changes in version 2.3.0b3
|
|
383
383
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
| **Name** | **Description** |
|
|
387
|
-
| ---------------- | ------------------------------------------------------------------------------- |
|
|
388
|
-
| `StaticButton` | A non-interactive inline button that performs no action when pressed. |
|
|
389
|
-
| `AnswerButton` | A button that responds to a callback query with a notification or alert without executing custom logic. |
|
|
390
|
-
|
|
391
|
-
- `AnswerButton` and its subclasses (`AlertButton`, `NotificationButton`) now support the `persistent` parameter (Defauts to `True`):
|
|
392
|
-
- `True` — non-blocking hint (does not affect further interactions)
|
|
393
|
-
- `False` — terminates interaction after click
|
|
394
|
-
|
|
395
|
-
## Traits
|
|
396
|
-
|
|
397
|
-
| **Name** | **Description** |
|
|
398
|
-
| -------------------------- | --------------------------------------------------------------------- |
|
|
399
|
-
| `TrackHandoffOrigin` | Tracks which handler transferred control to this one via `handoff()`. |
|
|
400
|
-
| `PaginatedChoice` | Adds a paginated inline keyboard for choosing from a list of items. |
|
|
401
|
-
|
|
402
|
-
### TrackHandoffOrigin
|
|
403
|
-
|
|
404
|
-
`TrackHandoffOrigin` adds three members to any handler that inherits it:
|
|
405
|
-
|
|
406
|
-
| **Member** | **Description** |
|
|
407
|
-
| ------------------- | -------------------------------------------------------------------------------- |
|
|
408
|
-
| `handoff_origin` | The handler instance that handed off to this one, or `None` if invoked directly. |
|
|
409
|
-
| `is_handed_off` | `True` if this handler was reached via `handoff()`, `False` otherwise. |
|
|
410
|
-
| `handoff_back()` | Transfer control back to the origin handler via `self.handoff_origin.handle()` |
|
|
411
|
-
| `handoff_back_or(handler)` | Like `handoff_back()`, but falls back to `handler` on fail. |
|
|
412
|
-
|
|
413
|
-
Set `TRACK_HANDOFF_ORIGIN = False` on any subclass to opt out of tracking.
|
|
414
|
-
|
|
415
|
-
```python
|
|
416
|
-
class MyHandler(TrackHandoffOrigin, telekit.Handler):
|
|
417
|
-
|
|
418
|
-
def handle(self):
|
|
419
|
-
...
|
|
420
|
-
self.chain.set_inline_keyboard({
|
|
421
|
-
"« Back": self.handoff_back_or(StartHandler)
|
|
422
|
-
})
|
|
423
|
-
self.chain.edit()
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
### PaginatedChoice
|
|
427
|
-
|
|
428
|
-
Renders a paginated inline keyboard from any dict or iterable.
|
|
429
|
-
Navigation buttons (`« Back`, `Next »`) are added automatically.
|
|
430
|
-
|
|
431
|
-
| **Member** | **Description** |
|
|
432
|
-
| -------------------- | ---------------------------------------------------------------------------- |
|
|
433
|
-
| `paginated_choice(choices, on_choice, ...)` | Display a paginated choice keyboard. |
|
|
434
|
-
| `PAGINATED_CHOICE_BACK_LABEL` | Label for the back navigation button. Defaults to `« Back`. |
|
|
435
|
-
| `PAGINATED_CHOICE_NEXT_LABEL` | Label for the next navigation button. Defaults to `Next »`. |
|
|
436
|
-
| `PAGINATED_CHOICE_PAGE_LABEL` | Label template for the page indicator button. Supports `{page}` and `{pages}` placeholders. Set to `None` to hide. Defaults to `{page} / {pages}`. |
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
```python
|
|
440
|
-
self.chain.sender.set_title("🔤 What is your initial?")
|
|
441
|
-
self.chain.sender.set_message("Pick the first letter of your name")
|
|
442
|
-
self.chain.sender.set_remove_text(False)
|
|
443
|
-
|
|
444
|
-
self.paginated_choice(
|
|
445
|
-
choices="ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
446
|
-
on_choice=self.handle_letter,
|
|
447
|
-
row_width=5
|
|
448
|
-
)
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
Override labels per handler to localise or restyle navigation:
|
|
452
|
-
|
|
453
|
-
```python
|
|
454
|
-
class MyHandler(PaginatedChoice, telekit.Handler):
|
|
455
|
-
PAGINATED_CHOICE_BACK_LABEL = "⬅️ Назад"
|
|
456
|
-
PAGINATED_CHOICE_NEXT_LABEL = "Далі ➡️"
|
|
457
|
-
PAGINATED_CHOICE_PAGE_LABEL = None # hide page indicator
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
<details>
|
|
461
|
-
<summary>(Click to see the result)</summary>
|
|
462
|
-
<table>
|
|
463
|
-
<tr>
|
|
464
|
-
<td><img src="https://github.com/Romashkaa/telekit/blob/main/docs/images/paginated_choice.png?raw=true" alt="Example" width="300"></td>
|
|
465
|
-
</tr>
|
|
466
|
-
</table>
|
|
467
|
-
</details>
|
|
468
|
-
|
|
469
|
-
`choices` accepts a `dict[str, T]`, or any `Iterable[T]`.
|
|
470
|
-
If only one item is present, `on_choice` is called immediately without rendering a keyboard.
|
|
471
|
-
|
|
472
|
-
## Utils
|
|
473
|
-
|
|
474
|
-
| **Name** | **Description** |
|
|
475
|
-
| ------------------ | ------------------------------------------------------------------ |
|
|
476
|
-
| `load_env` | Load all key-value pairs from a `.env` file into a dictionary. |
|
|
477
|
-
| `read_env_var` | Read a single variable by name from a `.env` file. |
|
|
478
|
-
| `compose_keyboard` | Merge multiple button groups into a single inline keyboard with computed `row_width`. |
|
|
479
|
-
|
|
480
|
-
### Environment Utils
|
|
481
|
-
|
|
482
|
-
- `read_token` and `read_canvas_path` now support reading from `.env` files.
|
|
483
|
-
Pass `".env"` to use the default key, or `".env:KEY"` to specify a custom one:
|
|
484
|
-
|
|
485
|
-
```python
|
|
486
|
-
read_token(".env") # reads TOKEN
|
|
487
|
-
read_token(".env:BOT_TOKEN") # reads BOT_TOKEN
|
|
488
|
-
|
|
489
|
-
read_canvas_path(".env") # reads CANVAS_PATH
|
|
490
|
-
read_canvas_path(".env:MY_CANVAS") # reads MY_CANVAS
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
### Inline Keyboard Utils
|
|
494
|
-
|
|
495
|
-
`compose_keyboard` combines multiple button groups into a single keyboard and automatically calculates `row_width` for each group.
|
|
496
|
-
|
|
497
|
-
Each group is laid out independently using its corresponding width from `widths`.
|
|
498
|
-
A width of `-1` means "all buttons in one row" (i.e. ``len(group)``).
|
|
499
|
-
|
|
500
|
-
| **Param** | **Type** | **Description** |
|
|
501
|
-
|-----------|------------------|---------------------------------------------|
|
|
502
|
-
| `groups` | `dict[str, Any]` | One or more button groups |
|
|
503
|
-
| `widths` | `Iterable[int]` | Row width per group or single value for all |
|
|
504
|
-
|
|
505
|
-
| **Returns** | **Type** |
|
|
506
|
-
|------------|----------|
|
|
507
|
-
| `keyboard` | `dict[str, Any]` |
|
|
508
|
-
| `row_width` | `tuple[int, ...]` |
|
|
509
|
-
|
|
510
|
-
```py
|
|
511
|
-
# row_width → (1, 3, 3, 3, 2)
|
|
512
|
-
# layout:
|
|
513
|
-
# | 🆕 Create |
|
|
514
|
-
# | 1 | 2 | 3 |
|
|
515
|
-
# | 4 | 5 | 6 |
|
|
516
|
-
# | 7 | 8 | 9 |
|
|
517
|
-
# | « Back | Next » |
|
|
518
|
-
|
|
519
|
-
keyboard, row_width = compose_keyboard(
|
|
520
|
-
{"🆕 Create": "create"},
|
|
521
|
-
{str(n): str(n) for n in range(1, 10)},
|
|
522
|
-
{"« Back": "back", "Next »": "next"},
|
|
523
|
-
widths=(-1, 3, -1), # or (1, 3, 2)
|
|
524
|
-
)
|
|
525
|
-
self.chain.set_inline_choice(keyboard, row_width)
|
|
526
|
-
```
|
|
384
|
+
[View changelog on GitHub](https://github.com/Romashkaa/telekit/blob/main/CHANGELOG.md)
|
|
@@ -28,8 +28,7 @@ def readme():
|
|
|
28
28
|
return f.read()
|
|
29
29
|
|
|
30
30
|
def changelog():
|
|
31
|
-
|
|
32
|
-
return f.read()
|
|
31
|
+
return f"[View changelog on GitHub](https://github.com/Romashkaa/telekit/blob/main/CHANGELOG.md)"
|
|
33
32
|
|
|
34
33
|
def install_requires():
|
|
35
34
|
with open('telekit/requirements.txt', 'r') as f:
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
#
|
|
19
19
|
|
|
20
20
|
from ._handler import Handler
|
|
21
|
+
from ._trait import Trait
|
|
21
22
|
from ._chain import Chain
|
|
22
23
|
from ._callback_query_handler import CallbackQueryHandler
|
|
23
24
|
from .server import Server, example
|
|
@@ -36,6 +37,7 @@ from . import inline_buttons
|
|
|
36
37
|
from . import dices
|
|
37
38
|
from . import utils
|
|
38
39
|
from . import traits
|
|
40
|
+
from . import debug
|
|
39
41
|
|
|
40
42
|
Styles = styles.Styles
|
|
41
43
|
|
|
@@ -56,6 +58,7 @@ __all__ = [
|
|
|
56
58
|
|
|
57
59
|
"Server",
|
|
58
60
|
"Chain",
|
|
61
|
+
"Trait",
|
|
59
62
|
"Handler",
|
|
60
63
|
"CallbackQueryHandler",
|
|
61
64
|
|
|
@@ -66,4 +69,5 @@ __all__ = [
|
|
|
66
69
|
"enable_file_logging",
|
|
67
70
|
"chapters",
|
|
68
71
|
"example",
|
|
72
|
+
"debug",
|
|
69
73
|
]
|
|
@@ -26,6 +26,7 @@ from telebot.types import Message
|
|
|
26
26
|
|
|
27
27
|
# Local modules
|
|
28
28
|
from . import senders
|
|
29
|
+
from .debug import Debug
|
|
29
30
|
from .styles import TextEntity
|
|
30
31
|
|
|
31
32
|
# Chain modules
|
|
@@ -154,7 +155,7 @@ class Chain(ChainInlineKeyboardLogic, ChainEntryLogic):
|
|
|
154
155
|
self.sender.set_edit_message(None)
|
|
155
156
|
self._previous_message = message
|
|
156
157
|
|
|
157
|
-
if
|
|
158
|
+
if Debug.timeout_warnings and _handler and not _timeout:
|
|
158
159
|
library.warning(
|
|
159
160
|
"Next-message handler is active, but no timeout was set for the chain. "
|
|
160
161
|
"This may cause the bot to wait indefinitely."
|
|
@@ -31,14 +31,13 @@ from . import _input_handler
|
|
|
31
31
|
from . import _timeout
|
|
32
32
|
|
|
33
33
|
# Logging
|
|
34
|
+
from .debug import Debug
|
|
34
35
|
from ._logger import logger
|
|
35
36
|
library = logger.library
|
|
36
37
|
|
|
37
38
|
class ChainBase:
|
|
38
39
|
|
|
39
40
|
bot: telebot.TeleBot
|
|
40
|
-
|
|
41
|
-
_timeout_warnings_enabled: bool = True
|
|
42
41
|
|
|
43
42
|
@classmethod
|
|
44
43
|
def _init(cls, bot: telebot.TeleBot):
|
|
@@ -206,4 +205,5 @@ class ChainBase:
|
|
|
206
205
|
# Timeout API
|
|
207
206
|
|
|
208
207
|
def disable_timeout_warnings(self, value: bool = True) -> None:
|
|
209
|
-
|
|
208
|
+
""".. deprecated:: Set ``Debug.timeout_warnings`` directly."""
|
|
209
|
+
Debug.timeout_warnings = not value
|
|
@@ -26,6 +26,8 @@ from telebot.types import InlineKeyboardButton, CallbackQuery
|
|
|
26
26
|
from telebot import TeleBot
|
|
27
27
|
from ._callback_query_handler import CallbackQueryHandler
|
|
28
28
|
|
|
29
|
+
from ._state import TelekitState
|
|
30
|
+
|
|
29
31
|
__all__ = [
|
|
30
32
|
"InlineButton",
|
|
31
33
|
|
|
@@ -122,6 +124,15 @@ class InlineButton:
|
|
|
122
124
|
raise ValueError(f"Unknown style: {style!r}. Must be one of {_BUTTON_STYLES_LIST}")
|
|
123
125
|
raise TypeError(f"Style must be str, ButtonStyle, or None, got {type(style)}")
|
|
124
126
|
|
|
127
|
+
def _normalize_custom_emoji_id(self, custom_emoji_id: int | str | None) -> str | None:
|
|
128
|
+
if not custom_emoji_id:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
if not TelekitState.is_premium():
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
return str(custom_emoji_id)
|
|
135
|
+
|
|
125
136
|
class StaticButton(InlineButton):
|
|
126
137
|
|
|
127
138
|
def __init__(self, *, style: str | None | ButtonStyle = None, **kwargs):
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
from telebot import TeleBot
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
class DebugLevel:
|
|
5
|
+
NONE: int = 0
|
|
6
|
+
INFO: int = 1
|
|
7
|
+
DEBUG: int = 2
|
|
8
|
+
|
|
9
|
+
|
|
4
10
|
class TelekitState:
|
|
5
11
|
|
|
6
12
|
__bot: TeleBot
|
|
7
13
|
|
|
8
14
|
@classmethod
|
|
9
|
-
def
|
|
15
|
+
def _init(cls, bot: TeleBot) -> None:
|
|
10
16
|
if hasattr(cls, "_TelekitState__bot"):
|
|
11
17
|
raise RuntimeError("TelekitState is already initialized")
|
|
12
18
|
|
|
13
19
|
cls.__bot = bot
|
|
20
|
+
cls._update()
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def _update(cls):
|
|
24
|
+
cls.__info = cls.__bot.get_me()
|
|
14
25
|
|
|
15
26
|
@classmethod
|
|
16
27
|
def get_bot(cls) -> TeleBot:
|
|
@@ -19,4 +30,6 @@ class TelekitState:
|
|
|
19
30
|
|
|
20
31
|
return cls.__bot
|
|
21
32
|
|
|
22
|
-
|
|
33
|
+
@classmethod
|
|
34
|
+
def is_premium(cls) -> bool:
|
|
35
|
+
return cls.__info.is_premium or False
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event distance bot — pick any date and find out how far it is from today.
|
|
3
|
+
Works for both past events ("it was X ago") and future events ("it will be in X").
|
|
4
|
+
Uses the Calendar trait for date selection; no database required.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import datetime
|
|
9
|
+
|
|
10
|
+
import telekit
|
|
11
|
+
import telekit.traits
|
|
12
|
+
|
|
13
|
+
from telekit.styles import Bold, Code, Group, Italic, Quote
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Time-distance helpers
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
def _diff(a: datetime.date, b: datetime.date) -> tuple[int, int, int]:
|
|
21
|
+
"""
|
|
22
|
+
Return the absolute difference between *a* and *b* as (years, months, days).
|
|
23
|
+
|
|
24
|
+
Always returns non-negative values; direction is determined by the caller.
|
|
25
|
+
|
|
26
|
+
:param a: First date.
|
|
27
|
+
:param b: Second date (order does not matter).
|
|
28
|
+
:returns: Tuple ``(years, months, days)`` with all values >= 0.
|
|
29
|
+
"""
|
|
30
|
+
if a > b:
|
|
31
|
+
a, b = b, a
|
|
32
|
+
|
|
33
|
+
years = b.year - a.year
|
|
34
|
+
months = b.month - a.month
|
|
35
|
+
days = b.day - a.day
|
|
36
|
+
|
|
37
|
+
if days < 0:
|
|
38
|
+
months -= 1
|
|
39
|
+
prev_month_last = (datetime.date(b.year, b.month, 1) - datetime.timedelta(days=1)).day
|
|
40
|
+
days += prev_month_last
|
|
41
|
+
|
|
42
|
+
if months < 0:
|
|
43
|
+
years -= 1
|
|
44
|
+
months += 12
|
|
45
|
+
|
|
46
|
+
return years, months, days
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _fmt(years: int, months: int, days: int) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Format a duration, omitting zero components.
|
|
52
|
+
|
|
53
|
+
Examples::
|
|
54
|
+
|
|
55
|
+
_fmt(0, 0, 1) -> "1 day"
|
|
56
|
+
_fmt(2, 3, 0) -> "2 years and 3 months"
|
|
57
|
+
_fmt(1, 0, 5) -> "1 year and 5 days"
|
|
58
|
+
|
|
59
|
+
:param years: Number of whole years.
|
|
60
|
+
:param months: Number of remaining whole months.
|
|
61
|
+
:param days: Number of remaining days.
|
|
62
|
+
:returns: Human-readable duration string.
|
|
63
|
+
"""
|
|
64
|
+
parts: list[str] = []
|
|
65
|
+
if years:
|
|
66
|
+
parts.append(f"{years} year{'s' if years != 1 else ''}")
|
|
67
|
+
if months:
|
|
68
|
+
parts.append(f"{months} month{'s' if months != 1 else ''}")
|
|
69
|
+
if days:
|
|
70
|
+
parts.append(f"{days} day{'s' if days != 1 else ''}")
|
|
71
|
+
|
|
72
|
+
if not parts:
|
|
73
|
+
return "today"
|
|
74
|
+
if len(parts) == 1:
|
|
75
|
+
return parts[0]
|
|
76
|
+
return ", ".join(parts[:-1]) + " and " + parts[-1]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _describe(chosen: datetime.date) -> str:
|
|
80
|
+
"""
|
|
81
|
+
Build a human-readable distance string from *chosen* to today.
|
|
82
|
+
|
|
83
|
+
:param chosen: The date selected by the user.
|
|
84
|
+
:returns: Sentence describing how far the date is from today.
|
|
85
|
+
"""
|
|
86
|
+
today = datetime.date.today()
|
|
87
|
+
y, mo, d = _diff(chosen, today)
|
|
88
|
+
duration = _fmt(y, mo, d)
|
|
89
|
+
|
|
90
|
+
if chosen == today:
|
|
91
|
+
return "That's today! 🎉"
|
|
92
|
+
elif chosen < today:
|
|
93
|
+
return f"That was {duration} ago."
|
|
94
|
+
else:
|
|
95
|
+
return f"That will be in {duration}."
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# In-memory store (no DB)
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Help text
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
_HELP_TEXT = Group(
|
|
108
|
+
Bold("Buttons"),
|
|
109
|
+
Group(
|
|
110
|
+
Group(Bold("MAY"), " — tap the month name to switch to month picker"),
|
|
111
|
+
Group(Bold("2025"), " — tap the year to switch to year picker"),
|
|
112
|
+
Group(Bold("This month"), " — jump back to the current month"),
|
|
113
|
+
sep="\n",
|
|
114
|
+
),
|
|
115
|
+
Bold("Keyboard input"),
|
|
116
|
+
Group(
|
|
117
|
+
"Type a date and send it as a message:",
|
|
118
|
+
Group(Code("YYYY"), " — jump to that year"),
|
|
119
|
+
Group(Code("YYYY.MM"), " — jump to that month"),
|
|
120
|
+
Group(Code("YYYY.MM.DD"), " — select that date immediately"),
|
|
121
|
+
Quote("Separator: . or -", end=""),
|
|
122
|
+
sep="\n",
|
|
123
|
+
),
|
|
124
|
+
Italic("Values outside the allowed range are silently ignored."),
|
|
125
|
+
sep="\n\n",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# Handler
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
class CalendarHandler(
|
|
134
|
+
telekit.traits.CalendarPick,
|
|
135
|
+
telekit.Handler,
|
|
136
|
+
):
|
|
137
|
+
@classmethod
|
|
138
|
+
def init_handler(cls) -> None:
|
|
139
|
+
cls.on.command("calendar").invoke(cls.handle)
|
|
140
|
+
|
|
141
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
142
|
+
super().__init__(*args, **kwargs)
|
|
143
|
+
self._date: datetime.date | None = None
|
|
144
|
+
|
|
145
|
+
# -----------------------------------------------------------------------
|
|
146
|
+
# Step 1 — welcome screen
|
|
147
|
+
# -----------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def handle(self) -> None:
|
|
150
|
+
"""Entry point: welcome message with Help and Begin buttons side by side."""
|
|
151
|
+
self._date = None
|
|
152
|
+
|
|
153
|
+
self.chain.sender.set_title("📅 Event Distance")
|
|
154
|
+
self.chain.sender.set_message(
|
|
155
|
+
"Pick any date and find out how far it is from today — "
|
|
156
|
+
"whether it happened long ago or is still coming"
|
|
157
|
+
)
|
|
158
|
+
self.chain.set_inline_keyboard(
|
|
159
|
+
{
|
|
160
|
+
"?Help": self.show_help,
|
|
161
|
+
"Begin »": self.entry_date,
|
|
162
|
+
},
|
|
163
|
+
row_width=2,
|
|
164
|
+
)
|
|
165
|
+
self.chain.edit()
|
|
166
|
+
|
|
167
|
+
# -----------------------------------------------------------------------
|
|
168
|
+
# Help screen
|
|
169
|
+
# -----------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def show_help(self) -> None:
|
|
172
|
+
"""Show calendar navigation tips; a Back button returns to the welcome screen."""
|
|
173
|
+
self.chain.sender.set_title("📖 How to use the calendar")
|
|
174
|
+
self.chain.sender.set_message(_HELP_TEXT)
|
|
175
|
+
self.chain.set_inline_keyboard({"✓ Okay": self.handle}, row_width=1)
|
|
176
|
+
self.chain.edit()
|
|
177
|
+
|
|
178
|
+
# -----------------------------------------------------------------------
|
|
179
|
+
# Step 2 — calendar
|
|
180
|
+
# -----------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
def entry_date(self) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Open the calendar picker.
|
|
185
|
+
|
|
186
|
+
Restores the previously chosen date as the initial view so the user
|
|
187
|
+
lands on a familiar position when changing their answer.
|
|
188
|
+
"""
|
|
189
|
+
self.chain.sender.set_title("📅 Choose a date")
|
|
190
|
+
self.chain.sender.set_message("Select any date — past or future:")
|
|
191
|
+
self.chain.sender.set_remove_text(False)
|
|
192
|
+
|
|
193
|
+
self.calendar_pick(
|
|
194
|
+
self.handle_date,
|
|
195
|
+
initial=self._date,
|
|
196
|
+
show_nav_hints=True,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# -----------------------------------------------------------------------
|
|
200
|
+
# Step 3 — result
|
|
201
|
+
# -----------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
def handle_date(self, date: datetime.date) -> None:
|
|
204
|
+
"""
|
|
205
|
+
Display the distance result for *date*.
|
|
206
|
+
|
|
207
|
+
:param date: The date chosen by the user in the calendar.
|
|
208
|
+
"""
|
|
209
|
+
self._date = date
|
|
210
|
+
|
|
211
|
+
self.chain.sender.set_title(f"🗓 {date.strftime('%d %B %Y')}")
|
|
212
|
+
self.chain.sender.set_message(
|
|
213
|
+
_describe(date)
|
|
214
|
+
)
|
|
215
|
+
# «Change reopens the calendar at the previously selected month.
|
|
216
|
+
self.chain.set_inline_keyboard(
|
|
217
|
+
{
|
|
218
|
+
"« Change": self.entry_date,
|
|
219
|
+
"↺ New date": self.handle,
|
|
220
|
+
},
|
|
221
|
+
row_width=2,
|
|
222
|
+
)
|
|
223
|
+
self.chain.edit()
|
|
@@ -30,7 +30,8 @@ class StartHandler(telekit.Handler):
|
|
|
30
30
|
"📄 File Info": "TextDocumentHandler",
|
|
31
31
|
|
|
32
32
|
"🖼️ QR Editor": "QRHandler",
|
|
33
|
-
|
|
33
|
+
"📆 Calendar": "CalendarHandler",
|
|
34
|
+
}, row_width=[3, 1, 3, 2]
|
|
34
35
|
)
|
|
35
36
|
def handle_response(handler: str):
|
|
36
37
|
self.handoff(handler).handle()
|
|
@@ -30,6 +30,7 @@ from telebot.types import (
|
|
|
30
30
|
InputMediaVideo, InputMediaAnimation
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
+
from telekit.debug import Debug
|
|
33
34
|
from telekit.styles import TextEntity, Escape, Raw, Group, Bold, Italic
|
|
34
35
|
from telekit.types import ParseMode, Effect as _Effect, ChatAction as _ChatAction
|
|
35
36
|
from telekit import dices
|
|
@@ -1143,7 +1144,8 @@ class BaseSender:
|
|
|
1143
1144
|
try:
|
|
1144
1145
|
return self.bot.delete_message(chat_id=self.chat_id, message_id=message_id)
|
|
1145
1146
|
except Exception as exception:
|
|
1146
|
-
|
|
1147
|
+
if Debug.deletion_warnings:
|
|
1148
|
+
library.warning(f"Failed to delete message {message_id}. Maybe the user deleted it. Exception: {exception}")
|
|
1147
1149
|
return False
|
|
1148
1150
|
|
|
1149
1151
|
def delete_message(self, message: Message | None, only_user_messages: bool=False) -> bool:
|
|
@@ -22,7 +22,7 @@ import traceback
|
|
|
22
22
|
import sys
|
|
23
23
|
from typing import Optional
|
|
24
24
|
|
|
25
|
-
from . import _init, _state
|
|
25
|
+
from . import _init, _state, debug as _debug
|
|
26
26
|
import telebot
|
|
27
27
|
|
|
28
28
|
from ._logger import logger
|
|
@@ -49,7 +49,7 @@ class Server:
|
|
|
49
49
|
):
|
|
50
50
|
|
|
51
51
|
self._auto_restart = auto_restart
|
|
52
|
-
|
|
52
|
+
_debug.Debug.set_all(debug)
|
|
53
53
|
|
|
54
54
|
if isinstance(bot, str):
|
|
55
55
|
bot = telebot.TeleBot(bot)
|
|
@@ -133,6 +133,7 @@ class Server:
|
|
|
133
133
|
raise exception
|
|
134
134
|
finally:
|
|
135
135
|
time.sleep(10)
|
|
136
|
+
_state.TelekitState._update()
|
|
136
137
|
|
|
137
138
|
def long_polling(self, *, timeout: int = 60):
|
|
138
139
|
"""Long `bot.polling(none_stop=True, timeout=timeout)` polling with custom timeout"""
|
|
@@ -150,6 +151,7 @@ class Server:
|
|
|
150
151
|
raise exception
|
|
151
152
|
finally:
|
|
152
153
|
time.sleep(5)
|
|
154
|
+
_state.TelekitState._update()
|
|
153
155
|
|
|
154
156
|
|
|
155
157
|
# Example
|