telekit 2.0.1__tar.gz → 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {telekit-2.0.1/telekit.egg-info → telekit-2.1.0}/PKG-INFO +19 -67
  2. {telekit-2.0.1 → telekit-2.1.0}/README.md +1 -1
  3. {telekit-2.0.1 → telekit-2.1.0}/telekit/_buildtext/styles.py +4 -22
  4. {telekit-2.0.1 → telekit-2.1.0}/telekit/_on.py +84 -70
  5. telekit-2.1.0/telekit/_version.py +1 -0
  6. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/example_handlers/__init__.py +2 -1
  7. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/example_handlers/on_text.py +2 -0
  8. telekit-2.1.0/telekit/example/example_handlers/qr.py +86 -0
  9. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/example_handlers/start.py +5 -1
  10. {telekit-2.0.1 → telekit-2.1.0}/telekit/utils.py +141 -1
  11. {telekit-2.0.1 → telekit-2.1.0/telekit.egg-info}/PKG-INFO +19 -67
  12. {telekit-2.0.1 → telekit-2.1.0}/telekit.egg-info/SOURCES.txt +1 -0
  13. telekit-2.0.1/telekit/_version.py +0 -1
  14. {telekit-2.0.1 → telekit-2.1.0}/LICENSE +0 -0
  15. {telekit-2.0.1 → telekit-2.1.0}/setup.cfg +0 -0
  16. {telekit-2.0.1 → telekit-2.1.0}/setup.py +0 -0
  17. {telekit-2.0.1 → telekit-2.1.0}/telekit/__init__.py +0 -0
  18. {telekit-2.0.1 → telekit-2.1.0}/telekit/_buildtext/__init__.py +0 -0
  19. {telekit-2.0.1 → telekit-2.1.0}/telekit/_buildtext/formatter.py +0 -0
  20. {telekit-2.0.1 → telekit-2.1.0}/telekit/_callback_query_handler.py +0 -0
  21. {telekit-2.0.1 → telekit-2.1.0}/telekit/_chain.py +0 -0
  22. {telekit-2.0.1 → telekit-2.1.0}/telekit/_chain_base.py +0 -0
  23. {telekit-2.0.1 → telekit-2.1.0}/telekit/_chain_entry_logic.py +0 -0
  24. {telekit-2.0.1 → telekit-2.1.0}/telekit/_chain_inline_keyboards_logic.py +0 -0
  25. {telekit-2.0.1 → telekit-2.1.0}/telekit/_chapters/__init__.py +0 -0
  26. {telekit-2.0.1 → telekit-2.1.0}/telekit/_chapters/chapters.py +0 -0
  27. {telekit-2.0.1 → telekit-2.1.0}/telekit/_handler.py +0 -0
  28. {telekit-2.0.1 → telekit-2.1.0}/telekit/_init.py +0 -0
  29. {telekit-2.0.1 → telekit-2.1.0}/telekit/_inline_buttons.py +0 -0
  30. {telekit-2.0.1 → telekit-2.1.0}/telekit/_input_handler.py +0 -0
  31. {telekit-2.0.1 → telekit-2.1.0}/telekit/_logger.py +0 -0
  32. {telekit-2.0.1 → telekit-2.1.0}/telekit/_snapvault/__init__.py +0 -0
  33. {telekit-2.0.1 → telekit-2.1.0}/telekit/_snapvault/snapcode.py +0 -0
  34. {telekit-2.0.1 → telekit-2.1.0}/telekit/_snapvault/snapvault.py +0 -0
  35. {telekit-2.0.1 → telekit-2.1.0}/telekit/_state.py +0 -0
  36. {telekit-2.0.1 → telekit-2.1.0}/telekit/_telekit_dsl/__init__.py +0 -0
  37. {telekit-2.0.1 → telekit-2.1.0}/telekit/_telekit_dsl/mixin.py +0 -0
  38. {telekit-2.0.1 → telekit-2.1.0}/telekit/_telekit_dsl/parser/__init__.py +0 -0
  39. {telekit-2.0.1 → telekit-2.1.0}/telekit/_telekit_dsl/parser/builder.py +0 -0
  40. {telekit-2.0.1 → telekit-2.1.0}/telekit/_telekit_dsl/parser/canvas_parser.py +0 -0
  41. {telekit-2.0.1 → telekit-2.1.0}/telekit/_telekit_dsl/parser/lexer.py +0 -0
  42. {telekit-2.0.1 → telekit-2.1.0}/telekit/_telekit_dsl/parser/nodes.py +0 -0
  43. {telekit-2.0.1 → telekit-2.1.0}/telekit/_telekit_dsl/parser/parser.py +0 -0
  44. {telekit-2.0.1 → telekit-2.1.0}/telekit/_telekit_dsl/parser/token.py +0 -0
  45. {telekit-2.0.1 → telekit-2.1.0}/telekit/_telekit_dsl/telekit_dsl.py +0 -0
  46. {telekit-2.0.1 → telekit-2.1.0}/telekit/_telekit_dsl/telekit_orm.py +0 -0
  47. {telekit-2.0.1 → telekit-2.1.0}/telekit/_timeout.py +0 -0
  48. {telekit-2.0.1 → telekit-2.1.0}/telekit/_user.py +0 -0
  49. {telekit-2.0.1 → telekit-2.1.0}/telekit/dices.py +0 -0
  50. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/__init__.py +0 -0
  51. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/example_handlers/complete_hotel.py +0 -0
  52. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/example_handlers/counter.py +0 -0
  53. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/example_handlers/dsl.py +0 -0
  54. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/example_handlers/entry.py +0 -0
  55. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/example_handlers/faq.py +0 -0
  56. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/example_handlers/hotel.py +0 -0
  57. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/example_handlers/pages.py +0 -0
  58. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/example_handlers/pyapi.py +0 -0
  59. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/example_handlers/quiz.py +0 -0
  60. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/example_handlers/spells.py +0 -0
  61. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/example_handlers/text_document.py +0 -0
  62. {telekit-2.0.1 → telekit-2.1.0}/telekit/example/example_server.py +0 -0
  63. {telekit-2.0.1 → telekit-2.1.0}/telekit/inline_buttons.py +0 -0
  64. {telekit-2.0.1 → telekit-2.1.0}/telekit/parameters.py +0 -0
  65. {telekit-2.0.1 → telekit-2.1.0}/telekit/senders.py +0 -0
  66. {telekit-2.0.1 → telekit-2.1.0}/telekit/server.py +0 -0
  67. {telekit-2.0.1 → telekit-2.1.0}/telekit/styles.py +0 -0
  68. {telekit-2.0.1 → telekit-2.1.0}/telekit/types.py +0 -0
  69. {telekit-2.0.1 → telekit-2.1.0}/telekit.egg-info/dependency_links.txt +0 -0
  70. {telekit-2.0.1 → telekit-2.1.0}/telekit.egg-info/requires.txt +0 -0
  71. {telekit-2.0.1 → telekit-2.1.0}/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.0.1
