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.
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.0.dist-info}/METADATA +2 -1
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.0.dist-info}/RECORD +30 -14
- trueconf/__init__.py +2 -0
- trueconf/_version.py +2 -2
- trueconf/client/bot.py +193 -37
- trueconf/dispatcher/dispatcher.py +126 -40
- trueconf/dispatcher/router.py +156 -38
- trueconf/exceptions.py +107 -10
- trueconf/fsm/__init__.py +20 -0
- trueconf/fsm/context.py +57 -0
- trueconf/fsm/filters.py +31 -0
- trueconf/fsm/key_builder.py +39 -0
- trueconf/fsm/manager.py +36 -0
- trueconf/fsm/middleware.py +24 -0
- trueconf/fsm/state.py +135 -0
- trueconf/fsm/storage/__init__.py +4 -0
- trueconf/fsm/storage/base.py +36 -0
- trueconf/fsm/storage/memory.py +57 -0
- trueconf/fsm/strategy.py +36 -0
- trueconf/middleware.py +78 -0
- trueconf/types/message.py +57 -21
- trueconf/utils/_ssl.py +44 -0
- trueconf/utils/_token.py +4 -4
- trueconf/utils/_version_checker.py +6 -0
- trueconf/utils/formatting/__init__.py +21 -0
- trueconf/utils/formatting/decorations.py +87 -0
- trueconf/utils/formatting/text.py +252 -0
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.0.dist-info}/WHEEL +0 -0
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.0.dist-info}/top_level.txt +0 -0
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
|
|
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
|
-
|
|
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:
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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) ->
|
|
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) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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.
|
|
631
|
+
return await self.copy_to(await self.bot.me_chat)
|
|
596
632
|
else:
|
|
597
|
-
return await self.forward(await self.bot.
|
|
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
|
|
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
|
-
|
|
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=
|
|
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()
|
|
File without changes
|
{python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|