telegrinder 0.1.dev161__py3-none-any.whl → 0.1.dev163__py3-none-any.whl

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.

Potentially problematic release.


This version of telegrinder might be problematic. Click here for more details.

Files changed (35) hide show
  1. telegrinder/api/api.py +4 -2
  2. telegrinder/api/error.py +3 -3
  3. telegrinder/bot/bot.py +8 -12
  4. telegrinder/bot/cute_types/base.py +1 -4
  5. telegrinder/bot/cute_types/message.py +140 -0
  6. telegrinder/bot/dispatch/composition.py +1 -1
  7. telegrinder/bot/dispatch/dispatch.py +3 -4
  8. telegrinder/bot/dispatch/view/box.py +6 -6
  9. telegrinder/bot/dispatch/waiter_machine/short_state.py +1 -1
  10. telegrinder/bot/rules/__init__.py +4 -0
  11. telegrinder/bot/rules/is_from.py +18 -3
  12. telegrinder/bot/scenario/checkbox.py +3 -4
  13. telegrinder/client/aiohttp.py +1 -2
  14. telegrinder/modules.py +5 -3
  15. telegrinder/msgspec_json.py +3 -3
  16. telegrinder/msgspec_utils.py +80 -38
  17. telegrinder/node/source.py +2 -3
  18. telegrinder/tools/__init__.py +6 -0
  19. telegrinder/tools/buttons.py +8 -13
  20. telegrinder/tools/error_handler/error_handler.py +2 -2
  21. telegrinder/tools/formatting/__init__.py +6 -0
  22. telegrinder/tools/formatting/html.py +10 -0
  23. telegrinder/tools/formatting/links.py +7 -0
  24. telegrinder/tools/formatting/spec_html_formats.py +27 -15
  25. telegrinder/tools/global_context/global_context.py +7 -5
  26. telegrinder/tools/keyboard.py +3 -3
  27. telegrinder/tools/loop_wrapper/abc.py +4 -4
  28. telegrinder/tools/magic.py +1 -1
  29. telegrinder/types/enums.py +4 -0
  30. telegrinder/types/methods.py +175 -41
  31. telegrinder/types/objects.py +442 -201
  32. {telegrinder-0.1.dev161.dist-info → telegrinder-0.1.dev163.dist-info}/METADATA +1 -1
  33. {telegrinder-0.1.dev161.dist-info → telegrinder-0.1.dev163.dist-info}/RECORD +35 -35
  34. {telegrinder-0.1.dev161.dist-info → telegrinder-0.1.dev163.dist-info}/WHEEL +1 -1
  35. {telegrinder-0.1.dev161.dist-info → telegrinder-0.1.dev163.dist-info}/LICENSE +0 -0
