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.
Files changed (71) hide show
  1. {telekit-2.2.0a1/telekit.egg-info → telekit-2.2.0a3}/PKG-INFO +44 -10
  2. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_chain.py +1 -4
  3. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_chain_base.py +6 -0
  4. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_chain_entry_logic.py +12 -0
  5. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_chain_inline_keyboards_logic.py +41 -20
  6. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_inline_buttons.py +132 -4
  7. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/mixin.py +2 -2
  8. telekit-2.2.0a3/telekit/_user.py +199 -0
  9. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_version.py +1 -1
  10. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/entry.py +1 -3
  11. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/pages.py +12 -5
  12. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/inline_buttons.py +2 -0
  13. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/senders.py +277 -66
  14. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/types.py +43 -30
  15. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/utils.py +25 -0
  16. {telekit-2.2.0a1 → telekit-2.2.0a3/telekit.egg-info}/PKG-INFO +44 -10
  17. telekit-2.2.0a1/telekit/_user.py +0 -116
  18. {telekit-2.2.0a1 → telekit-2.2.0a3}/LICENSE +0 -0
  19. {telekit-2.2.0a1 → telekit-2.2.0a3}/README.md +0 -0
  20. {telekit-2.2.0a1 → telekit-2.2.0a3}/setup.cfg +0 -0
  21. {telekit-2.2.0a1 → telekit-2.2.0a3}/setup.py +0 -0
  22. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/__init__.py +0 -0
  23. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_buildtext/__init__.py +0 -0
  24. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_buildtext/formatter.py +0 -0
  25. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_buildtext/styles.py +0 -0
  26. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_callback_query_handler.py +0 -0
  27. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_chapters/__init__.py +0 -0
  28. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_chapters/chapters.py +0 -0
  29. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_handler.py +0 -0
  30. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_init.py +0 -0
  31. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_input_handler.py +0 -0
  32. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_logger.py +0 -0
  33. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_on.py +0 -0
  34. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_snapvault/__init__.py +0 -0
  35. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_snapvault/snapcode.py +0 -0
  36. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_snapvault/snapvault.py +0 -0
  37. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_state.py +0 -0
  38. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/__init__.py +0 -0
  39. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/parser/__init__.py +0 -0
  40. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/parser/builder.py +0 -0
  41. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/parser/canvas_parser.py +0 -0
  42. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/parser/lexer.py +0 -0
  43. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/parser/nodes.py +0 -0
  44. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/parser/parser.py +0 -0
  45. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/parser/token.py +0 -0
  46. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/telekit_dsl.py +0 -0
  47. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_telekit_dsl/telekit_orm.py +0 -0
  48. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/_timeout.py +0 -0
  49. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/dices.py +0 -0
  50. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/__init__.py +0 -0
  51. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/__init__.py +0 -0
  52. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/complete_hotel.py +0 -0
  53. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/counter.py +0 -0
  54. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/dsl.py +0 -0
  55. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/faq.py +0 -0
  56. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/hotel.py +0 -0
  57. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/on_text.py +0 -0
  58. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/pyapi.py +0 -0
  59. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/qr.py +0 -0
  60. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/quiz.py +0 -0
  61. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/spells.py +0 -0
  62. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/start.py +0 -0
  63. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_handlers/text_document.py +0 -0
  64. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/example/example_server.py +0 -0
  65. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/parameters.py +0 -0
  66. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/server.py +0 -0
  67. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit/styles.py +0 -0
  68. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit.egg-info/SOURCES.txt +0 -0
  69. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit.egg-info/dependency_links.txt +0 -0
  70. {telekit-2.2.0a1 → telekit-2.2.0a3}/telekit.egg-info/requires.txt +0 -0
  71. {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.0a1
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.0a1
381
+ # Changes in version 2.2.0a3
382
382
 
383
383
  ## New Button Types
384
384
 
385
- | **Name** | **Description** |
386
- |----------|-----------------|
387
- | `AlertButton` | A callback button that shows a popup alert when pressed. |
388
- | `NotificationButton` | A callback button that shows a brief notification at the top of the chat screen. |
389
-
390
- ## User Refactor
391
-
392
- `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.
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] = self._get_callback_with_argument(func, value)
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] = self._get_callback_with_argument(func, value)
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 _get_callback_with_argument(self, func: Callable, argument: Any, query_answer: tuple[str, bool] | None = None) -> Callable[[CallbackQuery], None]:
318
- def callback(call: CallbackQuery) -> None:
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
- func(argument)
345
+ callback(argument)
321
346
  self._answer_callback_query(call, query_answer)
322
347
 
323
- return callback
348
+ return invoker
324
349
 
325
- def _get_callback(self, func: Callable[..., None], query_answer: tuple[str, bool] | None = None) -> Callable[[CallbackQuery], None]:
326
- def callback(call: CallbackQuery):
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
- func()
353
+ callback()
329
354
  self._answer_callback_query(call, query_answer)
330
355
 
331
- return callback
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.pyerror(RuntimeError(message))
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.error("🤷 Something went wrong...", message)
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})"
@@ -3,4 +3,4 @@
3
3
  # PyPI history: https://pypi.org/project/telekit/#history
4
4
  # ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
5
5
 
6
- __version__ = "2.2.0a1"
6
+ __version__ = "2.2.0a3"
@@ -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])