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.
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.3.0.dist-info}/METADATA +2 -1
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.3.0.dist-info}/RECORD +15 -11
- trueconf/_version.py +2 -2
- trueconf/client/bot.py +184 -34
- trueconf/exceptions.py +107 -10
- 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.3.0.dist-info}/WHEEL +0 -0
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.3.0.dist-info}/top_level.txt +0 -0
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
|
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()
|