@@ -30,6 +30,8 @@ from .is_from import (
30
30
  IsDartDice,
31
31
  IsDice,
32
32
  IsForum,
33
+ IsForward,
34
+ IsForwardType,
33
35
  IsGroup,
34
36
  IsLanguageCode,
35
37
  IsPremium,
@@ -78,6 +80,8 @@ __all__ = (
78
80
  "IsDartDice",
79
81
  "IsDice",
80
82
  "IsForum",
83
+ "IsForward",
84
+ "IsForwardType",
81
85
  "IsGroup",
82
86
  "IsLanguageCode",
83
87
  "IsPremium",
@@ -12,13 +12,13 @@ T = typing.TypeVar("T", bound=BaseCute)
12
12
 
13
13
 
14
14
  @typing.runtime_checkable
15
- class HasFromProto(typing.Protocol):
15
+ class FromUserProto(typing.Protocol):
16
16
  from_: User | Option[User]
17
17
 
18
18
 
19
19
  class HasFrom(ABCRule[T]):
20
20
  async def check(self, event: T, ctx: Context) -> bool:
21
- return isinstance(event, HasFromProto) and bool(event.from_)
21
+ return isinstance(event, FromUserProto) and bool(event.from_)
22
22
 
23
23
 
24
24
  class HasDice(MessageRule):
@@ -26,6 +26,19 @@ class HasDice(MessageRule):
26
26
  return bool(message.dice)
27
27
 
28
28
 
29
+ class IsForward(MessageRule):
30
+ async def check(self, message: Message, ctx: Context) -> bool:
31
+ return bool(message.forward_origin)
32
+
33
+
34
+ class IsForwardType(MessageRule, requires=[IsForward()]):
35
+ def __init__(self, fwd_type: typing.Literal["user", "hidden_user", "chat", "channel"], /) -> None:
36
+ self.fwd_type = fwd_type
37
+
38
+ async def check(self, message: Message, ctx: Context) -> bool:
39
+ return message.forward_origin.unwrap().v.type == self.fwd_type
40
+
41
+
29
42
  class IsReply(MessageRule):
30
43
  async def check(self, message: Message, ctx: Context) -> bool:
31
44
  return bool(message.reply_to_message)
@@ -58,7 +71,7 @@ class IsLanguageCode(MessageRule, requires=[HasFrom()]):
58
71
  async def check(self, message: Message, ctx: Context) -> bool:
59
72
  if not message.from_user.language_code:
60
73
  return False
61
- return message.from_user.language_code.unwrap_or_none() in self.lang_codes
74
+ return message.from_user.language_code.unwrap() in self.lang_codes
62
75
 
63
76
 
64
77
  class IsForum(MessageRule):
@@ -141,6 +154,8 @@ __all__ = (
141
154
  "IsDartDice",
142
155
  "IsDice",
143
156
  "IsForum",
157
+ "IsForward",
158
+ "IsForwardType",
144
159
  "IsGroup",
145
160
  "IsLanguageCode",
146
161
  "IsPremium",
@@ -43,7 +43,7 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
43
43
  self.choices: list[Choice] = []
44
44
  self.ready = ready_text
45
45
  self.max_in_row = max_in_row
46
- self.random_code = secrets.token_hex(16)
46
+ self.random_code = secrets.token_hex(8)
47
47
  self.waiter_machine = waiter_machine
48
48
 
49
49
  def get_markup(self) -> InlineKeyboardMarkup:
@@ -73,7 +73,7 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
73
73
  is_picked: bool = False,
74
74
  ) -> typing.Self:
75
75
  self.choices.append(
76
- Choice(name, is_picked, default_text, picked_text, secrets.token_hex(16)),
76
+ Choice(name, is_picked, default_text, picked_text, secrets.token_hex(8)),
77
77
  )
78
78
  return self
79
79
 
@@ -103,7 +103,7 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
103
103
  assert len(self.choices) > 1
104
104
  message = (
105
105
  await api.send_message(
106
- self.chat_id,
106
+ chat_id=self.chat_id,
107
107
  text=self.msg,
108
108
  parse_mode=self.PARSE_MODE,
109
109
  reply_markup=self.get_markup(),
@@ -111,7 +111,6 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
111
111
  ).unwrap()
112
112
 
113
113
  while True:
114
- q: CallbackQueryCute
115
114
  q, _ = await self.waiter_machine.wait(view, (api, message.message_id))
116
115
  should_continue = await self.handle(q)
117
116
  await q.answer(self.CALLBACK_ANSWER)
@@ -1,4 +1,3 @@
1
- import secrets
2
1
  import ssl
3
2
  import typing
4
3
 
@@ -31,7 +30,7 @@ class AiohttpClient(ABCClient):
31
30
  self.__class__.__name__,
32
31
  self.session,
33
32
  self.timeout,
34
- False if self.session is None else self.session.closed,
33
+ True if self.session is None else self.session.closed,
35
34
  )
36
35
 
37
36
  async def request_raw(
telegrinder/modules.py CHANGED
@@ -4,14 +4,16 @@ import typing
4
4
  from choicelib import choice_in_order
5
5
 
6
6
 
7
+ @typing.runtime_checkable
7
8
  class JSONModule(typing.Protocol):
8
- def loads(self, s: str | bytes) -> dict[str, typing.Any] | list[typing.Any]:
9
+ def loads(self, s: str | bytes) -> typing.Any:
9
10
  ...
10
11
 
11
- def dumps(self, o: dict[str, typing.Any] | list[typing.Any]) -> str:
12
+ def dumps(self, o: typing.Any) -> str:
12
13
  ...
13
14
 
14
15
 
16
+ @typing.runtime_checkable
15
17
  class LoggerModule(typing.Protocol):
16
18
  def debug(self, __msg: object, *args: object, **kwargs: object) -> None:
17
19
  ...
@@ -47,7 +49,7 @@ class LoggerModule(typing.Protocol):
47
49
 
48
50
  logger: LoggerModule
49
51
  json: JSONModule = choice_in_order(
50
- ["ujson", "hyperjson", "orjson"], do_import=True, default="telegrinder.msgspec_json"
52
+ ["orjson", "ujson", "hyperjson"], do_import=True, default="telegrinder.msgspec_json"
51
53
  )
52
54
  logging_module = choice_in_order(["loguru"], default="logging")
53
55
  logging_level = os.getenv("LOGGER_LEVEL", default="DEBUG").upper()
@@ -3,11 +3,11 @@ import typing
3
3
  from .msgspec_utils import decoder, encoder
4
4
 
5
5
 
6
- def loads(s: str | bytes) -> dict[str, typing.Any] | list[typing.Any]:
7
- return decoder.decode(s, type=dict[str, typing.Any] | list[typing.Any]) # type: ignore
6
+ def loads(s: str | bytes) -> typing.Any:
7
+ return decoder.decode(s)
8
8
 
9
9
 
10
- def dumps(o: dict[str, typing.Any] | list[typing.Any]) -> str:
10
+ def dumps(o: typing.Any) -> str:
11
11
  return encoder.encode(o)
12
12
 
13
13
 
@@ -4,11 +4,7 @@ import fntypes.option
4
4
  import msgspec
5
5
  from fntypes.co import Error, Ok, Result, Variative
6
6
 
7
- T = typing.TypeVar("T")
8
- Ts = typing.TypeVarTuple("Ts")
9
-
10
7
  if typing.TYPE_CHECKING:
11
- import types
12
8
  from datetime import datetime
13
9
 
14
10
  from fntypes.option import Option
@@ -19,6 +15,7 @@ else:
19
15
 
20
16
  datetime = type("datetime", (dt,), {})
21
17
 
18
+
22
19
  class OptionMeta(type):
23
20
  def __instancecheck__(cls, __instance: typing.Any) -> bool:
24
21
  return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
@@ -27,6 +24,9 @@ else:
27
24
  class Option(typing.Generic[Value], metaclass=OptionMeta):
28
25
  pass
29
26
 
27
+ T = typing.TypeVar("T")
28
+ Ts = typing.TypeVarTuple("Ts")
29
+
30
30
  DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any], object]
31
31
  EncHook: typing.TypeAlias = typing.Callable[[T], typing.Any]
32
32
 
@@ -48,10 +48,6 @@ def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, msgspec.Validation
48
48
  return Error(exc)
49
49
 
50
50
 
51
- def datetime_dec_hook(tp: type[datetime], obj: int | float) -> datetime:
52
- return tp.fromtimestamp(obj)
53
-
54
-
55
51
  def option_dec_hook(tp: type[Option[typing.Any]], obj: typing.Any) -> Option[typing.Any]:
56
52
  if obj is None:
57
53
  return Nothing
@@ -88,37 +84,49 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
88
84
 
89
85
  raise TypeError(
90
86
  "Object of type `{}` does not belong to types `{}`".format(
91
- repr_type(type(obj)),
87
+ repr_type(obj.__class__),
92
88
  " | ".join(map(repr_type, union_types)),
93
89
  )
94
90
  )
95
91
 
96
92
 
97
- def datetime_enc_hook(obj: datetime) -> int:
98
- return int(obj.timestamp())
93
+ class Decoder:
94
+ """Class `Decoder` for `msgspec` module with decode hook
95
+ for objects with the specified type.
96
+
97
+ ```
98
+ import enum
99
+
100
+ from datetime import datetime as dt
99
101
 
102
+ class Digit(enum.IntEnum):
103
+ ONE = 1
104
+ TWO = 2
105
+ THREE = 3
100
106
 
101
- def option_enc_hook(obj: Option[typing.Any]) -> typing.Any | None:
102
- return obj.value if isinstance(obj, fntypes.option.Some) else None
107
+ decoder = Encoder()
108
+ decoder.dec_hooks[dt] = lambda t, timestamp: t.fromtimestamp(timestamp)
103
109
 
110
+ decoder.dec_hook(dt, 1713354732) #> datetime.datetime(2024, 4, 17, 14, 52, 12)
111
+ decoder.dec_hook(int, "123") #> TypeError: Unknown type `int`. You can implement decode hook for this type.
104
112
 
105
- def variative_enc_hook(obj: Variative[typing.Any]) -> typing.Any:
106
- return obj.v
113
+ decoder.convert("123", type=int, strict=False) #> 123
114
+ decoder.convert(1, type=Digit) #> <Digit.ONE: 1>
107
115
 
116
+ decoder.decode(b'{"digit":3}', type=dict[str, Digit]) #> {'digit': <Digit.THREE: 3>}
117
+ ```
118
+ """
108
119
 
109
- class Decoder:
110
120
  def __init__(self) -> None:
111
- self.dec_hooks: dict[
112
- typing.Union[type[typing.Any], "types.UnionType"], DecHook[typing.Any]
113
- ] = {
121
+ self.dec_hooks: dict[typing.Any, DecHook[typing.Any]] = {
114
122
  Option: option_dec_hook,
115
123
  Variative: variative_dec_hook,
116
- datetime: datetime_dec_hook,
124
+ datetime: lambda t, obj: t.fromtimestamp(obj),
117
125
  }
118
126
 
119
- def add_dec_hook(self, tp: type[T]):
127
+ def add_dec_hook(self, t: T): # type: ignore
120
128
  def decorator(func: DecHook[T]) -> DecHook[T]:
121
- return self.dec_hooks.setdefault(get_origin(tp), func)
129
+ return self.dec_hooks.setdefault(get_origin(t), func) # type: ignore
122
130
 
123
131
  return decorator
124
132
 
@@ -138,7 +146,7 @@ class Decoder:
138
146
  type: type[T] = dict,
139
147
  strict: bool = True,
140
148
  from_attributes: bool = False,
141
- builtin_types: typing.Iterable[type] | None = None,
149
+ builtin_types: typing.Iterable[type[typing.Any]] | None = None,
142
150
  str_keys: bool = False,
143
151
  ) -> T:
144
152
  return msgspec.convert(
@@ -150,12 +158,30 @@ class Decoder:
150
158
  builtin_types=builtin_types,
151
159
  str_keys=str_keys,
152
160
  )
161
+
162
+ @typing.overload
163
+ def decode(self, buf: str | bytes) -> typing.Any:
164
+ ...
165
+
166
+ @typing.overload
167
+ def decode(self, buf: str | bytes, *, strict: bool = True) -> typing.Any:
168
+ ...
153
169
 
170
+ @typing.overload
154
171
  def decode(
155
172
  self,
156
173
  buf: str | bytes,
157
174
  *,
158
- type: type[T] = dict,
175
+ type: type[T],
176
+ strict: bool = True,
177
+ ) -> T:
178
+ ...
179
+
180
+ def decode(
181
+ self,
182
+ buf: str | bytes,
183
+ *,
184
+ type: type[T] = typing.Any, # type: ignore
159
185
  strict: bool = True,
160
186
  ) -> T:
161
187
  return msgspec.json.decode(
@@ -167,35 +193,55 @@ class Decoder:
167
193
 
168
194
 
169
195
  class Encoder:
196
+ """Class `Encoder` for `msgspec` module with encode hooks for objects.
197
+
198
+ ```
199
+ from datetime import datetime as dt
200
+
201
+ encoder = Encoder()
202
+ encoder.enc_hooks[dt] = lambda d: int(d.timestamp())
203
+
204
+ encoder.enc_hook(dt.now()) #> 1713354732
205
+ encoder.enc_hook(123) #> NotImplementedError: Not implemented encode hook for object of type `int`.
206
+
207
+ encoder.encode({'digit': Digit.ONE}) #> '{"digit":1}'
208
+ ```
209
+ """
210
+
170
211
  def __init__(self) -> None:
171
- self.enc_hooks: dict[type, EncHook] = {
172
- fntypes.option.Some: option_enc_hook,
173
- fntypes.option.Nothing: option_enc_hook,
174
- Variative: variative_enc_hook,
175
- datetime: datetime_enc_hook,
212
+ self.enc_hooks: dict[typing.Any, EncHook[typing.Any]] = {
213
+ fntypes.option.Some: lambda opt: opt.value,
214
+ fntypes.option.Nothing: lambda _: None,
215
+ Variative: lambda variative: variative.v,
216
+ datetime: lambda date: int(date.timestamp()),
176
217
  }
177
218
 
178
- def add_dec_hook(self, tp: type[T]):
219
+ def add_dec_hook(self, t: type[T]):
179
220
  def decorator(func: EncHook[T]) -> EncHook[T]:
180
- return self.enc_hooks.setdefault(get_origin(tp), func)
221
+ encode_hook = self.enc_hooks.setdefault(get_origin(t), func)
222
+ return func if encode_hook is not func else encode_hook
181
223
 
182
224
  return decorator
183
225
 
184
226
  def enc_hook(self, obj: object) -> object:
185
- origin_type = get_origin(type(obj))
227
+ origin_type = get_origin(obj.__class__)
186
228
  if origin_type not in self.enc_hooks:
187
229
  raise NotImplementedError(
188
230
  "Not implemented encode hook for "
189
231
  f"object of type `{repr_type(origin_type)}`."
190
232
  )
191
233
  return self.enc_hooks[origin_type](obj)
234
+
235
+ @typing.overload
236
+ def encode(self, obj: typing.Any) -> str:
237
+ ...
192
238
 
193
239
  @typing.overload
194
- def encode(self, obj: typing.Any, *, as_str: typing.Literal[True] = True) -> str:
240
+ def encode(self, obj: typing.Any, *, as_str: typing.Literal[True]) -> str:
195
241
  ...
196
242
 
197
243
  @typing.overload
198
- def encode(self, obj: typing.Any, *, as_str: typing.Literal[False] = False) -> bytes:
244
+ def encode(self, obj: typing.Any, *, as_str: typing.Literal[False]) -> bytes:
199
245
  ...
200
246
 
201
247
  def encode(self, obj: typing.Any, *, as_str: bool = True) -> str | bytes:
@@ -215,12 +261,8 @@ __all__ = (
215
261
  "get_origin",
216
262
  "repr_type",
217
263
  "msgspec_convert",
218
- "datetime_dec_hook",
219
- "datetime_enc_hook",
220
264
  "option_dec_hook",
221
- "option_enc_hook",
222
265
  "variative_dec_hook",
223
- "variative_enc_hook",
224
266
  "datetime",
225
267
  "decoder",
226
268
  "encoder",
@@ -2,7 +2,6 @@ import dataclasses
2
2
  import typing
3
3
 
4
4
  from telegrinder.api import API
5
- from telegrinder.msgspec_utils import Nothing, Option
6
5
  from telegrinder.types import Chat, Message
7
6
 
8
7
  from .base import DataNode
@@ -13,14 +12,14 @@ from .message import MessageNode
13
12
  class Source(DataNode):
14
13
  api: API
15
14
  chat: Chat
16
- thread_id: Option[int] = dataclasses.field(default_factory=lambda: Nothing)
15
+ thread_id: int | None = None
17
16
 
18
17
  @classmethod
19
18
  async def compose(cls, message: MessageNode) -> typing.Self:
20
19
  return cls(
21
20
  api=message.ctx_api,
22
21
  chat=message.chat,
23
- thread_id=message.message_thread_id,
22
+ thread_id=message.message_thread_id.unwrap_or_none(),
24
23
  )
25
24
 
26
25
  async def send(self, text: str) -> Message:
@@ -14,6 +14,7 @@ from .formatting import (
14
14
  StartBotLink,
15
15
  StartGroupLink,
16
16
  TgEmoji,
17
+ UserOpenMessage,
17
18
  block_quote,
18
19
  bold,
19
20
  channel_boost_link,
@@ -37,6 +38,8 @@ from .formatting import (
37
38
  strike,
38
39
  tg_emoji,
39
40
  underline,
41
+ user_open_message,
42
+ user_open_message_link,
40
43
  )
41
44
  from .global_context import (
42
45
  ABCGlobalContext,
@@ -109,6 +112,7 @@ __all__ = (
109
112
  "StartGroupLink",
110
113
  "TelegrinderCtx",
111
114
  "TgEmoji",
115
+ "UserOpenMessage",
112
116
  "block_quote",
113
117
  "bold",
114
118
  "channel_boost_link",
@@ -135,4 +139,6 @@ __all__ = (
135
139
  "strike",
136
140
  "tg_emoji",
137
141
  "underline",
142
+ "user_open_message",
143
+ "user_open_message_link",
138
144
  )
@@ -26,13 +26,11 @@ class DataclassInstance(typing.Protocol):
26
26
  class BaseButton:
27
27
  def get_data(self) -> dict[str, typing.Any]:
28
28
  return {
29
- k: v
30
- if k != "callback_data" or isinstance(v, str)
31
- else encoder.encode(v)
29
+ k: v if k != "callback_data" or isinstance(v, str) else encoder.encode(v)
32
30
  for k, v in dataclasses.asdict(self).items()
33
31
  if v is not None
34
32
  }
35
-
33
+
36
34
 
37
35
  class RowButtons(typing.Generic[ButtonT]):
38
36
  buttons: list[ButtonT]
@@ -65,18 +63,15 @@ class InlineButton(BaseButton):
65
63
  url: str | None = None
66
64
  login_url: dict[str, typing.Any] | LoginUrl | None = None
67
65
  pay: bool | None = None
68
- callback_data: typing.Union[
69
- str,
70
- dict[str, typing.Any],
71
- DataclassInstance,
72
- msgspec.Struct,
73
- ] | None = None
66
+ callback_data: (
67
+ str | dict[str, typing.Any] | DataclassInstance | msgspec.Struct | None
68
+ ) = None
74
69
  callback_game: dict[str, typing.Any] | CallbackGame | None = None
75
70
  switch_inline_query: str | None = None
76
71
  switch_inline_query_current_chat: str | None = None
77
- switch_inline_query_chosen_chat: dict[
78
- str, typing.Any
79
- ] | SwitchInlineQueryChosenChat | None = None
72
+ switch_inline_query_chosen_chat: (
73
+ dict[str, typing.Any] | SwitchInlineQueryChosenChat | None
74
+ ) = None
80
75
  web_app: dict[str, typing.Any] | WebAppInfo | None = None
81
76
 
82
77
 
@@ -74,9 +74,9 @@ class Catcher(typing.Generic[EventT]):
74
74
 
75
75
  def match_exception(self, exception: BaseException) -> bool:
76
76
  for exc in self.exceptions:
77
- if isinstance(exc, type) and type(exception) == exc:
77
+ if isinstance(exc, type) and type(exception) is exc:
78
78
  return True
79
- if isinstance(exc, object) and type(exception) == type(exc):
79
+ if isinstance(exc, object) and type(exception) is type(exc):
80
80
  return True if not exc.args else exc.args == exception.args
81
81
  return False
82
82
 
@@ -18,6 +18,7 @@ from .html import (
18
18
  strike,
19
19
  tg_emoji,
20
20
  underline,
21
+ user_open_message,
21
22
  )
22
23
  from .links import (
23
24
  get_channel_boost_link,
@@ -26,6 +27,7 @@ from .links import (
26
27
  get_resolve_domain_link,
27
28
  get_start_bot_link,
28
29
  get_start_group_link,
30
+ user_open_message_link,
29
31
  )
30
32
  from .spec_html_formats import (
31
33
  BaseSpecFormat,
@@ -39,6 +41,7 @@ from .spec_html_formats import (
39
41
  StartBotLink,
40
42
  StartGroupLink,
41
43
  TgEmoji,
44
+ UserOpenMessage,
42
45
  )
43
46
 
44
47
  __all__ = (
@@ -55,6 +58,7 @@ __all__ = (
55
58
  "StartBotLink",
56
59
  "StartGroupLink",
57
60
  "TgEmoji",
61
+ "UserOpenMessage",
58
62
  "block_quote",
59
63
  "bold",
60
64
  "channel_boost_link",
@@ -78,4 +82,6 @@ __all__ = (
78
82
  "strike",
79
83
  "tg_emoji",
80
84
  "underline",
85
+ "user_open_message",
86
+ "user_open_message_link",
81
87
  )
@@ -13,6 +13,7 @@ from .links import (
13
13
  get_resolve_domain_link,
14
14
  get_start_bot_link,
15
15
  get_start_group_link,
16
+ user_open_message_link,
16
17
  )
17
18
  from .spec_html_formats import SpecialFormat, is_spec_format
18
19
 
@@ -274,6 +275,14 @@ def underline(string: str) -> TagFormat:
274
275
  return TagFormat(string, tag="u")
275
276
 
276
277
 
278
+ def user_open_message(
279
+ user_id: int,
280
+ message: str | None = None,
281
+ string: str | None = None,
282
+ ) -> TagFormat:
283
+ return link(user_open_message_link(user_id, message), string)
284
+
285
+
277
286
  __all__ = (
278
287
  "FormatString",
279
288
  "HTMLFormatter",
@@ -301,4 +310,5 @@ __all__ = (
301
310
  "strike",
302
311
  "tg_emoji",
303
312
  "underline",
313
+ "user_open_message",
304
314
  )
@@ -22,6 +22,12 @@ def get_invite_chat_link(invite_link: str) -> str:
22
22
  return f"tg://join?invite={invite_link}"
23
23
 
24
24
 
25
+ def user_open_message_link(user_id: int, message: str | None = None) -> str:
26
+ return f"tg://openmessage?user_id={user_id}" + (
27
+ "" if not message else f"&msg?text={message}"
28
+ )
29
+
30
+
25
31
  __all__ = (
26
32
  "get_channel_boost_link",
27
33
  "get_invite_chat_link",
@@ -29,4 +35,5 @@ __all__ = (
29
35
  "get_resolve_domain_link",
30
36
  "get_start_bot_link",
31
37
  "get_start_group_link",
38
+ "user_open_message_link",
32
39
  )