python-trueconf-bot 1.2.3.dev1__py3-none-any.whl → 1.4.0__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.
trueconf/types/message.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
  from dataclasses import dataclass, field
3
- from typing import Optional, Union
3
+ from typing import Union, TYPE_CHECKING
4
4
  from mashumaro import DataClassDictMixin
5
5
  from trueconf.enums.message_type import MessageType
6
6
  from trueconf.types.author_box import EnvelopeAuthor, EnvelopeBox
@@ -21,9 +21,16 @@ from trueconf.enums.parse_mode import ParseMode
21
21
  from trueconf.types.input_file import InputFile
22
22
 
23
23
  import logging
24
+ import re
24
25
 
25
- logger = logging.getLogger("chat_bot")
26
+ if TYPE_CHECKING:
27
+ from trueconf.types.responses.send_file_response import SendFileResponse
28
+ from trueconf.types.responses.send_message_response import SendMessageResponse
29
+ from trueconf.types.responses.forward_message_response import ForwardMessageResponse
30
+ from trueconf.types.responses.remove_message_response import RemoveMessageResponse
26
31
 
32
+ logger = logging.getLogger("chat_bot")
33
+ MENTION_RE = re.compile(r"href=[\"']trueconf:([^\"'&]+)(?:&do=profile)?[\"']")
27
34
 
28
35
  @dataclass
29
36
  class Message(BoundToBot, DataClassDictMixin):
@@ -55,6 +62,7 @@ class Message(BoundToBot, DataClassDictMixin):
55
62
  from_user (EnvelopeAuthor): Shortcut for accessing the message author.
56
63
  content_type (MessageType): Returns the type of the message.
57
64
  text (Optional[str]): Returns the message text if it contains text, else None.
65
+ mention (Optional[bool]): Returns True if the message mentions the bot, else None.
58
66
  document (Optional[Document]): Returns a document attachment if the message
59
67
  contains a non-media file (not photo, video, sticker).
60
68
  photo (Optional[Photo]): Returns a photo attachment if available.
@@ -89,7 +97,7 @@ class Message(BoundToBot, DataClassDictMixin):
89
97
  message_id: str = field(metadata={"alias": "messageId"})
90
98
  chat_id: str = field(metadata={"alias": "chatId"})
91
99
  is_edited: bool = field(metadata={"alias": "isEdited"})
92
- reply_message_id: Optional[str] = field(default=None, metadata={"alias": "replyMessageId"})
100
+ reply_message_id: str | None = field(default=None, metadata={"alias": "replyMessageId"})
93
101
 
94
102
  @property
95
103
  def from_user(self) -> EnvelopeAuthor:
@@ -122,7 +130,7 @@ class Message(BoundToBot, DataClassDictMixin):
122
130
  return self.content.text if isinstance(self.content, TextContent) else None
123
131
 
124
132
  @property
125
- def document(self) -> Optional["Document"]:
133
+ def document(self) -> Document | None:
126
134
  """
127
135
  Returns the attached document if the message contains a non-media file.
128
136
 
@@ -146,7 +154,7 @@ class Message(BoundToBot, DataClassDictMixin):
146
154
  return None
147
155
 
148
156
  @property
149
- def photo(self) -> Optional["Photo"]:
157
+ def photo(self) -> Photo | None:
150
158
  """
151
159
  Returns the attached photo object if the current message contains an image.
152
160
 
@@ -166,7 +174,7 @@ class Message(BoundToBot, DataClassDictMixin):
166
174
  return None
167
175
 
168
176
  @property
169
- def video(self) -> Optional["Video"]:
177
+ def video(self) -> Video | None:
170
178
  """
171
179
  Returns the attached video object if the current message contains a video.
172
180
 
@@ -186,7 +194,7 @@ class Message(BoundToBot, DataClassDictMixin):
186
194
  return None
187
195
 
188
196
  @property
189
- def sticker(self) -> Optional["Sticker"]:
197
+ def sticker(self) -> Sticker | None:
190
198
  """
191
199
  Returns the attached sticker object if the current message contains a sticker.
192
200
 
@@ -203,13 +211,41 @@ class Message(BoundToBot, DataClassDictMixin):
203
211
  ).bind(self.bot)
204
212
  return None
205
213
 