3
+ Version: 2.1.0
4
4
  Summary: Declarative, developer-friendly library for building Telegram bots
5
5
  Home-page: https://github.com/Romashkaa/telekit
6
6
  Author: romashka
@@ -62,7 +62,7 @@ telekit.Server("BOT_TOKEN").polling()
62
62
 
63
63
  > Send "Hello!" with a photo on `/start`
64
64
 
65
- Telekit comes with a [built-in DSL](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial/11_telekit_dsl.md), allowing developers to create fully interactive bots with minimal code. It also integrates **Jinja**, giving you loops, conditionals, expressions, and filters to generate dynamic content.
65
+ Telekit comes with a [built-in DSL](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial/11_telekit_dsl.md), allowing developers to create fully interactive bots with minimal code. It also integrates [**Jinja**](https://github.com/Romashkaa/telekit/blob/main/docs/examples/jinja_engine.md), giving you loops, conditionals, expressions, and filters to generate dynamic content.
66
66
 
67
67
  ```js
68
68
  @ main {
@@ -378,68 +378,20 @@ It tries to make Telegram bot development easier.
378
378
 
379
379
  ---
380
380
 
381
- # Changes in version 2.0.1
382
-
383
- ## Obsidian Canvas Mode *(Beta)*
384
- Build bots visually — no DSL required. Design your dialog flow as a flowchart in Obsidian and connect it to Telekit with a single method call.
385
-
386
- - Added `analyze_canvas()` to `DSLHandler` — loads a `.canvas` file as an executable model
387
-
388
- ## Styles Improvements
389
- The style system has been rewritten from the ground up. Previously, automatic escaping was quite unpredictable — users couldn't always tell when a string would be escaped or not. That's over.
390
-
391
- - Migrated markdown rendering to **MarkdownV2**
392
- - Added `Stack` style for structured list rendering
393
- - Added `Mention` style
394
- - Renamed `Sanitize` to `Escape`
395
- - Renamed `NoSanitize` to `Raw`
396
- - Added `utils.sanitize_markdown()` escapes all MarkdownV2 special characters outside valid entities
397
- - Added `utils.adapt_markdown()` converts `**bold**` / `*italic*` to Telegram-compatible format
398
- - Added `utils.telegramify_markdown()` — convenience pipeline: adapt + sanitize
399
- - `StyleFormatter` renamed to `TextEntity`
400
-
401
- ## Inline Keyboards
402
-
403
- Inline keyboards got a proper type system. Previously, any string value in the keyboard dict was treated as a link. Now every button type is explicit.
404
-
405
- - Introduced `InlineButton` base class with subclasses:
406
- `LinkButton`, `WebAppButton`, `SuggestButton`, `CopyTextButton`, `CallbackButton`
407
- - `CallbackButton` — pass arguments to callbacks and set visual style
408
- - Added `ButtonStyle` enum — style your inline buttons (Bot API 9.4)
409
- - Added `inline_choice` / `set_inline_choice` methods
410
- - `set_entry_suggestions` now warns when Telegram Bot API limits are exceeded
411
- - It's no longer pass `Message` object to callbacks
412
- - Reduced closure size and improved callback performance
413
-
414
- ## Chain Improvements
415
- - Added `set_break_only_on_match()` — control whether chain interrupts only on valid handler match, not random input
416
- - Added `set_break_on_commands()` — control whether commands take priority over entry handlers
417
- - Improved file extension validation via **pathlib**
418
- - `entry_*` handlers no longer pass `Message` by default — use `include_message=True` to restore
419
-
420
- ## CallbackQueryHandler
421
- - Callbacks now invoked directly — no virtual message conversion, faster response times
422
- - Native alert support for expired or inactive buttons
423
- - Added `set_invalid_data_answer()` and `set_button_is_no_active_answer()` for global customization
424
-
425
- ## DSL Improvements
426
- - `jinja_env` is now private — use `get_jinja_env()` instead
427
- - `TelekitDSL` class:
428
- - All `from_*` factory methods now use `type()` for class creation
429
-
430
- ## Fixes
431
- - Fixed one-time trigger bug — triggers like `on.text` now fire reliably across multiple messages while a chain timeout is active
432
-
433
- ## Other
434
- - Added `utils.read_token()` and `utils.read_canvas_path()` — clean helpers for reading credentials from files
435
- - Added `utils.format_file_size()`
436
- - Added `set_license_text` to senders
437
- - Added `format_size` to `TextDocument`
438
- - Added `TelekitState` class
439
- - Bumped `pyTelegramBotAPI` to **v4.31.0**
440
- - Added GPL v3 License
441
-
442
- ## 🚀 Upcoming
443
-
444
- - New triggers: `document`, `text_document`
445
- - Telekit DSL: shorter notation and button attributes with style, alert, ... support
381
+ # Changes in version 2.1.0
382
+
383
+ ## Trigger Improvements
384
+
385
+ | **Name** | **Description** |
386
+ |----------|-----------------|
387
+ | `on.text` | *BugFix:* Commands are no longer matched as text triggers. |
388
+
389
+ - Internal refactor of the `On` class.
390
+
391
+ ## New Utils
392
+
393
+ | **Name** | **Description** |
394
+ |----------|-----------------|
395
+ | `make_bot_link` | Builds a `t.me` URL to a bot with an optional deep-link payload. |
396
+ | `make_user_link` | Builds a `t.me` URL to a user with an optional pre-filled message. |
397
+ | `make_qrcode` | Generates a [QR code](./docs/examples/qr_gen.md) URL from any text or link. |
@@ -26,7 +26,7 @@ telekit.Server("BOT_TOKEN").polling()
26
26
 
27
27
  > Send "Hello!" with a photo on `/start`
28
28
 
29
- Telekit comes with a [built-in DSL](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial/11_telekit_dsl.md), allowing developers to create fully interactive bots with minimal code. It also integrates **Jinja**, giving you loops, conditionals, expressions, and filters to generate dynamic content.
29
+ Telekit comes with a [built-in DSL](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial/11_telekit_dsl.md), allowing developers to create fully interactive bots with minimal code. It also integrates [**Jinja**](https://github.com/Romashkaa/telekit/blob/main/docs/examples/jinja_engine.md), giving you loops, conditionals, expressions, and filters to generate dynamic content.
30
30
 
31
31
  ```js
32
32
  @ main {
@@ -21,6 +21,8 @@ from urllib.parse import quote
21
21
 
22
22
  import telebot.formatting
23
23
 
24
+ import telekit.utils
25
+
24
26
  from .formatter import TextEntity, EasyTextEntity, StaticTextEntity, EasyTextEntityWithPostRender, Group
25
27
 
26
28
 
@@ -516,20 +518,10 @@ class UserLink(Link):
516
518
  `Documentation <https://github.com/Romashkaa/telekit/blob/main/docs/tutorial2/6_styles.md>`_ · on GitHub
517
519
  """
518
520
  def __init__(self, *content, username: str, text: str | None = None, escape: bool = True, sep: Union["TextEntity", str] = ""):
519
- url: str = self.gen_link(username, text)
521
+ url: str = telekit.utils.make_user_link(username, text)
520
522
 
521
523
  super().__init__(*content, url=url, escape=escape, sep=sep)
522
524
 
523
- @staticmethod
524
- def gen_link(username: str, text: str | None = None) -> str:
525
- username = username.lstrip("@")
526
-
527
- if text is None:
528
- return f"https://t.me/{username}"
529
- else:
530
- encoded_text: str = quote(text, safe="")
531
- return f"https://t.me/{username}?text={encoded_text}"
532
-
533
525
 
534
526
  class BotLink(Link):
535
527
  """
@@ -559,20 +551,10 @@ class BotLink(Link):
559
551
  """
560
552
 
561
553
  def __init__(self, *content, username: str, start: str | None = None, escape: bool = True, sep: Union["TextEntity", str] = ""):
562
- url: str = self.gen_link(username, start)
554
+ url: str = telekit.utils.make_bot_link(username, start)
563
555
 
564
556
  super().__init__(*content, url=url, escape=escape, sep=sep)
565
557
 
566
- @staticmethod
567
- def gen_link(username: str, start: str | None = None) -> str:
568
- username = username.lstrip("@")
569
-
570
- if start is None:
571
- return f"https://t.me/{username}"
572
- else:
573
- encoded_start = quote(start, safe="")
574
- return f"https://t.me/{username}?start={encoded_start}"
575
-
576
558
  class EncodeURL(StaticTextEntity):
577
559
  """
578
560
  URL-encodes the provided content string.
@@ -23,6 +23,7 @@ import shlex
23
23
  import re
24
24
 
25
25
  import telebot
26
+ import telebot.types
26
27
 
27
28
  from . import parameters
28
29
  from ._logger import logger
@@ -32,7 +33,7 @@ if typing.TYPE_CHECKING:
32
33
  from telekit._handler import Handler # only for type hints
33
34
 
34
35
  # --------------------------------------------------------
35
- # Event Handler
36
+ # Invoker
36
37
  # --------------------------------------------------------
37
38
 
38
39
  class Invoker:
@@ -40,12 +41,11 @@ class Invoker:
40
41
  • [See Documentation on GitHub](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial2/3_triggers.md)
41
42
  """
42
43
 
43
- def __init__(self, decorator: Callable[[Callable], Callable | None], handler: type["Handler"]):
44
+ def __init__(self, registrar: Callable[[Callable], Callable | None], handler: type["Handler"]):
44
45
  """
45
- Wraps an existing decorator (like cls.on_text)
46
- to add callback/trigger functionality.
46
+ Wraps a registrar to add callback/trigger functionality.
47
47
  """
48
- self._decorator: Callable = decorator
48
+ self._register: Callable = registrar
49
49
  self._Handler = handler
50
50
 
51
51
  def __call__(self, func: Callable):
@@ -58,7 +58,7 @@ class Invoker:
58
58
  cls(message).handle(*args, **kwargs)
59
59
  ```
60
60
  """
61
- return self._decorator(func)
61
+ return self._register(func)
62
62
 
63
63
  def invoke(self, callback: Callable, pass_args: bool = True):
64
64
  """
@@ -74,10 +74,10 @@ class Invoker:
74
74
  else:
75
75
  callback(self._Handler(message))
76
76
 
77
- self._decorator(handler)
77
+ self._register(handler)
78
78
 
79
79
  # --------------------------------------------------------
80
- # On Handlers
80
+ # Triggers
81
81
  # --------------------------------------------------------
82
82
 
83
83
  class On:
@@ -149,7 +149,7 @@ class On:
149
149
  Returns:
150
150
  Invoker: An invoker object allowing `.invoke()` or @decorator-style usage.
151
151
  """
152
- original_decorator = self.bot.message_handler(
152
+ register_trigger = self.bot.message_handler(
153
153
  commands=commands,
154
154
  regexp=regexp,
155
155
  func=func,
@@ -159,18 +159,18 @@ class On:
159
159
  )
160
160
 
161
161
  def decorator(handler: Callable[..., typing.Any]):
162
- def wrapped(message):
162
+ def trigger(message):
163
163
  if whitelist is not None and message.chat.id not in whitelist:
164
164
  return
165
165
  return handler(message)
166
166
 
167
- return original_decorator(wrapped)
167
+ return register_trigger(trigger)
168
168
 
169
169
  return Invoker(decorator, self.handler)
170
170
 
171
171
  def text(
172
172
  self,
173
- *patterns: str,
173
+ *patterns: str,
174
174
  chat_types: list[str] | None = None,
175
175
  whitelist: list[int] | None = None
176
176
  ):
@@ -225,11 +225,14 @@ class On:
225
225
  re.IGNORECASE
226
226
  ))
227
227
 
228
- def _filter_func(message: telebot.types.Message) -> bool:
229
- if whitelist is not None and message.chat.id not in whitelist:
228
+ def _filter(message: telebot.types.Message) -> bool:
229
+ if not message.text:
230
230
  return False
231
231
 
232
- if not message.text:
232
+ if message.text.startswith("/"):
233
+ return False
234
+
235
+ if whitelist is not None and message.chat.id not in whitelist:
233
236
  return False
234
237
 
235
238
  for cregex in compiled:
@@ -239,25 +242,37 @@ class On:
239
242
 
240
243
  return False
241
244
 
242
- def decorator(func: Callable):
243
- @self.bot.message_handler(func=_filter_func, chat_types=chat_types)
244
- def _(message):
245
+ def register(func: Callable): # pyright: ignore[reportRedeclaration]
246
+ @self.bot.message_handler(func=_filter, chat_types=chat_types)
247
+ def trigger(message):
245
248
  for cregex in compiled:
246
249
  match = cregex.match(message.text)
247
250
  if match:
248
251
  func(message, **match.groupdict())
249
252
  return
250
-
251
- return func
253
+
254
+ return trigger
252
255
  else:
253
- def decorator(func: Callable):
254
- @self.bot.message_handler(content_types=["text"], chat_types=chat_types)
255
- def _(message):
256
- if whitelist is not None and message.chat.id not in whitelist:
257
- return
258
- func(message)
259
- return func
260
- return Invoker(decorator, self.handler)
256
+ def _filter(message: telebot.types.Message) -> bool:
257
+ if not message.text:
258
+ return False
259
+
260
+ if message.text.startswith("/"):
261
+ return False
262
+
263
+ if whitelist is not None and message.chat.id not in whitelist:
264
+ return False
265
+
266
+ return True
267
+
268
+ def register(func: Callable):
269
+ return self.bot.message_handler(
270
+ content_types=["text"],
271
+ func=_filter,
272
+ chat_types=chat_types
273
+ )(func)
274
+
275
+ return Invoker(register, self.handler)
261
276
 
262
277
  def command(
263
278
  self,
@@ -305,14 +320,13 @@ class On:
305
320
  Returns:
306
321
  Invoker: An invoker object allowing `.invoke()` or decorator-style usage.
307
322
  """
308
- original_decorator = self.bot.message_handler(
309
- commands=[c.lstrip("/") for c in commands],
310
- chat_types=chat_types,
311
- **kwargs
312
- )
313
-
314
- def decorator(handler: Callable[..., typing.Any]):
315
- def wrapped(message):
323
+ def register(handler: Callable[..., typing.Any]):
324
+ @self.bot.message_handler(
325
+ commands=[c.lstrip("/") for c in commands],
326
+ chat_types=chat_types,
327
+ **kwargs
328
+ )
329
+ def trigger(message):
316
330
  if whitelist is not None and message.chat.id not in whitelist:
317
331
  return
318
332
  if params:
@@ -320,9 +334,9 @@ class On:
320
334
 
321
335
  return handler(message)
322
336
 
323
- return original_decorator(wrapped)
337
+ return trigger
324
338
 
325
- return Invoker(decorator, self.handler)
339
+ return Invoker(register, self.handler)
326
340
 
327
341
  def _analyze_params(self, text: str, types: list[parameters.Parameter]) -> list:
328
342
  values: list[str] = shlex.split(text)[1:] # skip the command name
@@ -380,21 +394,20 @@ class On:
380
394
  Returns:
381
395
  Invoker: An invoker object allowing `.invoke()` or decorator-style usage.
382
396
  """
383
- original_decorator = self.bot.message_handler(
384
- regexp=regexp,
385
- chat_types=chat_types,
386
- **kwargs
387
- )
388
-
389
- def decorator(handler: Callable[..., typing.Any]):
390
- def wrapped(message):
397
+ def register(handler: Callable[..., typing.Any]):
398
+ @self.bot.message_handler(
399
+ regexp=regexp,
400
+ chat_types=chat_types,
401
+ **kwargs
402
+ )
403
+ def trigger(message):
391
404
  if whitelist is not None and message.chat.id not in whitelist:
392
405
  return
393
406
  return handler(message)
394
407
 
395
- return original_decorator(wrapped)
408
+ return trigger
396
409
 
397
- return Invoker(decorator, self.handler)
410
+ return Invoker(register, self.handler)
398
411
 
399
412
  def photo(
400
413
  self,
@@ -422,8 +435,7 @@ class On:
422
435
  ```
423
436
  ---
424
437
 
425
- Triggers:
426
- - By receiving a photo message (content_type="photo")
438
+ Triggers when receive a photo (content_type="photo")
427
439
 
428
440
  Filters:
429
441
  - By chat type(s) (via `chat_types` argument)
@@ -438,21 +450,20 @@ class On:
438
450
  Returns:
439
451
  Invoker: An invoker object allowing `.invoke()` or decorator-style usage.
440
452
  """
441
- original_decorator = self.bot.message_handler(
442
- content_types=["photo"],
443
- chat_types=chat_types,
444
- **kwargs
445
- )
446
-
447
- def decorator(handler: Callable[..., typing.Any]):
448
- def wrapped(message):
453
+ def register(handler: Callable[..., typing.Any]):
454
+ @self.bot.message_handler(
455
+ content_types=["photo"],
456
+ chat_types=chat_types,
457
+ **kwargs
458
+ )
459
+ def trigger(message):
449
460
  if whitelist is not None and message.chat.id not in whitelist:
450
461
  return
451
462
  return handler(message)
452
463
 
453
- return original_decorator(wrapped)
464
+ return trigger
454
465
 
455
- return Invoker(decorator, self.handler)
466
+ return Invoker(register, self.handler)
456
467
 
457
468
  def func(
458
469
  self,
@@ -508,17 +519,20 @@ class On:
508
519
  return False
509
520
  return bool(func(message))
510
521
 
511
- original_decorator = self.bot.message_handler(
512
- func=_filter,
513
- chat_types=chat_types,
514
- **kwargs
515
- )
516
-
517
522
  def decorator(handler: Callable[..., typing.Any]):
518
523
  if not invoke_args and not invoke_kwargs:
519
- return original_decorator(handler)
520
-
521
- def _wrap_args(message, *args, **kwargs):
524
+ return self.bot.message_handler(
525
+ func=_filter,
526
+ chat_types=chat_types,
527
+ **kwargs
528
+ )(handler)
529
+
530
+ @self.bot.message_handler(
531
+ func=_filter,
532
+ chat_types=chat_types,
533
+ **kwargs
534
+ )
535
+ def trigger(message, *args, **kwargs):
522
536
  final_args = args
523
537
  final_kwargs = kwargs
524
538
 
@@ -530,6 +544,6 @@ class On:
530
544
 
531
545
  return handler(message, *final_args, **final_kwargs)
532
546
 
533
- return original_decorator(_wrap_args)
547
+ return trigger
534
548
 
535
549
  return Invoker(decorator, self.handler)
@@ -0,0 +1 @@
1
+ __version__ = "2.1.0"
@@ -11,5 +11,6 @@ from . import (
11
11
  quiz,
12
12
  hotel,
13
13
  complete_hotel,
14
- text_document
14
+ text_document,
15
+ qr
15
16
  )
@@ -13,6 +13,8 @@ class OnTextHandler(telekit.Handler):
13
13
  "I'm {age} years old"
14
14
  ).invoke(cls.handle_name_age)
15
15
 
16
+ cls.on.text().invoke(cls.handle)
17
+
16
18
  cls.on.command("on_text").invoke(cls.handle)
17
19
 
18
20
  # ------------------------------------------
@@ -0,0 +1,86 @@
1
+ from typing import Any
2
+
3
+ import telekit
4
+ from telekit.styles import Quote
5
+
6
+
7
+ class QRCode:
8
+ def __init__(self) -> None:
9
+ self.text: str | None = None
10
+ self.caption: str | None = None
11
+
12
+ def get_url(self) -> str:
13
+ kwargs: dict[str, Any] = {"text": self.text or "Example"}
14
+ if self.caption is not None:
15
+ kwargs["caption"] = self.caption
16
+ return telekit.utils.make_qrcode(**kwargs)
17
+
18
+
19
+ class QRHandler(telekit.Handler):
20
+
21
+ @classmethod
22
+ def init_handler(cls) -> None:
23
+ cls.on.command("start").invoke(cls.handle)
24
+
25
+ def handle(self) -> None:
26
+ self.qrcode = QRCode()
27
+ self.display_qrcode()
28
+
29
+ # ── entry points ──────────────────────────────────────────────
30
+
31
+ def change_text(self) -> None:
32
+ self.chain.sender.set_title("⌨️ Enter text")
33
+ self.chain.sender.set_message("Send any text or URL to encode into a QR code")
34
+ self.chain.set_entry_text(self.set_text, delete_user_response=True)
35
+
36
+ keyboard: dict[str, Any] = {}
37
+ keyboard["« Back"] = self.display_qrcode
38
+
39
+ if self.qrcode.text:
40
+ keyboard["✕ Remove"] = self.set_text
41
+
42
+ self.chain.set_inline_keyboard(keyboard, row_width=2)
43
+ self.chain.edit()
44
+
45
+ def change_caption(self) -> None:
46
+ self.chain.sender.set_title("⌨️ Enter caption")
47
+ self.chain.sender.set_message("Send a caption to display below the QR code")
48
+ self.chain.set_entry_text(self.set_caption, delete_user_response=True)
49
+
50
+ keyboard: dict[str, Any] = {}
51
+ keyboard["« Back"] = self.display_qrcode
52
+
53
+ if self.qrcode.caption:
54
+ keyboard["✕ Remove"] = self.set_caption
55
+
56
+ self.chain.set_inline_keyboard(keyboard, row_width=2)
57
+ self.chain.edit()
58
+
59
+ # ── entry handlers ──────────────────────────────────────────────────
60
+
61
+ def set_text(self, text: str | None = None) -> None:
62
+ self.qrcode.text = text
63
+ self.display_qrcode()
64
+
65
+ def set_caption(self, caption: str | None = None) -> None:
66
+ self.qrcode.caption = caption
67
+ self.display_qrcode()
68
+
69
+ # ── display QR code ───────────────────────────────────────────────────
70
+
71
+ def display_qrcode(self) -> None:
72
+ url: str = self.qrcode.get_url()
73
+
74
+ self.chain.sender.set_photo(url)
75
+ self.chain.sender.set_text(
76
+ Quote(url, expandable=True),
77
+ "Use the buttons below to edit the QR code:"
78
+ )
79
+ self.chain.set_inline_keyboard(
80
+ {
81
+ "✏️ Edit Text": self.change_text,
82
+ "✏️ Edit Caption" if self.qrcode.caption else "➕ Add Caption": self.change_caption,
83
+ },
84
+ row_width=2
85
+ )
86
+ self.chain.edit()
@@ -22,11 +22,15 @@ class StartHandler(telekit.Handler):
22
22
  "🧮 Counter": "CounterHandler",
23
23
  "🧙‍♀️ Spells": "SpellsHandler",
24
24
  "⌨️ Entry": "EntryHandler",
25
+
25
26
  "📚 Telekit DSL Examples": "DSLExampleHandler",
27
+
26
28
  "📖 Pages": "PagesHandler",
27
29
  "🦻 On Text": "OnTextHandler",
28
30
  "📄 File Info": "TextDocumentHandler",
29
- }, row_width=[3, 1, 3]
31
+
32
+ "🖼️ QR Editor": "QRHandler",
33
+ }, row_width=[3, 1, 3, 1]
30
34
  )
31
35
  def handle_response(handler: str):
32
36
  self.handoff(handler).handle()
@@ -1,5 +1,8 @@
1
1
  import re
2
+
2
3
  from pathlib import Path
4
+ from urllib.parse import urlencode, quote
5
+ from typing import Literal
3
6
 
4
7
  ROOT_DIR = Path(__file__).resolve().parent # telekit/
5
8
 
@@ -100,7 +103,6 @@ def read_token(path: str = "token.txt") -> str:
100
103
  token, *_ = first_line.split()
101
104
  return token
102
105
 
103
-
104
106
  def read_canvas_path(path: str = "canvas_path.txt") -> str:
105
107
  """
106
108
  Read the ``.canvas`` file path from a file.
@@ -115,7 +117,145 @@ def read_canvas_path(path: str = "canvas_path.txt") -> str:
115
117
  """
116
118
  with open(path) as f:
117
119
  return f.readline().strip()
120
+
121
+ # ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
122
+ # Link Generators
123
+ # ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
124
+
125
+ def make_bot_link(botname: str, start: str | None = None) -> str:
126
+ """Return a t.me link to a bot.
127
+
128
+ >>> make_bot_link("UserNameOfBot")
129
+ "https://t.me/UserNameOfBot"
130
+
131
+ >>> make_bot_link("UserNameOfBot", start="Hello!")
132
+ "https://t.me/UserNameOfBot?start=Hello%21"
133
+
134
+ Args:
135
+ botname: Bot username with or without leading '@'.
136
+ start: Optional deep-link payload appended as ?start=.
137
+
138
+ Returns:
139
+ A t.me URL string.
140
+ """
141
+ botname = botname.lstrip("@")
142
+ if start is None:
143
+ return f"https://t.me/{botname}"
144
+ encoded_start = quote(start, safe="")
145
+ return f"https://t.me/{botname}?start={encoded_start}"
146
+
147
+ def make_user_link(username: str, text: str | None = None) -> str:
148
+ """Return a t.me link to a user.
149
+
150
+ >>> make_user_link("UserName")
151
+ "https://t.me/UserName"
152
+
153
+ >>> make_user_link("UserName", text="Hello!")
154
+ "https://t.me/UserName?text=Hello%21"
118
155
 
156
+ Args:
157
+ username: Telegram username with or without leading '@'.
158
+ text: Optional pre-filled message text appended as ?text=.
159
+
160
+ Returns:
161
+ A t.me URL string.
162
+ """
163
+ username = username.lstrip("@")
164
+ if text is None:
165
+ return f"https://t.me/{username}"
166
+ encoded_text = quote(text, safe="")
167
+ return f"https://t.me/{username}?text={encoded_text}"
168
+
169
+ def make_qrcode(
170
+ text: str,
171
+ *,
172
+ size: int | None = None,
173
+ margin: int | None = None,
174
+ dark: str | None = None,
175
+ light: str | None = None,
176
+ ec_level: Literal["L", "M", "Q", "H"] | None = None,
177
+ format: Literal["png", "svg", "base64"] | None = None,
178
+ center_image_url: str | None = None,
179
+ center_image_size_ratio: float | None = None,
180
+ center_image_width: int | None = None,
181
+ center_image_height: int | None = None,
182
+ caption: str | None = None,
183
+ caption_font_family: str | None = None,
184
+ caption_font_size: int | None = None,
185
+ caption_font_color: str | None = None,
186
+ ) -> str:
187
+ """Return a QuickChart URL that renders a QR code.
188
+
189
+ >>> make_qrcode("https://example.com")
190
+ 'https://quickchart.io/qr?text=https%3A%2F%2Fexample.com'
191
+
192
+ >>> make_qrcode("Hello", dark="ff0000", size=300)
193
+ 'https://quickchart.io/qr?text=Hello&size=300&dark=ff0000'
194
+
195
+ Use with sender:
196
+ >>> sender.set_photo(make_qrcode("https://example.com", caption="Scan Me"))
197
+
198
+ Check out the [QuickChart QR Code API](https://quickchart.io/documentation/qr-codes/)
199
+
200
+ Args:
201
+ text: Content to encode (URL or any string).
202
+ size: Width and height of the image in pixels.
203
+ margin: Whitespace around the QR image in modules.
204
+ dark: Hex color of dark cells, without '#'.
205
+ light: Hex color of light cells, without '#'.
206
+ Use "0000" for a transparent background.
207
+ ec_level: Error correction level — L, M, Q, or H.
208
+ format: Output format — png, svg, or base64.
209
+ center_image_url: URL of an image to display in the center.
210
+ Must be publicly accessible (PNG or JPG).
211
+ center_image_size_ratio: Float 0.0–1.0 — portion of QR area the
212
+ center image occupies. Keep below ~0.3.
213
+ center_image_width: Center image width in pixels.
214
+ center_image_height: Center image height in pixels.
215
+ caption: Caption text displayed below the QR code.
216
+ caption_font_family: Font family of the caption.
217
+ caption_font_size: Font size of the caption in pixels.
218
+ caption_font_color: Color of the caption — name or hex.
219
+
220
+ Returns:
221
+ A quickchart.io URL string.
222
+ """
223
+ params: dict = {"text": text}
224
+
225
+ if size is not None:
226
+ params["size"] = size
227
+ if margin is not None:
228
+ params["margin"] = margin
229
+ if dark is not None:
230
+ params["dark"] = dark
231
+ if light is not None:
232
+ params["light"] = light
233
+ if ec_level is not None:
234
+ params["ecLevel"] = ec_level
235
+ if format is not None:
236
+ params["format"] = format
237
+ if center_image_url is not None:
238
+ params["centerImageUrl"] = center_image_url
239
+ if center_image_size_ratio is not None:
240
+ params["centerImageSizeRatio"] = center_image_size_ratio
241
+ if center_image_width is not None:
242
+ params["centerImageWidth"] = center_image_width
243
+ if center_image_height is not None:
244
+ params["centerImageHeight"] = center_image_height
245
+ if caption is not None:
246
+ params["caption"] = caption
247
+ if caption_font_family is not None:
248
+ params["captionFontFamily"] = caption_font_family
249
+ if caption_font_size is not None:
250
+ params["captionFontSize"] = caption_font_size
251
+ if caption_font_color is not None:
252
+ params["captionFontColor"] = caption_font_color
253
+
254
+ return "https://quickchart.io/qr?" + urlencode(params, quote_via=quote)
255
+
256
+ # ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
257
+ # Markdown Sanitizing
258
+ # ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
119
259
 
120
260
  # All special chars that must be escaped in MarkdownV2
121
261
  _SPECIAL = r'_*[]()~`>#+-=|{}.!'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: telekit
3
- Version: 2.0.1
3
+ Version: 2.1.0
4
4
  Summary: Declarative, developer-friendly library for building Telegram bots
5
5
  Home-page: https://github.com/Romashkaa/telekit
6
6
  Author: romashka
@@ -62,7 +62,7 @@ telekit.Server("BOT_TOKEN").polling()
62
62
 
63
63
  > Send "Hello!" with a photo on `/start`
64
64
 
65
- Telekit comes with a [built-in DSL](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial/11_telekit_dsl.md), allowing developers to create fully interactive bots with minimal code. It also integrates **Jinja**, giving you loops, conditionals, expressions, and filters to generate dynamic content.
65
+ Telekit comes with a [built-in DSL](https://github.com/Romashkaa/telekit/blob/main/docs/tutorial/11_telekit_dsl.md), allowing developers to create fully interactive bots with minimal code. It also integrates [**Jinja**](https://github.com/Romashkaa/telekit/blob/main/docs/examples/jinja_engine.md), giving you loops, conditionals, expressions, and filters to generate dynamic content.
66
66
 
67
67
  ```js
68
68
  @ main {
@@ -378,68 +378,20 @@ It tries to make Telegram bot development easier.
378
378
 
379
379
  ---
380
380
 
381
- # Changes in version 2.0.1
382
-
383
- ## Obsidian Canvas Mode *(Beta)*
384
- Build bots visually — no DSL required. Design your dialog flow as a flowchart in Obsidian and connect it to Telekit with a single method call.
385
-
386
- - Added `analyze_canvas()` to `DSLHandler` — loads a `.canvas` file as an executable model
387
-
388
- ## Styles Improvements
389
- The style system has been rewritten from the ground up. Previously, automatic escaping was quite unpredictable — users couldn't always tell when a string would be escaped or not. That's over.
390
-
391
- - Migrated markdown rendering to **MarkdownV2**
392
- - Added `Stack` style for structured list rendering
393
- - Added `Mention` style
394
- - Renamed `Sanitize` to `Escape`
395
- - Renamed `NoSanitize` to `Raw`
396
- - Added `utils.sanitize_markdown()` escapes all MarkdownV2 special characters outside valid entities
397
- - Added `utils.adapt_markdown()` converts `**bold**` / `*italic*` to Telegram-compatible format
398
- - Added `utils.telegramify_markdown()` — convenience pipeline: adapt + sanitize
399
- - `StyleFormatter` renamed to `TextEntity`
400
-
401
- ## Inline Keyboards
402
-
403
- Inline keyboards got a proper type system. Previously, any string value in the keyboard dict was treated as a link. Now every button type is explicit.
404
-
405
- - Introduced `InlineButton` base class with subclasses:
406
- `LinkButton`, `WebAppButton`, `SuggestButton`, `CopyTextButton`, `CallbackButton`
407
- - `CallbackButton` — pass arguments to callbacks and set visual style
408
- - Added `ButtonStyle` enum — style your inline buttons (Bot API 9.4)
409
- - Added `inline_choice` / `set_inline_choice` methods
410
- - `set_entry_suggestions` now warns when Telegram Bot API limits are exceeded
411
- - It's no longer pass `Message` object to callbacks
412
- - Reduced closure size and improved callback performance
413
-
414
- ## Chain Improvements
415
- - Added `set_break_only_on_match()` — control whether chain interrupts only on valid handler match, not random input
416
- - Added `set_break_on_commands()` — control whether commands take priority over entry handlers
417
- - Improved file extension validation via **pathlib**
418
- - `entry_*` handlers no longer pass `Message` by default — use `include_message=True` to restore
419
-
420
- ## CallbackQueryHandler
421
- - Callbacks now invoked directly — no virtual message conversion, faster response times
422
- - Native alert support for expired or inactive buttons
423
- - Added `set_invalid_data_answer()` and `set_button_is_no_active_answer()` for global customization
424
-
425
- ## DSL Improvements
426
- - `jinja_env` is now private — use `get_jinja_env()` instead
427
- - `TelekitDSL` class:
428
- - All `from_*` factory methods now use `type()` for class creation
429
-
430
- ## Fixes
431
- - Fixed one-time trigger bug — triggers like `on.text` now fire reliably across multiple messages while a chain timeout is active
432
-
433
- ## Other
434
- - Added `utils.read_token()` and `utils.read_canvas_path()` — clean helpers for reading credentials from files
435
- - Added `utils.format_file_size()`
436
- - Added `set_license_text` to senders
437
- - Added `format_size` to `TextDocument`
438
- - Added `TelekitState` class
439
- - Bumped `pyTelegramBotAPI` to **v4.31.0**
440
- - Added GPL v3 License
441
-
442
- ## 🚀 Upcoming
443
-
444
- - New triggers: `document`, `text_document`
445
- - Telekit DSL: shorter notation and button attributes with style, alert, ... support
381
+ # Changes in version 2.1.0
382
+
383
+ ## Trigger Improvements
384
+
385
+ | **Name** | **Description** |
386
+ |----------|-----------------|
387
+ | `on.text` | *BugFix:* Commands are no longer matched as text triggers. |
388
+
389
+ - Internal refactor of the `On` class.
390
+
391
+ ## New Utils
392
+
393
+ | **Name** | **Description** |
394
+ |----------|-----------------|
395
+ | `make_bot_link` | Builds a `t.me` URL to a bot with an optional deep-link payload. |
396
+ | `make_user_link` | Builds a `t.me` URL to a user with an optional pre-filled message. |
397
+ | `make_qrcode` | Generates a [QR code](./docs/examples/qr_gen.md) URL from any text or link. |
@@ -62,6 +62,7 @@ telekit/example/example_handlers/hotel.py
62
62
  telekit/example/example_handlers/on_text.py
63
63
  telekit/example/example_handlers/pages.py
64
64
  telekit/example/example_handlers/pyapi.py
65
+ telekit/example/example_handlers/qr.py
65
66
  telekit/example/example_handlers/quiz.py
66
67
  telekit/example/example_handlers/spells.py
67
68
  telekit/example/example_handlers/start.py
@@ -1 +0,0 @@
1
- __version__ = "2.0.1"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes