python-trueconf-bot 1.2.3.dev1__py3-none-any.whl → 1.3.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/exceptions.py CHANGED
@@ -1,19 +1,41 @@
1
-
2
-
3
-
4
1
  class TrueConfChatBotError(Exception):
5
2
  """
6
3
  Base exception for all TrueConf ChatBot Connector errors.
4
+
5
+ All custom exceptions raised by the library inherit from this class.
6
+ You can catch this exception to handle any library-specific error in a
7
+ single place.
7
8
  """
8
9
 
9
10
  class TokenValidationError(TrueConfChatBotError):
11
+ """
12
+ Raised when the bot token fails local validation.
13
+
14
+ This error indicates that the token value has an invalid format or
15
+ cannot be used by the connector before the authorization request is sent.
16
+ """
10
17
  pass
11
18
 
12
19
  class InvalidGrantError(TrueConfChatBotError):
20
+ """
21
+ Raised when the server rejects the provided authorization grant.
22
+
23
+ This usually means that the token or credentials are invalid, expired,
24
+ revoked, or cannot be used to authorize the bot.
25
+ """
13
26
  pass
14
27
 
15
28
  class LimitExceededError(TrueConfChatBotError):
16
- """Base exception for all limit-related errors (length, size, etc.)."""
29
+ """
30
+ Base exception for all limit-related errors.
31
+
32
+ This class is used for errors related to text length, title length,
33
+ file size, and other numeric limits.
34
+
35
+ Attributes:
36
+ actual_value: Actual value that exceeded the limit.
37
+ limit: Maximum allowed value.
38
+ """
17
39
  def __init__(self, message: str, actual_value: int, limit: int):
18
40
  super().__init__(message)
19
41
  self.actual_value = actual_value
@@ -21,7 +43,17 @@ class LimitExceededError(TrueConfChatBotError):
21
43
 
22
44
 
23
45
  class TextMessageTooLongError(LimitExceededError):
24
- """Raised when the message text exceeds TrueConf's allowed length."""
46
+ """
47
+ Raised when a text message exceeds the maximum allowed length.
48
+
49
+ The default text message limit is 4096 visible characters. For long texts,
50
+ such as LLM-generated responses, use `safe_split_text` from `trueconf.utils`
51
+ and send the returned chunks one by one.
52
+
53
+ Attributes:
54
+ actual_value: Actual visible length of the message text.
55
+ limit: Maximum allowed message length.
56
+ """
25
57
  def __init__(self, actual_length: int, limit: int = 4096):
26
58
  message = (
27
59
  f"Bad Request: text message is too long. "
@@ -31,7 +63,17 @@ class TextMessageTooLongError(LimitExceededError):
31
63
  super().__init__(message, actual_length, limit)
32
64
 
33
65
  class FileCaptionTooLongError(LimitExceededError):
34
- """Raised when the caption exceeds TrueConf's allowed length."""
66
+ """
67
+ Raised when a file caption exceeds the maximum allowed length.
68
+
69
+ The default caption limit is 4096 visible characters. For long captions,
70
+ use `safe_split_text` from `trueconf.utils` and send the returned chunks
71
+ separately when appropriate.
72
+
73
+ Attributes:
74
+ actual_value: Actual visible length of the caption.
75
+ limit: Maximum allowed caption length.
76
+ """
35
77
  def __init__(self, actual_length: int, limit: int = 4096):
36
78
  message = (
37
79
  f"Bad Request: caption is too long. "
@@ -41,11 +83,22 @@ class FileCaptionTooLongError(LimitExceededError):
41
83
  super().__init__(message, actual_length, limit)
42
84
 
43
85
  class ChatTitleTooLongError(LimitExceededError):
44
- """Base error for long chat titles."""
86
+ """
87
+ Base exception for chat title length errors.
88
+
89
+ This class is inherited by more specific exceptions for group and channel
90
+ title length validation.
91
+ """
45
92
  pass
46
93
 
47
94
  class GroupTitleTooLongError(ChatTitleTooLongError):
48
- """Raised when the group title exceeds allowed length."""
95
+ """
96
+ Raised when a group title exceeds the maximum allowed length.
97
+
98
+ Attributes:
99
+ actual_value: Actual length of the group title.
100
+ limit: Maximum allowed group title length.
101
+ """
49
102
  def __init__(self, actual_length: int, limit: int = 256):
50
103
  message = (
51
104
  f"Bad Request: group title is too long. "
@@ -54,7 +107,13 @@ class GroupTitleTooLongError(ChatTitleTooLongError):
54
107
  super().__init__(message, actual_length, limit)
55
108
 
56
109
  class ChannelTitleTooLongError(ChatTitleTooLongError):
57
- """Raised when the channel title exceeds allowed length."""
110
+ """
111
+ Raised when a channel title exceeds the maximum allowed length.
112
+
113
+ Attributes:
114
+ actual_value: Actual length of the channel title.
115
+ limit: Maximum allowed channel title length.
116
+ """
58
117
  def __init__(self, actual_length: int, limit: int = 256):
59
118
  message = (
60
119
  f"Bad Request: channel title is too long. "
@@ -63,10 +122,25 @@ class ChannelTitleTooLongError(ChatTitleTooLongError):
63
122
  super().__init__(message, actual_length, limit)
64
123
 
65
124
  class FileValidationError(TrueConfChatBotError):
66
- """Общий класс для всех ошибок валидации файлов."""
125
+ """
126
+ Base exception for file validation errors.
127
+
128
+ This class is used for errors related to file size limits, extension filters,
129
+ and other file validation rules received from TrueConf Server.
130
+ """
67
131
  pass
68
132
 
69
133
  class FileSizeTooLargeError(FileValidationError, LimitExceededError):
134
+ """
135
+ Raised when a file exceeds the maximum size allowed by the server.
136
+
137
+ File size limits are configured by the TrueConf Server administrator and
138
+ are checked by the bot before sending a file.
139
+
140
+ Attributes:
141
+ actual_value: Actual file size in bytes.
142
+ limit: Maximum allowed file size in bytes.
143
+ """
70
144
  def __init__(self, actual_size: int, limit: int):
71
145
  actual_mb = round(actual_size / (1024 * 1024), 2)
72
146
  limit_mb = round(limit / (1024 * 1024), 2)
@@ -78,6 +152,18 @@ class FileSizeTooLargeError(FileValidationError, LimitExceededError):
78
152
 
79
153
 
80
154
  class InvalidFileExtensionError(FileValidationError):
155
+ """
156
+ Raised when a file extension does not satisfy the server extension filter.
157
+
158
+ File extension rules are configured by the TrueConf Server administrator.
159
+ Depending on the filter mode, the extension list can work either as an
160
+ allow list or as a block list.
161
+
162
+ Attributes:
163
+ extension: File extension that failed validation.
164
+ extensions: Set of extensions used for validation.
165
+ mode: Extension filter mode.
166
+ """
81
167
  def __init__(self, extension: str, extensions: set[str] | None = None, mode: str = "allow"):
82
168
  self.extension = extension
83
169
  self.extensions = extensions or set()
@@ -106,6 +192,17 @@ class InvalidFileExtensionError(FileValidationError):
106
192
  super().__init__(message)
107
193
 
108
194
  class ApiErrorException(TrueConfChatBotError):
195
+ """
196
+ Raised when TrueConf Server returns an API error response.
197
+
198
+ This exception stores the server error code, human-readable detail, and
199
+ optional response payload for additional context.
200
+
201
+ Attributes:
202
+ code: Error code returned by the server.
203
+ detail: Human-readable error description.
204
+ payload: Raw error payload returned by the server, or an empty dict.
205
+ """
109
206
  def __init__(self, code: int, detail: str, payload: dict | None = None):
110
207
  super().__init__(f"[{code}] {detail}")
111
208
  self.code = code
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()