214
+ @property
215
+ def mention(self) -> bool | None:
216
+ """
217
+ Returns whether the current text message mentions the bot.
218
+
219
+ The shortcut checks two supported mention formats:
220
+
221
+ - ``@all`` — treated as a mention because it targets all chat participants.
222
+ - TrueConf user mentions rendered as HTML links, for example
223
+ ``<a href="trueconf:john_doe@video.example.com">John Doe</a>``.
224
+
225
+ For user mentions, the extracted TrueConf ID is compared with ``bot.me_id``.
226
+
227
+ Returns:
228
+ bool | None: ``True`` if the message mentions the bot, otherwise ``None``.
229
+ """
230
+ if not isinstance(self.content, TextContent):
231
+ return None
232
+
233
+ text = self.content.text.strip()
234
+ if text == "@all":
235
+ return True
236
+
237
+ return any(
238
+ match.group(1) == self.bot.me_id
239
+ for match in MENTION_RE.finditer(text)
240
+ ) or None
241
+
206
242
  async def answer_photo(
207
243
  self,
208
244
  file: InputFile,
209
245
  preview: InputFile | None,
210
246
  caption: str | None = None,
211
247
  parse_mode: ParseMode | str = ParseMode.TEXT
212
- ) -> object:
248
+ ) -> SendFileResponse:
213
249
  """
214
250
  Shortcut for the [`send_photo`][trueconf.Bot.send_photo] method of the bot instance. Use this method to send a photo in response to the current message.
215
251
 
@@ -247,7 +283,7 @@ class Message(BoundToBot, DataClassDictMixin):
247
283
  file: InputFile,
248
284
  caption: str | None = None,
249
285
  parse_mode: ParseMode | str = ParseMode.TEXT
250
- ) -> object:
286
+ ) -> SendFileResponse:
251
287
  """
252
288
  Shortcut for the [`send_document`][trueconf.Bot.send_document] method of the bot instance. Use this method to send a document in response to the current message.
253
289
 
@@ -281,7 +317,7 @@ class Message(BoundToBot, DataClassDictMixin):
281
317
  async def answer_sticker(
282
318
  self,
283
319
  file: InputFile
284
- ) -> object:
320
+ ) -> SendFileResponse:
285
321
  """
286
322
  Shortcut for the [`send_sticker`][trueconf.Bot.send_sticker] method of the bot instance. Use this method to send a sticker in response to the current message.
287
323
 
@@ -308,7 +344,7 @@ class Message(BoundToBot, DataClassDictMixin):
308
344
  file=file,
309
345
  )
310
346
 
311
- async def answer(self, text: str, parse_mode: ParseMode | str = ParseMode.HTML) -> object:
347
+ async def answer(self, text: str, parse_mode: ParseMode | str = ParseMode.HTML) -> SendMessageResponse:
312
348
  """
313
349
  Shortcut for the [`send_message`][trueconf.Bot.send_message] method of the bot instance. Use this method to send a text message to the current chat.
314
350
 
@@ -341,7 +377,7 @@ class Message(BoundToBot, DataClassDictMixin):
341
377
  parse_mode=parse_mode
342
378
  )
343
379
 
344
- async def reply(self, text: str, parse_mode: ParseMode | str = ParseMode.HTML) -> object:
380
+ async def reply(self, text: str, parse_mode: ParseMode | str = ParseMode.HTML) -> SendMessageResponse:
345
381
  """
346
382
  Shortcut for the [`send_message`][trueconf.Bot.send_message] method.
347
383
  Sends a reply to this message in the current chat.
@@ -374,7 +410,7 @@ class Message(BoundToBot, DataClassDictMixin):
374
410
  caption: str | None = None,
375
411
  parse_mode: ParseMode | str = ParseMode.TEXT,
376
412
 
377
- ) -> object:
413
+ ) -> SendFileResponse:
378
414
  """
379
415
  Shortcut for the [`send_photo`][trueconf.Bot.send_photo] method of the bot instance.
380
416
  Sends a photo as a reply to the current message in this chat.
@@ -416,7 +452,7 @@ class Message(BoundToBot, DataClassDictMixin):
416
452
  file: InputFile,
417
453
  caption: str | None = None,
418
454
  parse_mode: ParseMode | str = ParseMode.TEXT
419
- ) -> object:
455
+ ) -> SendFileResponse:
420
456
  """
421
457
  Shortcut for the [`send_document`][trueconf.Bot.send_document] method of the bot instance.
422
458
  Sends a document as a reply to the current message in this chat.
@@ -453,7 +489,7 @@ class Message(BoundToBot, DataClassDictMixin):
453
489
  async def reply_sticker(
454
490
  self,
455
491
  file: InputFile
456
- ) -> object:
492
+ ) -> SendFileResponse:
457
493
  """
