telekit 2.2.0a1__tar.gz → 2.2.0a3__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.0a1/telekit.egg-info → telekit-2.2.0a3}/PKG-INFO +44 -10
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_chain.py +1 -4
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_chain_base.py +6 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_chain_entry_logic.py +12 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_chain_inline_keyboards_logic.py +41 -20
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_inline_buttons.py +132 -4
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/mixin.py +2 -2
- telekit-2.2.0a3/telekit/_user.py +199 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_version.py +1 -1
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/entry.py +1 -3
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/pages.py +12 -5
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/inline_buttons.py +2 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/senders.py +277 -66
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/types.py +43 -30
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/utils.py +25 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3/telekit.egg-info}/PKG-INFO +44 -10
- telekit-2.2.0a1/telekit/_user.py +0 -116
- {telekit-2.2.0a1 → telekit-2.2.0a3}/LICENSE +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/README.md +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/setup.cfg +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/setup.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/__init__.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_buildtext/__init__.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_buildtext/formatter.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_buildtext/styles.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_callback_query_handler.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_chapters/__init__.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_chapters/chapters.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_handler.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_init.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_input_handler.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_logger.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_on.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_snapvault/__init__.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_snapvault/snapcode.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_snapvault/snapvault.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_state.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/__init__.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/parser/__init__.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/parser/builder.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/parser/canvas_parser.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/parser/lexer.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/parser/nodes.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/parser/parser.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/parser/token.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/telekit_dsl.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/telekit_orm.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_timeout.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/dices.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/__init__.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/__init__.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/complete_hotel.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/counter.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/dsl.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/faq.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/hotel.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/on_text.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/pyapi.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/qr.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/quiz.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/spells.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/start.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/text_document.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_server.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/parameters.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/server.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/styles.py +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit.egg-info/SOURCES.txt +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit.egg-info/dependency_links.txt +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit.egg-info/requires.txt +0 -0
- {telekit-2.2.0a1 → telekit-2.2.0a3}/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.2.
|
|
3
|
+
Version: 2.2.0a3
|
|
4
4
|
Summary: Declarative, developer-friendly library for building Telegram bots
|
|
5
5
|
Home-page: https://github.com/Romashkaa/telekit
|
|
6
6
|
Author: romashka
|
|
@@ -378,15 +378,49 @@ It tries to make Telegram bot development easier.
|
|
|
378
378
|
|
|
379
379
|
---
|
|
380
380
|
|
|
381
|
-
# Changes in version 2.2.
|
|
381
|
+
# Changes in version 2.2.0a3
|
|
382
382
|
|
|
383
383
|
## New Button Types
|
|
384
384
|
|
|
385
|
-
| **Name**
|
|
386
|
-
|
|
387
|
-
| `AlertButton`
|
|
388
|
-
| `NotificationButton` | A callback button that shows a
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
385
|
+
| **Name** | **Description** |
|
|
386
|
+
|----------------------|----------------------------------------------------------|
|
|
387
|
+
| `AlertButton` | A callback button that shows a popup alert when pressed. |
|
|
388
|
+
| `NotificationButton` | A callback button that shows a notification. |
|
|
389
|
+
| `InvokeButton` | A callback button that calls the object method. |
|
|
390
|
+
|
|
391
|
+
## User Improvements
|
|
392
|
+
|
|
393
|
+
| **Name** | **Description** |
|
|
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. |
|
|
405
|
+
|
|
406
|
+
- Refactor: `User` now accepts a `Message` object instead of `chat_id` + `from_user`. All properties are derived from `_sender` (`from_user` or `chat`) and migrated to `cached_property`. Fixed broken `get_id` and `get_full_name` references.
|
|
407
|
+
- Added `__repr__`.
|
|
408
|
+
|
|
409
|
+
## Sender Improvements
|
|
410
|
+
|
|
411
|
+
| **Name** | **Description** |
|
|
412
|
+
| -------------------------- | ------------------------------------------------------ |
|
|
413
|
+
| `sent_message` | The last message sent by this sender instance. |
|
|
414
|
+
| `disable_notification` | Disables notification sound when the message is sent. |
|
|
415
|
+
| `protect_content` | Protects the message contents from forwarding and saving. |
|
|
416
|
+
| `reply_parameters` | Reply parameters for the message to be sent. |
|
|
417
|
+
| `link_preview_options` | Link preview options for the message to be sent. |
|
|
418
|
+
| `show_caption_above_media` | Shows the caption above the media instead of below. |
|
|
419
|
+
|
|
420
|
+
- Refactored sending system.
|
|
421
|
+
|
|
422
|
+
## Chain Improvements
|
|
423
|
+
|
|
424
|
+
| **Name** | **Description** |
|
|
425
|
+
| ----------- | ------------------------------------------------------------ |
|
|
426
|
+
| `received` | The message received from the user during entry processing. |
|
|
@@ -28,11 +28,8 @@ from telebot.types import Message
|
|
|
28
28
|
from . import senders
|
|
29
29
|
from .styles import TextEntity
|
|
30
30
|
|
|
31
|
-
# Logging
|
|
32
|
-
from ._logger import logger
|
|
33
|
-
library = logger.library
|
|
34
|
-
|
|
35
31
|
# Chain modules
|
|
32
|
+
from ._chain_base import library
|
|
36
33
|
from ._chain_inline_keyboards_logic import ChainInlineKeyboardLogic
|
|
37
34
|
from ._chain_entry_logic import ChainEntryLogic, TextDocument
|
|
38
35
|
|
|
@@ -30,6 +30,10 @@ from . import senders
|
|
|
30
30
|
from . import _input_handler
|
|
31
31
|
from . import _timeout
|
|
32
32
|
|
|
33
|
+
# Logging
|
|
34
|
+
from ._logger import logger
|
|
35
|
+
library = logger.library
|
|
36
|
+
|
|
33
37
|
class ChainBase:
|
|
34
38
|
|
|
35
39
|
bot: telebot.TeleBot
|
|
@@ -49,6 +53,8 @@ class ChainBase:
|
|
|
49
53
|
def __init__(self, chat_id: int, *, previous_message: Message | None = None):
|
|
50
54
|
self.chat_id = chat_id
|
|
51
55
|
self.sender = senders.Sender(chat_id)
|
|
56
|
+
self.received = None
|
|
57
|
+
|
|
52
58
|
self._handler = _input_handler.InputHandler(chat_id)
|
|
53
59
|
self._previous_message = previous_message
|
|
54
60
|
self._timeout_handler = _timeout.TimeoutHandler()
|
|
@@ -68,6 +68,8 @@ class ChainEntryLogic(ChainBase):
|
|
|
68
68
|
"""
|
|
69
69
|
|
|
70
70
|
def callback(message: Message) -> bool:
|
|
71
|
+
self.received = message
|
|
72
|
+
|
|
71
73
|
if delete_user_response:
|
|
72
74
|
self.sender.delete_message(message, True)
|
|
73
75
|
|
|
@@ -154,6 +156,8 @@ class ChainEntryLogic(ChainBase):
|
|
|
154
156
|
"""
|
|
155
157
|
|
|
156
158
|
def callback(message: Message) -> bool:
|
|
159
|
+
self.received = message
|
|
160
|
+
|
|
157
161
|
if delete_user_response:
|
|
158
162
|
self.sender.delete_message(message, True)
|
|
159
163
|
|
|
@@ -246,6 +250,8 @@ class ChainEntryLogic(ChainBase):
|
|
|
246
250
|
"""
|
|
247
251
|
|
|
248
252
|
def callback(message: Message) -> bool:
|
|
253
|
+
self.received = message
|
|
254
|
+
|
|
249
255
|
if delete_user_response:
|
|
250
256
|
self.sender.delete_message(message, True)
|
|
251
257
|
|
|
@@ -340,6 +346,8 @@ class ChainEntryLogic(ChainBase):
|
|
|
340
346
|
"""
|
|
341
347
|
|
|
342
348
|
def callback(message: Message) -> bool:
|
|
349
|
+
self.received = message
|
|
350
|
+
|
|
343
351
|
if delete_user_response:
|
|
344
352
|
self.sender.delete_message(message, True)
|
|
345
353
|
|
|
@@ -453,6 +461,8 @@ class ChainEntryLogic(ChainBase):
|
|
|
453
461
|
"""
|
|
454
462
|
|
|
455
463
|
def callback(message: Message) -> bool:
|
|
464
|
+
self.received = message
|
|
465
|
+
|
|
456
466
|
if delete_user_response:
|
|
457
467
|
self.sender.delete_message(message, True)
|
|
458
468
|
|
|
@@ -592,6 +602,8 @@ class ChainEntryLogic(ChainBase):
|
|
|
592
602
|
"""
|
|
593
603
|
|
|
594
604
|
def callback(message: Message) -> bool:
|
|
605
|
+
self.received = message
|
|
606
|
+
|
|
595
607
|
if delete_user_response:
|
|
596
608
|
self.sender.delete_message(message, True)
|
|
597
609
|
|
|
@@ -32,7 +32,7 @@ from telebot.types import (
|
|
|
32
32
|
|
|
33
33
|
from ._callback_query_handler import CallbackQueryHandler
|
|
34
34
|
from ._inline_buttons import InlineButton, CallbackButton
|
|
35
|
-
from ._chain_base import ChainBase
|
|
35
|
+
from ._chain_base import ChainBase, library
|
|
36
36
|
|
|
37
37
|
if typing.TYPE_CHECKING:
|
|
38
38
|
from ._chain import Chain # only for type hints
|
|
@@ -151,15 +151,24 @@ class ChainInlineKeyboardLogic(ChainBase):
|
|
|
151
151
|
buttons: list[InlineKeyboardButton] = []
|
|
152
152
|
|
|
153
153
|
for index, (caption, value) in enumerate(keyboard.items()):
|
|
154
|
-
if enable_special_buttons and isinstance(value, InlineButton):
|
|
154
|
+
if enable_special_buttons and isinstance(value, InlineButton) and not isinstance(value, CallbackButton):
|
|
155
155
|
buttons.append(value._compile(caption))
|
|
156
156
|
else:
|
|
157
|
+
if isinstance(value, CallbackButton):
|
|
158
|
+
invoker = value.build_invoker(self._cancel_timeout_and_handlers)
|
|
159
|
+
else:
|
|
160
|
+
invoker = self._get_invoker_with_argument(func, value)
|
|
161
|
+
|
|
157
162
|
callback_data = CallbackQueryHandler.inline_button(f"{index}:{random.randint(1000, 9999)}")
|
|
158
|
-
callback_functions[callback_data] =
|
|
163
|
+
callback_functions[callback_data] = invoker
|
|
164
|
+
|
|
165
|
+
kwargs: dict[str, Any] = getattr(invoker, "_kwargs", None) or {}
|
|
166
|
+
|
|
159
167
|
buttons.append(
|
|
160
168
|
InlineKeyboardButton(
|
|
161
169
|
text=caption,
|
|
162
|
-
callback_data=callback_data
|
|
170
|
+
callback_data=callback_data,
|
|
171
|
+
**kwargs
|
|
163
172
|
)
|
|
164
173
|
)
|
|
165
174
|
|
|
@@ -242,18 +251,34 @@ class ChainInlineKeyboardLogic(ChainBase):
|
|
|
242
251
|
buttons: list[InlineKeyboardButton] = []
|
|
243
252
|
|
|
244
253
|
if not isinstance(choices, dict):
|
|
254
|
+
for c in choices:
|
|
255
|
+
if type(c).__str__ is object.__str__:
|
|
256
|
+
library.warning(
|
|
257
|
+
f"{type(c).__name__} does not implement __str__. "
|
|
258
|
+
f"Consider passing a dict with explicit labels.",
|
|
259
|
+
stacklevel=3
|
|
260
|
+
)
|
|
245
261
|
choices = {str(c): c for c in choices}
|
|
246
262
|
|
|
247
263
|
for index, (caption, value) in enumerate(choices.items()):
|
|
248
|
-
if enable_special_buttons and isinstance(value, InlineButton):
|
|
264
|
+
if enable_special_buttons and isinstance(value, InlineButton) and not isinstance(value, CallbackButton):
|
|
249
265
|
buttons.append(value._compile(caption))
|
|
250
266
|
else:
|
|
267
|
+
if isinstance(value, CallbackButton):
|
|
268
|
+
invoker = value.build_invoker(self._cancel_timeout_and_handlers)
|
|
269
|
+
else:
|
|
270
|
+
invoker = self._get_invoker_with_argument(func, value)
|
|
271
|
+
|
|
251
272
|
callback_data = CallbackQueryHandler.inline_button(f"{index}:{random.randint(1000, 9999)}")
|
|
252
|
-
callback_functions[callback_data] =
|
|
273
|
+
callback_functions[callback_data] = invoker
|
|
274
|
+
|
|
275
|
+
kwargs: dict[str, Any] = getattr(invoker, "_kwargs", None) or {}
|
|
276
|
+
|
|
253
277
|
buttons.append(
|
|
254
278
|
InlineKeyboardButton(
|
|
255
279
|
text=caption,
|
|
256
|
-
callback_data=callback_data
|
|
280
|
+
callback_data=callback_data,
|
|
281
|
+
**kwargs
|
|
257
282
|
)
|
|
258
283
|
)
|
|
259
284
|
|
|
@@ -314,21 +339,21 @@ class ChainInlineKeyboardLogic(ChainBase):
|
|
|
314
339
|
|
|
315
340
|
self.sender.set_reply_markup(markup)
|
|
316
341
|
|
|
317
|
-
def
|
|
318
|
-
def
|
|
342
|
+
def _get_invoker_with_argument(self, callback: Callable, argument: Any, query_answer: tuple[str, bool] | None = None) -> Callable[[CallbackQuery], None]:
|
|
343
|
+
def invoker(call: CallbackQuery) -> None:
|
|
319
344
|
self._cancel_timeout_and_handlers()
|
|
320
|
-
|
|
345
|
+
callback(argument)
|
|
321
346
|
self._answer_callback_query(call, query_answer)
|
|
322
347
|
|
|
323
|
-
return
|
|
348
|
+
return invoker
|
|
324
349
|
|
|
325
|
-
def
|
|
326
|
-
def
|
|
350
|
+
def _get_invoker(self, callback: Callable[..., None], query_answer: tuple[str, bool] | None = None) -> Callable[[CallbackQuery], None]:
|
|
351
|
+
def invoker(call: CallbackQuery):
|
|
327
352
|
self._cancel_timeout_and_handlers()
|
|
328
|
-
|
|
353
|
+
callback()
|
|
329
354
|
self._answer_callback_query(call, query_answer)
|
|
330
355
|
|
|
331
|
-
return
|
|
356
|
+
return invoker
|
|
332
357
|
|
|
333
358
|
def _answer_callback_query(self, call: CallbackQuery, query_answer: tuple[str, bool] | None = None):
|
|
334
359
|
if query_answer is None:
|
|
@@ -369,8 +394,4 @@ class ChainInlineKeyboardLogic(ChainBase):
|
|
|
369
394
|
rows.append(buttons[index:index + last_width])
|
|
370
395
|
index += last_width
|
|
371
396
|
|
|
372
|
-
return rows
|
|
373
|
-
|
|
374
|
-
def _is_valid_telegram_callback(self, data: str) -> bool:
|
|
375
|
-
byte_size = len(data.encode('utf-8'))
|
|
376
|
-
return 1 <= byte_size <= 64
|
|
397
|
+
return rows
|
|
@@ -36,6 +36,7 @@ __all__ = [
|
|
|
36
36
|
"CallbackButton",
|
|
37
37
|
"AlertButton",
|
|
38
38
|
"NotificationButton",
|
|
39
|
+
"InvokeButton",
|
|
39
40
|
|
|
40
41
|
"ButtonStyle"
|
|
41
42
|
]
|
|
@@ -56,8 +57,9 @@ class InlineButton:
|
|
|
56
57
|
- `SuggestButton`
|
|
57
58
|
- `CopyTextButton`
|
|
58
59
|
- `CallbackButton`
|
|
59
|
-
- `AlertButton
|
|
60
|
-
- `NotificationButton
|
|
60
|
+
- `AlertButton`
|
|
61
|
+
- `NotificationButton`
|
|
62
|
+
- `InvokeButton`
|
|
61
63
|
"""
|
|
62
64
|
|
|
63
65
|
_bot: TeleBot
|
|
@@ -76,6 +78,7 @@ class InlineButton:
|
|
|
76
78
|
Callback: type["CallbackButton"]
|
|
77
79
|
Alert: type["AlertButton"]
|
|
78
80
|
Notification: type["NotificationButton"]
|
|
81
|
+
Invoke: type["InvokeButton"]
|
|
79
82
|
|
|
80
83
|
Styles: type[ButtonStyle] = ButtonStyle
|
|
81
84
|
|
|
@@ -340,7 +343,7 @@ class CallbackButton(InlineButton):
|
|
|
340
343
|
self._answer_callback_query(call)
|
|
341
344
|
|
|
342
345
|
def __init__(
|
|
343
|
-
self,
|
|
346
|
+
self,
|
|
344
347
|
callback: Callable[..., Any] | None,
|
|
345
348
|
|
|
346
349
|
pass_args: tuple | list | None = None,
|
|
@@ -482,6 +485,130 @@ class NotificationButton(CallbackButton):
|
|
|
482
485
|
style=style,
|
|
483
486
|
**kwargs
|
|
484
487
|
)
|
|
488
|
+
|
|
489
|
+
class InvokeButton(CallbackButton):
|
|
490
|
+
"""
|
|
491
|
+
An inline keyboard button that calls a named method on a given object when pressed.
|
|
492
|
+
|
|
493
|
+
Unlike `CallbackButton`, which takes a callable directly, `InvokeButton` resolves
|
|
494
|
+
the method at invocation time via `getattr(obj, invoke)`.
|
|
495
|
+
|
|
496
|
+
:param obj: The object on which the method will be called.
|
|
497
|
+
:type obj: `Any`
|
|
498
|
+
|
|
499
|
+
:param invoke: Name of the method to call on `obj`.
|
|
500
|
+
:type invoke: `str`
|
|
501
|
+
|
|
502
|
+
:param pass_args: Positional arguments to pass into the method.
|
|
503
|
+
:type pass_args: `tuple | list | None`
|
|
504
|
+
|
|
505
|
+
:param pass_kwargs: Keyword arguments to pass into the method.
|
|
506
|
+
:type pass_kwargs: `dict[str, Any] | None`
|
|
507
|
+
|
|
508
|
+
:param answer_text: Optional text to send as an answer to the callback query.
|
|
509
|
+
:type answer_text: `str | None`
|
|
510
|
+
|
|
511
|
+
:param answer_as_alert: If `True`, the answer is shown as a popup alert.
|
|
512
|
+
If `False`, it appears as a notification at the top of the chat.
|
|
513
|
+
:type answer_as_alert: `bool`
|
|
514
|
+
|
|
515
|
+
:param style: Style of the button. Must be one of `ButtonStyle.DANGER` (red),
|
|
516
|
+
`*.SUCCESS` (green) or `*.PRIMARY` (blue).
|
|
517
|
+
You can also pass these as string values: "danger", "success", "primary".
|
|
518
|
+
If omitted, an app-specific default style is used.
|
|
519
|
+
:type style: `str | ButtonStyle | None`
|
|
520
|
+
|
|
521
|
+
:param kwargs: Additional keyword arguments passed directly to `InlineKeyboardButton`.
|
|
522
|
+
:type kwargs: `Any`
|
|
523
|
+
|
|
524
|
+
Example::
|
|
525
|
+
|
|
526
|
+
self.chain.set_inline_keyboard({
|
|
527
|
+
"📖 My Deck": InvokeButton(self.handoff(DeckHandler), "handle"),
|
|
528
|
+
})
|
|
529
|
+
"""
|
|
530
|
+
class _CallbackInvoker(CallbackButton._CallbackInvoker):
|
|
531
|
+
def __init__(
|
|
532
|
+
self,
|
|
533
|
+
chain_callback: Callable[[], None],
|
|
534
|
+
|
|
535
|
+
obj: Any,
|
|
536
|
+
invoke: str,
|
|
537
|
+
|
|
538
|
+
pass_args: tuple | list | None = None,
|
|
539
|
+
pass_kwargs: dict[str, Any] | None = None,
|
|
540
|
+
|
|
541
|
+
answer_text: str | None = None,
|
|
542
|
+
answer_as_alert: bool = True,
|
|
543
|
+
|
|
544
|
+
style: str | None | ButtonStyle = None,
|
|
545
|
+
|
|
546
|
+
kwargs: dict[str, Any] = {}
|
|
547
|
+
):
|
|
548
|
+
self._obj = obj
|
|
549
|
+
self._invoke = invoke
|
|
550
|
+
|
|
551
|
+
self._pass_args = pass_args
|
|
552
|
+
self._pass_kwargs = pass_kwargs
|
|
553
|
+
|
|
554
|
+
self._answer_text = answer_text
|
|
555
|
+
self._answer_as_alert = answer_as_alert
|
|
556
|
+
|
|
557
|
+
self._style = style
|
|
558
|
+
|
|
559
|
+
self._kwargs = {"style": style} | kwargs
|
|
560
|
+
|
|
561
|
+
self._chain_callback: Callable[[], None] = chain_callback
|
|
562
|
+
|
|
563
|
+
def _invoke_callback(self):
|
|
564
|
+
obj: Any = self.__dict__["_obj"]
|
|
565
|
+
callback: Any = getattr(obj, self._invoke)
|
|
566
|
+
|
|
567
|
+
args = self._pass_args or ()
|
|
568
|
+
kwargs = self._pass_kwargs or {}
|
|
569
|
+
|
|
570
|
+
callback(*args, **kwargs)
|
|
571
|
+
|
|
572
|
+
def __init__(
|
|
573
|
+
self,
|
|
574
|
+
obj: Any,
|
|
575
|
+
invoke: str,
|
|
576
|
+
|
|
577
|
+
pass_args: tuple | list | None = None,
|
|
578
|
+
pass_kwargs: dict[str, Any] | None = None,
|
|
579
|
+
*,
|
|
580
|
+
answer_text: str | None = None,
|
|
581
|
+
answer_as_alert: bool = True,
|
|
582
|
+
|
|
583
|
+
style: str | None | ButtonStyle = None,
|
|
584
|
+
|
|
585
|
+
**kwargs
|
|
586
|
+
):
|
|
587
|
+
self._obj = obj
|
|
588
|
+
self._invoke = invoke
|
|
589
|
+
|
|
590
|
+
self._pass_args = pass_args
|
|
591
|
+
self._pass_kwargs = pass_kwargs
|
|
592
|
+
|
|
593
|
+
self._answer_text = answer_text
|
|
594
|
+
self._answer_as_alert = answer_as_alert
|
|
595
|
+
|
|
596
|
+
self._style = self._normalize_style(style)
|
|
597
|
+
|
|
598
|
+
self._kwargs = kwargs
|
|
599
|
+
|
|
600
|
+
def build_invoker(self, chain_callback: Callable[[], None]) -> _CallbackInvoker:
|
|
601
|
+
return self._CallbackInvoker(
|
|
602
|
+
chain_callback=chain_callback,
|
|
603
|
+
obj=self._obj,
|
|
604
|
+
invoke=self._invoke,
|
|
605
|
+
pass_args=self._pass_args,
|
|
606
|
+
pass_kwargs=self._pass_kwargs,
|
|
607
|
+
answer_text=self._answer_text,
|
|
608
|
+
answer_as_alert=self._answer_as_alert,
|
|
609
|
+
style=self._style,
|
|
610
|
+
kwargs=self._kwargs
|
|
611
|
+
)
|
|
485
612
|
|
|
486
613
|
InlineButton.Link = LinkButton
|
|
487
614
|
InlineButton.WebApp = WebAppButton
|
|
@@ -489,4 +616,5 @@ InlineButton.Suggest = SuggestButton
|
|
|
489
616
|
InlineButton.CopyText = CopyTextButton
|
|
490
617
|
InlineButton.Callback = CallbackButton
|
|
491
618
|
InlineButton.Alert = AlertButton
|
|
492
|
-
InlineButton.Notification = NotificationButton
|
|
619
|
+
InlineButton.Notification = NotificationButton
|
|
620
|
+
InlineButton.Invoke = InvokeButton
|
|
@@ -368,7 +368,7 @@ class DSLHandler(telekit.Handler):
|
|
|
368
368
|
if not hasattr(self, "_script_data_factory"):
|
|
369
369
|
message: str = f"{type(self).__name__}().start_script(): Script is not analyzed yet. Call cls.analyze_file() or cls.analyze_string() before starting it."
|
|
370
370
|
library.error(message)
|
|
371
|
-
self.chain.sender.
|
|
371
|
+
self.chain.sender.send_error("DSLError", message)
|
|
372
372
|
return
|
|
373
373
|
|
|
374
374
|
self.script_data = self._script_data_factory()
|
|
@@ -799,7 +799,7 @@ class DSLHandler(telekit.Handler):
|
|
|
799
799
|
# ----------------------------------------------------------------------------
|
|
800
800
|
|
|
801
801
|
def _fail(self, message: str, exception: type[Exception]=Exception):
|
|
802
|
-
self.chain.sender.
|
|
802
|
+
self.chain.sender.send_error("🤷 Something went wrong...", message)
|
|
803
803
|
library.error(message)
|
|
804
804
|
return exception(message)
|
|
805
805
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (C) 2026 Romashka
|
|
3
|
+
#
|
|
4
|
+
# This file is part of Telekit.
|
|
5
|
+
#
|
|
6
|
+
# Telekit is free software: you can redistribute it and/or modify it
|
|
7
|
+
# under the terms of the GNU General Public License as published by
|
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
#
|
|
11
|
+
# Telekit is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
13
|
+
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
|
14
|
+
# the GNU General Public License for more details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU General Public License
|
|
17
|
+
# along with Telekit. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
#
|
|
19
|
+
from functools import cached_property
|
|
20
|
+
from typing import Literal
|
|
21
|
+
|
|
22
|
+
import telebot
|
|
23
|
+
import telebot.types
|
|
24
|
+
|
|
25
|
+
from ._logger import logger
|
|
26
|
+
|
|
27
|
+
__all__ = ["User"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class User:
|
|
31
|
+
|
|
32
|
+
bot: telebot.TeleBot
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def _init(cls, bot: telebot.TeleBot) -> None:
|
|
36
|
+
cls.bot = bot
|
|
37
|
+
|
|
38
|
+
def __init__(self, message: telebot.types.Message) -> None:
|
|
39
|
+
self._message: telebot.types.Message = message
|
|
40
|
+
self._chat_id: int = message.chat.id
|
|
41
|
+
self.logger = logger.users(self._chat_id)
|
|
42
|
+
|
|
43
|
+
# ── Internal ──────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
@cached_property
|
|
46
|
+
def _sender(self) -> telebot.types.User | telebot.types.Chat:
|
|
47
|
+
"""Resolves to `from_user` if available, otherwise falls back to `chat`."""
|
|
48
|
+
return self._message.from_user or self._message.chat
|
|
49
|
+
|
|
50
|
+
# ── Logging ───────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
def enable_logging(self, *user_ids: int | str) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Enable logging for this user or for additional user IDs.
|
|
55
|
+
If no arguments are passed, enables logging for this instance's chat ID.
|
|
56
|
+
"""
|
|
57
|
+
logger.enable_user_logging(*(user_ids or (self._chat_id,)))
|
|
58
|
+
|
|
59
|
+
# ── Telegram native fields ────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def id(self) -> int:
|
|
63
|
+
"""Unique identifier of the user or chat."""
|
|
64
|
+
return self._sender.id
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def username(self) -> str | None:
|
|
68
|
+
"""Telegram username without the leading `@`, or `None` if not set."""
|
|
69
|
+
return self._sender.username
|
|
70
|
+
|
|
71
|
+
@cached_property
|
|
72
|
+
def first_name(self) -> str | None:
|
|
73
|
+
"""First name of the user, or the group/channel title as a fallback."""
|
|
74
|
+
if isinstance(self._sender, telebot.types.User):
|
|
75
|
+
return self._sender.first_name
|
|
76
|
+
return self._sender.first_name or self._sender.title
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def last_name(self) -> str | None:
|
|
80
|
+
"""Last name of the user, or `None` for chats and users without one."""
|
|
81
|
+
return self._sender.last_name
|
|
82
|
+
|
|
83
|
+
@cached_property
|
|
84
|
+
def is_bot(self) -> bool:
|
|
85
|
+
"""Whether the sender is a bot. Always `False` for chats."""
|
|
86
|
+
if isinstance(self._sender, telebot.types.User):
|
|
87
|
+
return self._sender.is_bot
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
@cached_property
|
|
91
|
+
def language_code(self) -> str | None:
|
|
92
|
+
"""
|
|
93
|
+
IETF language code of the user's Telegram client (e.g. `"en"`, `"uk"`).
|
|
94
|
+
Always `None` for chats.
|
|
95
|
+
"""
|
|
96
|
+
if isinstance(self._sender, telebot.types.User):
|
|
97
|
+
return self._sender.language_code
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
@cached_property
|
|
101
|
+
def is_premium(self) -> bool:
|
|
102
|
+
"""Whether the user has an active Telegram Premium subscription.
|
|
103
|
+
Always `False` for chats and bots.
|
|
104
|
+
"""
|
|
105
|
+
if isinstance(self._sender, telebot.types.User):
|
|
106
|
+
return bool(self._sender.is_premium)
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
@cached_property
|
|
110
|
+
def added_to_attachment_menu(self) -> bool:
|
|
111
|
+
"""Whether this bot has been added to the user's attachment menu.
|
|
112
|
+
Always `False` for chats.
|
|
113
|
+
"""
|
|
114
|
+
if isinstance(self._sender, telebot.types.User):
|
|
115
|
+
return bool(self._sender.added_to_attachment_menu)
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def chat_type(self) -> Literal["private", "group", "supergroup", "channel"]:
|
|
120
|
+
"""Type of the chat the message was sent in."""
|
|
121
|
+
return self._message.chat.type # pyright: ignore[reportReturnType]
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def bio(self) -> str | None:
|
|
125
|
+
"""Bio of the user or description of the chat, as set in Telegram."""
|
|
126
|
+
return self._message.chat.bio
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def birthdate(self) -> telebot.types.Birthdate | None:
|
|
130
|
+
"""Birthdate of the user, if set and visible."""
|
|
131
|
+
return self._message.chat.birthdate
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def description(self) -> str | None:
|
|
135
|
+
"""Description of the group or channel. `None` for private chats."""
|
|
136
|
+
return self._message.chat.description
|
|
137
|
+
|
|
138
|
+
# ── Computed helpers ──────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
@cached_property
|
|
141
|
+
def full_name(self) -> str | None:
|
|
142
|
+
"""Full name of the user (`first_name + last_name`), or the chat title.
|
|
143
|
+
Returns `None` if neither is available.
|
|
144
|
+
"""
|
|
145
|
+
if isinstance(self._sender, telebot.types.User):
|
|
146
|
+
return self._sender.full_name
|
|
147
|
+
return " ".join(filter(None, [self.first_name, self.last_name])) or None
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def mention(self) -> str:
|
|
151
|
+
"""
|
|
152
|
+
A `tg://user?id=` deep link that mentions the user in any context,
|
|
153
|
+
even without a username.
|
|
154
|
+
"""
|
|
155
|
+
return f"tg://user?id={self.id}"
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def is_private(self) -> bool:
|
|
159
|
+
"""Whether the message was sent in a private chat."""
|
|
160
|
+
return self.chat_type == "private"
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def is_group(self) -> bool:
|
|
164
|
+
"""Whether the message was sent in a group chat."""
|
|
165
|
+
return self.chat_type == "group"
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def is_supergroup(self) -> bool:
|
|
169
|
+
"""Whether the message was sent in a supergroup."""
|
|
170
|
+
return self.chat_type == "supergroup"
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def is_channel(self) -> bool:
|
|
174
|
+
"""Whether the message was sent in a channel."""
|
|
175
|
+
return self.chat_type == "channel"
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def avatar(self) -> str | None:
|
|
179
|
+
"""
|
|
180
|
+
File ID of the user's most recent profile photo, or `None` if not set.
|
|
181
|
+
Makes an API call each time — cache the result if called frequently.
|
|
182
|
+
"""
|
|
183
|
+
photos = self.bot.get_user_profile_photos(self.id, limit=1)
|
|
184
|
+
if photos.total_count == 0:
|
|
185
|
+
return None
|
|
186
|
+
return photos.photos[0].file_id
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def profile_photos_count(self) -> int:
|
|
190
|
+
"""
|
|
191
|
+
Total number of profile photos the user has set.
|
|
192
|
+
Makes an API call each time.
|
|
193
|
+
"""
|
|
194
|
+
return self.bot.get_user_profile_photos(self.id).total_count
|
|
195
|
+
|
|
196
|
+
# ── Dunder ────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
def __repr__(self) -> str:
|
|
199
|
+
return f"User(id={self.id}, username={self.username!r})"
|
|
@@ -63,9 +63,7 @@ class EntryHandler(telekit.Handler):
|
|
|
63
63
|
# Priority:
|
|
64
64
|
# 1. Previous response (internal database)
|
|
65
65
|
# 2. Telegram username (fallback)
|
|
66
|
-
name: str | None = self._user_data.get_name(
|
|
67
|
-
default=self.user.username
|
|
68
|
-
)
|
|
66
|
+
name: str | None = self._user_data.get_name() or self.user.username
|
|
69
67
|
|
|
70
68
|
if name:
|
|
71
69
|
self.chain.set_entry_suggestions([name])
|