458
494
  Shortcut for the [`send_sticker`][trueconf.Bot.send_sticker] method of the bot instance.
459
495
  Sends a sticker as a reply to the current message in this chat.
@@ -483,7 +519,7 @@ class Message(BoundToBot, DataClassDictMixin):
483
519
  reply_message_id=self.message_id
484
520
  )
485
521
 
486
- async def forward(self, chat_id: str) -> object:
522
+ async def forward(self, chat_id: str) -> ForwardMessageResponse:
487
523
  """
488
524
  Shortcut for the [`forward_message`][trueconf.Bot.forward_message] method of the bot instance. Use this method to forward the current message to another chat.
489
525
 
@@ -505,7 +541,7 @@ class Message(BoundToBot, DataClassDictMixin):
505
541
  message_id=self.message_id,
506
542
  )
507
543
 
508
- async def copy_to(self, chat_id: str) -> object:
544
+ async def copy_to(self, chat_id: str) -> SendMessageResponse | None:
509
545
  """
510
546
  Shortcut for the [`send_message`][trueconf.Bot.send_message] method of the bot instance. Use this method to send a copy of the current message (without metadata or reply context) to another chat.
511
547
 
@@ -537,7 +573,7 @@ class Message(BoundToBot, DataClassDictMixin):
537
573
  )
538
574
  return None
539
575
 
540
- async def delete(self, for_all: bool = False) -> object:
576
+ async def delete(self, for_all: bool = False) -> RemoveMessageResponse:
541
577
  """
542
578
  Shortcut for the [`remove_message`][trueconf.Bot.remove_message] method of the bot instance. Use this method to delete the current message from the chat.
543
579
 
@@ -560,7 +596,7 @@ class Message(BoundToBot, DataClassDictMixin):
560
596
  for_all=for_all,
561
597
  )
562
598
 
563
- async def save_to_favorites(self, copy: bool = False) -> object:
599
+ async def save_to_favorites(self, copy: bool = False) -> SendMessageResponse | ForwardMessageResponse | None:
564
600
  """
565
601
  Saves the current message to the bot's "Favorites" chat.
566
602
 
@@ -592,7 +628,7 @@ class Message(BoundToBot, DataClassDictMixin):
592
628
  """
593
629
 
594
630
  if copy:
595
- return await self.copy_to(await self.bot.me)
631
+ return await self.copy_to(await self.bot.me_chat)
596
632
  else:
597
- return await self.forward(await self.bot.me)
633
+ return await self.forward(await self.bot.me_chat)
598
634
 
trueconf/utils/_ssl.py ADDED
@@ -0,0 +1,44 @@
1
+ import ssl
2
+ from pathlib import Path
3
+ from typing import TypeAlias
4
+ import truststore
5
+
6
+ SSLVerify: TypeAlias = bool | str | ssl.SSLContext
7
+
8
+ def _build_ssl_context(verify_ssl: SSLVerify) -> ssl.SSLContext:
9
+ if isinstance(verify_ssl, ssl.SSLContext):
10
+ return verify_ssl
11
+
12
+ if verify_ssl is False:
13
+ ctx = ssl.create_default_context()
14
+ ctx.check_hostname = False
15
+ ctx.verify_mode = ssl.CERT_NONE
16
+ return ctx
17
+
18
+ if verify_ssl is True:
19
+ return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
20
+
21
+ if isinstance(verify_ssl, str):
22
+ cafile = Path(verify_ssl).expanduser()
23
+ if not cafile.is_file():
24
+ raise FileNotFoundError(
25
+ f"SSL CA bundle file not found: {cafile}. "
26
+ "Pass verify_ssl=True to use the system trust store, "
27
+ "verify_ssl=False to disable verification, or provide a valid path to a CA bundle."
28
+ )
29
+ return ssl.create_default_context(cafile=str(cafile))
30
+
31
+ raise TypeError(
32
+ "verify_ssl must be bool, str path to CA bundle, or ssl.SSLContext"
33
+ )
34
+
35
+ def _describe_ssl_context(ctx: ssl.SSLContext) -> str:
36
+ if ctx.verify_mode == ssl.CERT_NONE:
37
+ return "disabled"
38
+
39
+ return (
40
+ f"enabled "
41
+ f"(context={type(ctx).__module__}.{type(ctx).__name__}, "
42
+ f"verify_mode={ctx.verify_mode.name}, "
43
+ f"check_hostname={ctx.check_hostname})"
44
+ )
trueconf/utils/_token.py CHANGED
@@ -4,8 +4,8 @@ from datetime import datetime
4
4
  from functools import lru_cache
5
5
  from typing import Optional
6
6
 
7
- from httpx import AsyncClient, Client, HTTPStatusError
8
-
7
+ from httpx import Client, HTTPStatusError
8
+ from ._ssl import SSLVerify
9
9
  from ..exceptions import TokenValidationError, InvalidGrantError
10
10
 
11
11
 
@@ -13,7 +13,7 @@ def _get_auth_token(
13
13
  server: str,
14
14
  username: str,
15
15
  password: str,
16
- verify: bool,
16
+ ssl_context: SSLVerify,
17
17
  *,
18
18
  protocol: str = "https",
19
19
  port: int = 443,
@@ -26,7 +26,7 @@ def _get_auth_token(
26
26
  "username": str(username),
27
27
  "password": str(password)
28
28
  }
29
- with Client(timeout=timeout, verify=verify) as client:
29
+ with Client(timeout=timeout, verify=ssl_context) as client:
30
30
  r = client.post(url, json=params)
31
31
 
32
32
  try:
@@ -21,6 +21,12 @@ class _VersionChecker:
21
21
  library_spec=None,
22
22
  message="Chatbots are not supported (server version 5.5.0+ required).",
23
23
  ),
24
+ CompatibilityRule(
25
+ server_spec=SpecifierSet(">=5.5.0,<=5.5.2"),
26
+ library_spec=SpecifierSet(">=1.1.0,<1.2.0"),
27
+ message="Server version 5.5.0–5.5.2 requires python-trueconf-bot v1.1.x.",
28
+ install_hint='uv pip install "python-trueconf-bot>=1.1.0,<1.2.0"',
29
+ ),
24
30
  )
25
31
 
26
32
  @classmethod
@@ -0,0 +1,21 @@
1
+ from .text import (
2
+ AllMention,
3
+ Bold,
4
+ Italic,
5
+ Link,
6
+ Mention,
7
+ Strikethrough,
8
+ Text,
9
+ Underline,
10
+ )
11
+
12
+ __all__ = [
13
+ "AllMention",
14
+ "Bold",
15
+ "Italic",
16
+ "Link",
17
+ "Mention",
18
+ "Strikethrough",
19
+ "Text",
20
+ "Underline"
21
+ ]
@@ -0,0 +1,87 @@
1
+ from abc import ABC, abstractmethod
2
+ from html import escape
3
+ import re
4
+
5
+
6
+ class TextDecoration(ABC):
7
+ @abstractmethod
8
+ def escape(self, value: str) -> str: ...
9
+
10
+ @abstractmethod
11
+ def bold(self, value: str) -> str: ...
12
+
13
+ @abstractmethod
14
+ def italic(self, value: str) -> str: ...
15
+
16
+ @abstractmethod
17
+ def underline(self, value: str) -> str: ...
18
+
19
+ @abstractmethod
20
+ def strikethrough(self, value: str) -> str: ...
21
+
22
+ @abstractmethod
23
+ def link(self, value: str, url: str) -> str: ...
24
+
25
+ @abstractmethod
26
+ def mention(self, value: str, user_id: str) -> str: ...
27
+
28
+ @abstractmethod
29
+ def all_mention(self) -> str: ...
30
+
31
+
32
+ class HtmlDecoration(TextDecoration):
33
+ def escape(self, value: str) -> str:
34
+ return escape(value, quote=False)
35
+
36
+ def bold(self, value: str) -> str:
37
+ return f"<b>{value}</b>"
38
+
39
+ def italic(self, value: str) -> str:
40
+ return f"<i>{value}</i>"
41
+
42
+ def underline(self, value: str) -> str:
43
+ return f"<u>{value}</u>"
44
+
45
+ def strikethrough(self, value: str) -> str:
46
+ return f"<s>{value}</s>"
47
+
48
+ def link(self, value: str, url: str) -> str:
49
+ return f'<a href="{escape(url, quote=True)}">{value}</a>'
50
+
51
+ def mention(self, value: str, user_id: str) -> str:
52
+ return self.link(value, f"trueconf:{user_id}")
53
+
54
+ def all_mention(self) -> str:
55
+ return "@all"
56
+
57
+
58
+ class MarkdownDecoration(TextDecoration):
59
+ PATTERN = re.compile(r"([_*\[\]()~`>#+\-=|{}.!\\])")
60
+
61
+ def escape(self, value: str) -> str:
62
+ return self.PATTERN.sub(r"\\\1", value)
63
+
64
+ def bold(self, value: str) -> str:
65
+ return f"**{value}**"
66
+
67
+ def italic(self, value: str) -> str:
68
+ return f"*{value}*"
69
+
70
+ def underline(self, value: str) -> str:
71
+ return value
72
+
73
+ def strikethrough(self, value: str) -> str:
74
+ return f"~~{value}~~"
75
+
76
+ def link(self, value: str, url: str) -> str:
77
+ return f"[{value}]({url})"
78
+
79
+ def mention(self, value: str, user_id: str) -> str:
80
+ return self.link(value, f"trueconf:{user_id}")
81
+
82
+ def all_mention(self) -> str:
83
+ return "@all"
84
+
85
+
86
+ html_decoration = HtmlDecoration()
87
+ markdown_decoration = MarkdownDecoration()
@@ -0,0 +1,252 @@
1
+
2
+ from __future__ import annotations
3
+
4
+ """
5
+ Utilities for building formatted message text.
6
+
7
+ The module provides a small tree-based formatting API similar to aiogram's formatting
8
+ helpers. Formatting nodes can be composed and rendered either as HTML or Markdown.
9
+
10
+ Example:
11
+ ```python
12
+ from trueconf.utils.formatting import Bold, Link, Mention, Text
13
+
14
+ content = Text(
15
+ Bold("Important"),
16
+ " message for ",
17
+ Mention("John Doe", user_id="john_doe@video.example.com"),
18
+ "\n",
19
+ Link("Open website", url="https://trueconf.com"),
20
+ )
21
+
22
+ html = content.as_html()
23
+ markdown = content.as_markdown()
24
+ ```
25
+ """
26
+
27
+ from abc import ABC, abstractmethod
28
+
29
+ from .decorations import TextDecoration, html_decoration, markdown_decoration
30
+
31
+ class TextNode(ABC):
32
+ """
33
+ Base class for all formatting nodes.
34
+
35
+ Formatting nodes render themselves using a text decoration strategy, such as
36
+ HTML or Markdown. End users usually do not instantiate this class directly;
37
+ use :class:`Text`, :class:`Bold`, :class:`Italic`, :class:`Link`, and other
38
+ concrete nodes instead.
39
+ """
40
+
41
+ @abstractmethod
42
+ def render(self, decoration: TextDecoration) -> str:
43
+ """
44
+ Renders the node using the provided decoration strategy.
45
+
46
+ Args:
47
+ decoration (TextDecoration): Formatting strategy used to render the node.
48
+
49
+ Returns:
50
+ str: Rendered text.
51
+ """
52
+ ...
53
+
54
+
55
+ class Text(TextNode):
56
+ """
57
+ Container node for plain text and nested formatting nodes.
58
+
59
+ Args:
60
+ *body (str | TextNode): Plain strings or formatting nodes to concatenate.
61
+
62
+ Example:
63
+ ```python
64
+ content = Text("Hello, ", Bold("world"))
65
+ html = content.as_html()
66
+ ```
67
+ """
68
+ def __init__(self, *body: str | TextNode):
69
+ """
70
+ Initializes a text container.
71
+
72
+ Args:
73
+ *body (str | TextNode): Plain strings or formatting nodes.
74
+ """
75
+ self.body = body
76
+
77
+ def render(self, decoration: TextDecoration) -> str:
78
+ """
79
+ Renders the text container using the provided decoration strategy.
80
+
81
+ Args:
82
+ decoration (TextDecoration): Formatting strategy used for rendering.
83
+
84
+ Returns:
85
+ str: Rendered text.
86
+ """
87
+ parts: list[str] = []
88
+
89
+ for item in self.body:
90
+ if isinstance(item, TextNode):
91
+ parts.append(item.render(decoration))
92
+ else:
93
+ parts.append(decoration.escape(str(item)))
94
+
95
+ return "".join(parts)
96
+
97
+ def as_html(self) -> str:
98
+ """
99
+ Renders the text container as HTML.
100
+
101
+ Returns:
102
+ str: HTML representation of the formatted text.
103
+ """
104
+ return self.render(html_decoration)
105
+
106
+ def as_markdown(self) -> str:
107
+ """
108
+ Renders the text container as Markdown.
109
+
110
+ Returns:
111
+ str: Markdown representation of the formatted text.
112
+ """
113
+ return self.render(markdown_decoration)
114
+
115
+
116
+ class Bold(Text):
117
+ """Represents bold text."""
118
+ def render(self, decoration: TextDecoration) -> str:
119
+ """
120
+ Renders the nested content as bold text.
121
+
122
+ Args:
123
+ decoration (TextDecoration): Formatting strategy used for rendering.
124
+
125
+ Returns:
126
+ str: Rendered bold text.
127
+ """
128
+ return decoration.bold(super().render(decoration))
129
+
130
+
131
+ class Italic(Text):
132
+ """Represents italic text."""
133
+ def render(self, decoration: TextDecoration) -> str:
134
+ """
135
+ Renders the nested content as italic text.
136
+
137
+ Args:
138
+ decoration (TextDecoration): Formatting strategy used for rendering.
139
+
140
+ Returns:
141
+ str: Rendered italic text.
142
+ """
143
+ return decoration.italic(super().render(decoration))
144
+
145
+
146
+ class Underline(Text):
147
+ """Represents underlined text."""
148
+ def render(self, decoration: TextDecoration) -> str:
149
+ """
150
+ Renders the nested content as underlined text.
151
+
152
+ Args:
153
+ decoration (TextDecoration): Formatting strategy used for rendering.
154
+
155
+ Returns:
156
+ str: Rendered underlined text.
157
+ """
158
+ return decoration.underline(super().render(decoration))
159
+
160
+
161
+ class Strikethrough(Text):
162
+ """Represents strikethrough text."""
163
+ def render(self, decoration: TextDecoration) -> str:
164
+ """
165
+ Renders the nested content as strikethrough text.
166
+
167
+ Args:
168
+ decoration (TextDecoration): Formatting strategy used for rendering.
169
+
170
+ Returns:
171
+ str: Rendered strikethrough text.
172
+ """
173
+ return decoration.strikethrough(super().render(decoration))
174
+
175
+
176
+ class Link(Text):
177
+ """
178
+ Represents a hyperlink.
179
+
180
+ Args:
181
+ *body (str | TextNode): Link label as plain text or formatting nodes.
182
+ url (str): Link target URL.
183
+ """
184
+ def __init__(self, *body: str | TextNode, url: str):
185
+ """
186
+ Initializes a hyperlink node.
187
+
188
+ Args:
189
+ *body (str | TextNode): Link label as plain text or formatting nodes.
190
+ url (str): Link target URL.
191
+ """
192
+ super().__init__(*body)
193
+ self.url = url
194
+
195
+ def render(self, decoration: TextDecoration) -> str:
196
+ """
197
+ Renders the nested content as a hyperlink.
198
+
199
+ Args:
200
+ decoration (TextDecoration): Formatting strategy used for rendering.
201
+
202
+ Returns:
203
+ str: Rendered hyperlink.
204
+ """
205
+ return decoration.link(super().render(decoration), self.url)
206
+
207
+
208
+ class Mention(Text):
209
+ """
210
+ Represents a TrueConf user mention.
211
+
212
+ Args:
213
+ *body (str | TextNode): Mention label as plain text or formatting nodes.
214
+ user_id (str): TrueConf user ID, for example ``john_doe@video.example.com``.
215
+ """
216
+ def __init__(self, *body: str | TextNode, user_id: str):
217
+ """
218
+ Initializes a TrueConf mention node.
219
+
220
+ Args:
221
+ *body (str | TextNode): Mention label as plain text or formatting nodes.
222
+ user_id (str): TrueConf user ID.
223
+ """
224
+ super().__init__(*body)
225
+ self.user_id = user_id
226
+
227
+ def render(self, decoration: TextDecoration) -> str:
228
+ """
229
+ Renders the nested content as a TrueConf user mention.
230
+
231
+ Args:
232
+ decoration (TextDecoration): Formatting strategy used for rendering.
233
+
234
+ Returns:
235
+ str: Rendered mention.
236
+ """
237
+ return decoration.mention(super().render(decoration), self.user_id)
238
+
239
+
240
+ class AllMention(TextNode):
241
+ """Represents an ``@all`` mention."""
242
+ def render(self, decoration: TextDecoration) -> str:
243
+ """
244
+ Renders an ``@all`` mention.
245
+
246
+ Args:
247
+ decoration (TextDecoration): Formatting strategy used for rendering.
248
+
249
+ Returns:
250
+ str: Rendered ``@all`` mention.
251
+ """
252
+ return decoration.all_mention()