python-trueconf-bot 1.0.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.0.0.dist-info/METADATA +115 -0
- python_trueconf_bot-1.0.0.dist-info/RECORD +120 -0
- python_trueconf_bot-1.0.0.dist-info/WHEEL +5 -0
- python_trueconf_bot-1.0.0.dist-info/licenses/LICENSE +32 -0
- python_trueconf_bot-1.0.0.dist-info/top_level.txt +1 -0
- trueconf/__init__.py +18 -0
- trueconf/_version.py +34 -0
- trueconf/client/__init__.py +0 -0
- trueconf/client/bot.py +1269 -0
- trueconf/client/context_controller.py +27 -0
- trueconf/client/session.py +60 -0
- trueconf/dispatcher/__init__.py +0 -0
- trueconf/dispatcher/dispatcher.py +56 -0
- trueconf/dispatcher/router.py +207 -0
- trueconf/enums/__init__.py +23 -0
- trueconf/enums/aouth_error.py +15 -0
- trueconf/enums/chat_participant_role.py +18 -0
- trueconf/enums/chat_type.py +17 -0
- trueconf/enums/envelope_author_type.py +13 -0
- trueconf/enums/file_ready_state.py +14 -0
- trueconf/enums/incoming_update_method.py +14 -0
- trueconf/enums/message_type.py +27 -0
- trueconf/enums/parse_mode.py +14 -0
- trueconf/enums/survey_type.py +6 -0
- trueconf/enums/update_type.py +14 -0
- trueconf/exceptions.py +17 -0
- trueconf/filters/__init__.py +11 -0
- trueconf/filters/base.py +8 -0
- trueconf/filters/command.py +32 -0
- trueconf/filters/instance_of.py +11 -0
- trueconf/filters/message.py +15 -0
- trueconf/filters/method.py +12 -0
- trueconf/loggers.py +4 -0
- trueconf/methods/__init__.py +59 -0
- trueconf/methods/add_participant_to_chat.py +22 -0
- trueconf/methods/auth.py +25 -0
- trueconf/methods/base.py +107 -0
- trueconf/methods/create_channel.py +20 -0
- trueconf/methods/create_group_chat.py +20 -0
- trueconf/methods/create_p2p_chat.py +20 -0
- trueconf/methods/edit_message.py +25 -0
- trueconf/methods/edit_survey.py +32 -0
- trueconf/methods/forward_message.py +22 -0
- trueconf/methods/get_chat_by_id.py +20 -0
- trueconf/methods/get_chat_history.py +24 -0
- trueconf/methods/get_chat_participants.py +24 -0
- trueconf/methods/get_chats.py +22 -0
- trueconf/methods/get_file_info.py +20 -0
- trueconf/methods/get_message_by_id.py +19 -0
- trueconf/methods/get_user_display_name.py +20 -0
- trueconf/methods/has_chat_participant.py +22 -0
- trueconf/methods/remove_chat.py +20 -0
- trueconf/methods/remove_message.py +23 -0
- trueconf/methods/remove_participant_from_chat.py +23 -0
- trueconf/methods/send_file.py +25 -0
- trueconf/methods/send_message.py +29 -0
- trueconf/methods/send_survey.py +40 -0
- trueconf/methods/subscribe_file_progress.py +21 -0
- trueconf/methods/unsubscribe_file_progress.py +21 -0
- trueconf/methods/upload_file.py +21 -0
- trueconf/py.typed +0 -0
- trueconf/types/__init__.py +25 -0
- trueconf/types/author_box.py +17 -0
- trueconf/types/chat_participant.py +11 -0
- trueconf/types/content/__init__.py +25 -0
- trueconf/types/content/attachment.py +14 -0
- trueconf/types/content/base.py +8 -0
- trueconf/types/content/chat_created.py +9 -0
- trueconf/types/content/document.py +71 -0
- trueconf/types/content/forward_message.py +21 -0
- trueconf/types/content/photo.py +70 -0
- trueconf/types/content/remove_participant.py +9 -0
- trueconf/types/content/sticker.py +70 -0
- trueconf/types/content/survey.py +19 -0
- trueconf/types/content/text.py +9 -0
- trueconf/types/content/video.py +71 -0
- trueconf/types/last_message.py +33 -0
- trueconf/types/message.py +411 -0
- trueconf/types/parser.py +90 -0
- trueconf/types/requests/__init__.py +21 -0
- trueconf/types/requests/added_chat_participant.py +40 -0
- trueconf/types/requests/created_channel.py +42 -0
- trueconf/types/requests/created_group_chat.py +42 -0
- trueconf/types/requests/created_personal_chat.py +42 -0
- trueconf/types/requests/edited_message.py +37 -0
- trueconf/types/requests/removed_chat.py +32 -0
- trueconf/types/requests/removed_chat_participant.py +39 -0
- trueconf/types/requests/removed_message.py +37 -0
- trueconf/types/requests/uploading_progress.py +34 -0
- trueconf/types/responses/__init__.py +57 -0
- trueconf/types/responses/add_chat_participant_response.py +8 -0
- trueconf/types/responses/api_error.py +38 -0
- trueconf/types/responses/auth_response_payload.py +9 -0
- trueconf/types/responses/create_channel_response.py +8 -0
- trueconf/types/responses/create_group_chat_response.py +8 -0
- trueconf/types/responses/create_p2p_chat_response.py +8 -0
- trueconf/types/responses/edit_message_response.py +9 -0
- trueconf/types/responses/edit_survey_response.py +9 -0
- trueconf/types/responses/forward_message_response.py +10 -0
- trueconf/types/responses/get_chat_by_id_response.py +13 -0
- trueconf/types/responses/get_chat_history_response.py +13 -0
- trueconf/types/responses/get_chat_participants_response.py +12 -0
- trueconf/types/responses/get_chats_response.py +12 -0
- trueconf/types/responses/get_file_info_response.py +24 -0
- trueconf/types/responses/get_message_by_id_response.py +26 -0
- trueconf/types/responses/get_user_display_name_response.py +8 -0
- trueconf/types/responses/has_chat_participant_response.py +8 -0
- trueconf/types/responses/remove_chat_participant_response.py +8 -0
- trueconf/types/responses/remove_chat_response.py +8 -0
- trueconf/types/responses/remove_message_response.py +8 -0
- trueconf/types/responses/send_file_response.py +10 -0
- trueconf/types/responses/send_message_response.py +10 -0
- trueconf/types/responses/send_survey_response.py +10 -0
- trueconf/types/responses/subscribe_file_progress_response.py +8 -0
- trueconf/types/responses/unsubscribe_file_progress_response.py +8 -0
- trueconf/types/responses/upload_file_response.py +8 -0
- trueconf/types/update.py +12 -0
- trueconf/utils/__init__.py +3 -0
- trueconf/utils/generate_secret_for_survey.py +10 -0
- trueconf/utils/token.py +78 -0
trueconf/client/bot.py
ADDED
|
@@ -0,0 +1,1269 @@
|
|
|
1
|
+
import aiofiles
|
|
2
|
+
import asyncio
|
|
3
|
+
import contextlib
|
|
4
|
+
import httpx
|
|
5
|
+
import json
|
|
6
|
+
import mimetypes
|
|
7
|
+
import signal
|
|
8
|
+
import ssl
|
|
9
|
+
import tempfile
|
|
10
|
+
import websockets
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from contextlib import AsyncExitStack
|
|
13
|
+
from typing import (
|
|
14
|
+
Callable,
|
|
15
|
+
Awaitable,
|
|
16
|
+
Dict,
|
|
17
|
+
List,
|
|
18
|
+
Tuple,
|
|
19
|
+
TypeVar,
|
|
20
|
+
Self,
|
|
21
|
+
TypedDict,
|
|
22
|
+
Unpack
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from trueconf import loggers
|
|
26
|
+
from trueconf.client.session import WebSocketSession
|
|
27
|
+
from trueconf.dispatcher.dispatcher import Dispatcher
|
|
28
|
+
from trueconf.enums.file_ready_state import FileReadyState
|
|
29
|
+
from trueconf.enums.parse_mode import ParseMode
|
|
30
|
+
from trueconf.enums.survey_type import SurveyType
|
|
31
|
+
from trueconf.exceptions import ApiError
|
|
32
|
+
from trueconf.methods.add_participant_to_chat import AddChatParticipant
|
|
33
|
+
from trueconf.methods.auth import AuthMethod
|
|
34
|
+
from trueconf.methods.base import TrueConfMethod
|
|
35
|
+
from trueconf.methods.create_channel import CreateChannel
|
|
36
|
+
from trueconf.methods.create_group_chat import CreateGroupChat
|
|
37
|
+
from trueconf.methods.create_p2p_chat import CreateP2PChat
|
|
38
|
+
from trueconf.methods.edit_message import EditMessage
|
|
39
|
+
from trueconf.methods.edit_survey import EditSurvey
|
|
40
|
+
from trueconf.methods.forward_message import ForwardMessage
|
|
41
|
+
from trueconf.methods.get_chat_by_id import GetChatByID
|
|
42
|
+
from trueconf.methods.get_chat_history import GetChatHistory
|
|
43
|
+
from trueconf.methods.get_chat_participants import GetChatParticipants
|
|
44
|
+
from trueconf.methods.get_chats import GetChats
|
|
45
|
+
from trueconf.methods.get_file_info import GetFileInfo
|
|
46
|
+
from trueconf.methods.get_message_by_id import GetMessageById
|
|
47
|
+
from trueconf.methods.get_user_display_name import GetUserDisplayName
|
|
48
|
+
from trueconf.methods.has_chat_participant import HasChatParticipant
|
|
49
|
+
from trueconf.methods.remove_chat import RemoveChat
|
|
50
|
+
from trueconf.methods.remove_message import RemoveMessage
|
|
51
|
+
from trueconf.methods.remove_participant_from_chat import RemoveChatParticipant
|
|
52
|
+
from trueconf.methods.send_file import SendFile
|
|
53
|
+
from trueconf.methods.send_message import SendMessage
|
|
54
|
+
from trueconf.methods.send_survey import SendSurvey
|
|
55
|
+
from trueconf.methods.subscribe_file_progress import SubscribeFileProgress
|
|
56
|
+
from trueconf.methods.unsubscribe_file_progress import UnsubscribeFileProgress
|
|
57
|
+
from trueconf.methods.upload_file import UploadFile
|
|
58
|
+
from trueconf.types.parser import parse_update
|
|
59
|
+
from trueconf.types.requests.uploading_progress import UploadingProgress
|
|
60
|
+
from trueconf.types.responses.add_chat_participant_response import AddChatParticipantResponse
|
|
61
|
+
from trueconf.types.responses.api_error import ApiError
|
|
62
|
+
from trueconf.types.responses.create_channel_response import CreateChannelResponse
|
|
63
|
+
from trueconf.types.responses.create_group_chat_response import CreateGroupChatResponse
|
|
64
|
+
from trueconf.types.responses.create_p2p_chat_response import CreateP2PChatResponse
|
|
65
|
+
from trueconf.types.responses.edit_message_response import EditMessageResponse
|
|
66
|
+
from trueconf.types.responses.edit_survey_response import EditSurveyResponse
|
|
67
|
+
from trueconf.types.responses.forward_message_response import ForwardMessageResponse
|
|
68
|
+
from trueconf.types.responses.get_chat_by_id_response import GetChatByIdResponse
|
|
69
|
+
from trueconf.types.responses.get_chat_history_response import GetChatHistoryResponse
|
|
70
|
+
from trueconf.types.responses.get_chat_participants_response import GetChatParticipantsResponse
|
|
71
|
+
from trueconf.types.responses.get_chats_response import GetChatsResponse
|
|
72
|
+
from trueconf.types.responses.get_file_info_response import GetFileInfoResponse
|
|
73
|
+
from trueconf.types.responses.get_message_by_id_response import GetMessageByIdResponse
|
|
74
|
+
from trueconf.types.responses.get_user_display_name_response import GetUserDisplayNameResponse
|
|
75
|
+
from trueconf.types.responses.has_chat_participant_response import HasChatParticipantResponse
|
|
76
|
+
from trueconf.types.responses.remove_chat_participant_response import RemoveChatParticipantResponse
|
|
77
|
+
from trueconf.types.responses.remove_chat_response import RemoveChatResponse
|
|
78
|
+
from trueconf.types.responses.remove_message_response import RemoveMessageResponse
|
|
79
|
+
from trueconf.types.responses.send_file_response import SendFileResponse
|
|
80
|
+
from trueconf.types.responses.send_message_response import SendMessageResponse
|
|
81
|
+
from trueconf.types.responses.send_survey_response import SendSurveyResponse
|
|
82
|
+
from trueconf.types.responses.subscribe_file_progress_response import SubscribeFileProgressResponse
|
|
83
|
+
from trueconf.types.responses.unsubscribe_file_progress_response import UnsubscribeFileProgressResponse
|
|
84
|
+
from trueconf.utils import generate_secret_for_survey
|
|
85
|
+
from trueconf.utils import get_auth_token
|
|
86
|
+
from trueconf.utils import validate_token
|
|
87
|
+
|
|
88
|
+
T = TypeVar("T")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TokenOpts(TypedDict, total=False):
|
|
92
|
+
web_port: int
|
|
93
|
+
https: bool
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Bot:
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
server: str,
|
|
100
|
+
token: str,
|
|
101
|
+
web_port: int = 443,
|
|
102
|
+
https: bool = True,
|
|
103
|
+
debug: bool = False,
|
|
104
|
+
verify_ssl: bool = True,
|
|
105
|
+
dispatcher: Dispatcher | None = None,
|
|
106
|
+
receive_unread_messages: bool = False,
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
Initializes a TrueConf chatbot instance with WebSocket connection and configuration options.
|
|
110
|
+
|
|
111
|
+
Source:
|
|
112
|
+
https://trueconf.com/docs/chatbot-connector/en/connect-and-auth/#websocket-connection-authorization
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
server (str): Address of the TrueConf server.
|
|
116
|
+
token (str): Bot authorization token.
|
|
117
|
+
web_port (int, optional): WebSocket connection port. Defaults to 443.
|
|
118
|
+
https (bool, optional): Whether to use HTTPS protocol. Defaults to True.
|
|
119
|
+
debug (bool, optional): Enables debug mode. Defaults to False.
|
|
120
|
+
verify_ssl (bool, optional): Whether to verify the server's SSL certificate. Defaults to True.
|
|
121
|
+
dispatcher (Dispatcher | None, optional): Dispatcher instance for registering handlers.
|
|
122
|
+
receive_unread_messages (bool, optional): Whether to receive unread messages on connection. Defaults to False.
|
|
123
|
+
|
|
124
|
+
Note:
|
|
125
|
+
Alternatively, you can authorize using a username and password via the `from_credentials()` class method.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
validate_token(token)
|
|
129
|
+
|
|
130
|
+
self.server = server
|
|
131
|
+
self.__token = token
|
|
132
|
+
self.web_port = web_port
|
|
133
|
+
self.https = https
|
|
134
|
+
self.debug = debug
|
|
135
|
+
self.connected_event = asyncio.Event()
|
|
136
|
+
self.authorized_event = asyncio.Event()
|
|
137
|
+
self._session: WebSocketSession | None = None
|
|
138
|
+
self._connect_task: asyncio.Task | None = None
|
|
139
|
+
self.stopped_event = asyncio.Event()
|
|
140
|
+
self.dp = dispatcher or Dispatcher()
|
|
141
|
+
self._protocol = "https" if self.https else "http"
|
|
142
|
+
self.port = 443 if self.https else self.web_port
|
|
143
|
+
self.receive_unread_messages = receive_unread_messages
|
|
144
|
+
self._url_for_upload_files = (
|
|
145
|
+
f"{self._protocol}://{self.server}:{self.port}/bridge/api/client/v1/files"
|
|
146
|
+
)
|
|
147
|
+
self.verify_ssl = verify_ssl
|
|
148
|
+
self._progress_queues: Dict[str, asyncio.Queue] = {}
|
|
149
|
+
|
|
150
|
+
self._domain = None
|
|
151
|
+
|
|
152
|
+
self._futures: Dict[int, asyncio.Future] = {}
|
|
153
|
+
self._handlers: List[Tuple[dict, Callable[[dict], Awaitable]]] = []
|
|
154
|
+
|
|
155
|
+
self._stop = False
|
|
156
|
+
self._ws = None
|
|
157
|
+
|
|
158
|
+
async def __call__(self, method: TrueConfMethod[T]) -> T:
|
|
159
|
+
return await method(self)
|
|
160
|
+
|
|
161
|
+
def __get_domain_name(self):
|
|
162
|
+
url = f"{self._protocol}://{self.server}:{self.port}/api/v4/server"
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
with httpx.Client(verify=False, timeout=5) as client:
|
|
166
|
+
response = client.get(url)
|
|
167
|
+
return response.json().get("product").get("display_name")
|
|
168
|
+
except Exception as e:
|
|
169
|
+
loggers.chatbot.error(f"Failed to get server domain_name: {e}")
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def token(self) -> str:
|
|
174
|
+
"""
|
|
175
|
+
Returns the bot's authorization token.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
str: The access token used for authentication.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
return self.__token
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def server_name(self) -> str:
|
|
185
|
+
"""
|
|
186
|
+
Returns the domain name of the TrueConf server.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
str: Domain name of the connected server.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
if self._domain is None:
|
|
193
|
+
self._domain = self.__get_domain_name()
|
|
194
|
+
return self._domain
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def from_credentials(
|
|
198
|
+
cls,
|
|
199
|
+
server: str,
|
|
200
|
+
username: str,
|
|
201
|
+
password: str,
|
|
202
|
+
dispatcher: Dispatcher | None = None,
|
|
203
|
+
receive_unread_messages: bool = False,
|
|
204
|
+
verify_ssl: bool = True,
|
|
205
|
+
**token_opts: Unpack[TokenOpts],
|
|
206
|
+
) -> Self:
|
|
207
|
+
"""
|
|
208
|
+
Creates a bot instance using username and password authentication.
|
|
209
|
+
|
|
210
|
+
Source:
|
|
211
|
+
https://trueconf.com/docs/chatbot-connector/en/getting-started/#authorization
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
server (str): Address of the TrueConf server.
|
|
215
|
+
username (str): Username for authentication.
|
|
216
|
+
password (str): Password for authentication.
|
|
217
|
+
dispatcher (Dispatcher | None, optional): Dispatcher instance for registering handlers.
|
|
218
|
+
receive_unread_messages (bool, optional): Whether to receive unread messages on connection. Defaults to False.
|
|
219
|
+
verify_ssl (bool, optional): Whether to verify the server's SSL certificate. Defaults to True.
|
|
220
|
+
**token_opts: Additional options passed to the token request, such as `web_port` and `https`.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Bot: An authorized bot instance.
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
RuntimeError: If the token could not be obtained.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
token = get_auth_token(server, username, password, verify=verify_ssl)
|
|
230
|
+
if not token:
|
|
231
|
+
raise RuntimeError("Failed to obtain token")
|
|
232
|
+
return cls(
|
|
233
|
+
server,
|
|
234
|
+
token,
|
|
235
|
+
web_port=token_opts.get("web_port", 443),
|
|
236
|
+
https=token_opts.get("https", True),
|
|
237
|
+
dispatcher=dispatcher,
|
|
238
|
+
receive_unread_messages=receive_unread_messages,
|
|
239
|
+
verify_ssl=verify_ssl,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
async def __wait_upload_complete(
|
|
243
|
+
self,
|
|
244
|
+
file_id: str,
|
|
245
|
+
expected_size: int,
|
|
246
|
+
timeout: float | None = None,
|
|
247
|
+
) -> bool:
|
|
248
|
+
q = self._progress_queues.get(file_id)
|
|
249
|
+
if q is None:
|
|
250
|
+
q = asyncio.Queue()
|
|
251
|
+
self._progress_queues[file_id] = q
|
|
252
|
+
|
|
253
|
+
await self.subscribe_file_progress(file_id)
|
|
254
|
+
try:
|
|
255
|
+
while True:
|
|
256
|
+
if timeout is None:
|
|
257
|
+
update = await q.get()
|
|
258
|
+
else:
|
|
259
|
+
update = await asyncio.wait_for(q.get(), timeout=timeout)
|
|
260
|
+
|
|
261
|
+
if update.progress >= expected_size:
|
|
262
|
+
return True
|
|
263
|
+
except asyncio.TimeoutError:
|
|
264
|
+
return False
|
|
265
|
+
finally:
|
|
266
|
+
await self.unsubscribe_file_progress(file_id)
|
|
267
|
+
if self._progress_queues.get(file_id) is q:
|
|
268
|
+
self._progress_queues.pop(file_id, None)
|
|
269
|
+
|
|
270
|
+
async def __download_file_from_server(
|
|
271
|
+
self,
|
|
272
|
+
url: str,
|
|
273
|
+
file_name: str,
|
|
274
|
+
dest_path: str | Path | None = None,
|
|
275
|
+
verify: bool | None = None,
|
|
276
|
+
timeout: int = 60,
|
|
277
|
+
chunk_size: int = 64 * 1024,
|
|
278
|
+
) -> Path | None:
|
|
279
|
+
|
|
280
|
+
"""
|
|
281
|
+
Asynchronously download file by URL and save it to disk.
|
|
282
|
+
|
|
283
|
+
If `dest_path` isn't provided, a temporary file will be created (similar to aiogram.File.download()).
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
url: Direct download URL.
|
|
287
|
+
dest_path: Destination path; if None, a NamedTemporaryFile will be created and returned.
|
|
288
|
+
verify: SSL verification flag; defaults to self.verify_ssl.
|
|
289
|
+
timeout: Request timeout (seconds).
|
|
290
|
+
chunk_size: Stream chunk size in bytes.
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Path | None: Path to saved file, or None on error.
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
v = self.verify_ssl if verify is None else verify
|
|
298
|
+
|
|
299
|
+
if dest_path is None:
|
|
300
|
+
tmp = tempfile.NamedTemporaryFile(prefix="tc_dl_", suffix=file_name, delete=False)
|
|
301
|
+
dest = Path(tmp.name)
|
|
302
|
+
tmp.close()
|
|
303
|
+
else:
|
|
304
|
+
dest = Path(dest_path) / file_name
|
|
305
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
async with httpx.AsyncClient(verify=v, timeout=httpx.Timeout(timeout)) as client:
|
|
309
|
+
async with client.stream("GET", url) as resp:
|
|
310
|
+
resp.raise_for_status()
|
|
311
|
+
async with aiofiles.open(dest, "wb") as f:
|
|
312
|
+
async for chunk in resp.aiter_bytes(chunk_size):
|
|
313
|
+
if chunk:
|
|
314
|
+
await f.write(chunk)
|
|
315
|
+
return dest
|
|
316
|
+
except Exception as e:
|
|
317
|
+
loggers.chatbot.error(f"Failed to download file from {url}: {e}")
|
|
318
|
+
with contextlib.suppress(Exception):
|
|
319
|
+
if dest.exists():
|
|
320
|
+
dest.unlink()
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
async def __upload_file_to_server(
|
|
324
|
+
self,
|
|
325
|
+
file_path: str,
|
|
326
|
+
preview_path: str | None = None,
|
|
327
|
+
preview_mimetype: str | None = None,
|
|
328
|
+
verify: bool = True,
|
|
329
|
+
timeout: int = 60,
|
|
330
|
+
) -> str | None:
|
|
331
|
+
"""
|
|
332
|
+
Uploads a file to the server and returns its temporary identifier (temporalFileId).
|
|
333
|
+
|
|
334
|
+
Source:
|
|
335
|
+
https://trueconf.com/docs/chatbot-connector/en/files/#uploading-a-file-to-the-file-storage
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
file_path (str): Path to the file to be uploaded.
|
|
339
|
+
preview_path (bytes | None, optional): Preview data in WebP format (if applicable).
|
|
340
|
+
preview_mimetype (str, optional): MIME type of the preview, e.g., "image/webp".
|
|
341
|
+
verify (bool, optional): Whether to verify the SSL certificate. Defaults to True.
|
|
342
|
+
timeout (int, optional): Upload timeout in seconds. Defaults to 60.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
str | None: Temporary file identifier (temporalFileId), or None if the upload failed.
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
file = Path(file_path)
|
|
349
|
+
file_size = file.stat().st_size
|
|
350
|
+
file_name = file.name
|
|
351
|
+
file_mimetype = mimetypes.guess_type(file_path)[0]
|
|
352
|
+
|
|
353
|
+
res = await self(UploadFile(file_size=file_size))
|
|
354
|
+
upload_task_id = res.upload_task_id
|
|
355
|
+
|
|
356
|
+
headers = {
|
|
357
|
+
"Upload-Task-Id": upload_task_id,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
|
|
362
|
+
async with AsyncExitStack() as stack:
|
|
363
|
+
client = await stack.enter_async_context(
|
|
364
|
+
httpx.AsyncClient(verify=verify, timeout=httpx.Timeout(timeout)))
|
|
365
|
+
|
|
366
|
+
f = stack.enter_context(open(file_path, "rb"))
|
|
367
|
+
files = {"file": (file_name, f, file_mimetype)}
|
|
368
|
+
|
|
369
|
+
if preview_path is not None:
|
|
370
|
+
p = stack.enter_context(open(preview_path, "rb"))
|
|
371
|
+
files["preview"] = (file_name, p, mimetypes.guess_type(preview_path)[0])
|
|
372
|
+
|
|
373
|
+
elif preview_mimetype == "sticker/webp":
|
|
374
|
+
files["preview"] = (file_name, f, preview_mimetype)
|
|
375
|
+
|
|
376
|
+
response = await client.post(self._url_for_upload_files, headers=headers, files=files)
|
|
377
|
+
return response.json().get("temporalFileId")
|
|
378
|
+
except Exception as e:
|
|
379
|
+
loggers.chatbot.error(f"Failed to upload file to server: {e}")
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
async def _send_ws_payload(self, message: dict) -> bool:
|
|
383
|
+
if not self._session:
|
|
384
|
+
loggers.chatbot.warning("Session is None — not connected")
|
|
385
|
+
return False
|
|
386
|
+
try:
|
|
387
|
+
await self._session.send_json(message)
|
|
388
|
+
return True
|
|
389
|
+
except Exception as e:
|
|
390
|
+
loggers.chatbot.error(f"❌ Send failed or connection closed: {e}")
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
async def __connect_and_listen(self):
|
|
394
|
+
ssl_context = ssl._create_unverified_context() if self.https else None
|
|
395
|
+
uri = f"wss://{self.server}:{self.web_port}/websocket/chat_bot"
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
async for ws in websockets.connect(
|
|
399
|
+
uri, ssl=ssl_context, ping_interval=30, ping_timeout=10
|
|
400
|
+
):
|
|
401
|
+
if self._stop:
|
|
402
|
+
break
|
|
403
|
+
|
|
404
|
+
self._ws = ws
|
|
405
|
+
loggers.chatbot.info("✅ WebSocket connected")
|
|
406
|
+
|
|
407
|
+
if self._session is None:
|
|
408
|
+
self._session = WebSocketSession(on_message=self.__on_raw_message)
|
|
409
|
+
self._session.attach(ws)
|
|
410
|
+
|
|
411
|
+
self.connected_event.set()
|
|
412
|
+
self.authorized_event.clear()
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
await self.__authorize()
|
|
416
|
+
self.authorized_event.set()
|
|
417
|
+
try:
|
|
418
|
+
await ws.wait_closed()
|
|
419
|
+
except asyncio.CancelledError:
|
|
420
|
+
loggers.chatbot.info("🛑 Cancellation requested; closing ws")
|
|
421
|
+
with contextlib.suppress(Exception):
|
|
422
|
+
await ws.close()
|
|
423
|
+
raise
|
|
424
|
+
|
|
425
|
+
except websockets.exceptions.ConnectionClosed as e:
|
|
426
|
+
loggers.chatbot.warning(
|
|
427
|
+
f"🔌 Connection closed: {getattr(e, 'code', '?')} - {getattr(e, 'reason', '?')}"
|
|
428
|
+
)
|
|
429
|
+
continue
|
|
430
|
+
except asyncio.CancelledError:
|
|
431
|
+
loggers.chatbot.info("🛑 Connect loop cancelled")
|
|
432
|
+
raise
|
|
433
|
+
except ApiError as e:
|
|
434
|
+
print(e)
|
|
435
|
+
await self.shutdown()
|
|
436
|
+
finally:
|
|
437
|
+
self.connected_event.clear()
|
|
438
|
+
if self._session:
|
|
439
|
+
await self._session.detach()
|
|
440
|
+
if self._stop:
|
|
441
|
+
break
|
|
442
|
+
except asyncio.CancelledError:
|
|
443
|
+
loggers.chatbot.info("🛑 connect_and_listen task finished by cancellation")
|
|
444
|
+
raise
|
|
445
|
+
|
|
446
|
+
def _register_future(self, id_: int, future):
|
|
447
|
+
loggers.chatbot.debug(f"📬 Registered future for id={id_}")
|
|
448
|
+
self._futures[id_] = future
|
|
449
|
+
|
|
450
|
+
def __resolve_future(self, message: dict):
|
|
451
|
+
if message.get("type") == 2 and "id" in message:
|
|
452
|
+
future = self._futures.pop(message["id"], None)
|
|
453
|
+
if future and not future.done():
|
|
454
|
+
future.set_result(message)
|
|
455
|
+
|
|
456
|
+
async def __authorize(self):
|
|
457
|
+
loggers.chatbot.info("🚀 Starting authorization")
|
|
458
|
+
|
|
459
|
+
call = AuthMethod(
|
|
460
|
+
token=self.__token, receive_unread_messages=self.receive_unread_messages
|
|
461
|
+
)
|
|
462
|
+
loggers.chatbot.info(f"🛠 Created AuthMethod with id={call.id}")
|
|
463
|
+
result = await self(call)
|
|
464
|
+
loggers.chatbot.info(f"🔐 Authenticated as {result.user_id}")
|
|
465
|
+
|
|
466
|
+
async def __process_message(self, data: dict):
|
|
467
|
+
data = parse_update(data)
|
|
468
|
+
if data is None:
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
if isinstance(data, UploadingProgress):
|
|
472
|
+
q = self._progress_queues.get(data.file_id)
|
|
473
|
+
if q:
|
|
474
|
+
q.put_nowait(data)
|
|
475
|
+
return
|
|
476
|
+
|
|
477
|
+
if hasattr(data, "bind"):
|
|
478
|
+
data.bind(self)
|
|
479
|
+
|
|
480
|
+
payload = getattr(data, "payload", None)
|
|
481
|
+
if hasattr(payload, "bind"):
|
|
482
|
+
payload.bind(self)
|
|
483
|
+
|
|
484
|
+
await self.dp._feed_update(data)
|
|
485
|
+
|
|
486
|
+
async def __on_raw_message(self, raw: str):
|
|
487
|
+
try:
|
|
488
|
+
data = json.loads(raw)
|
|
489
|
+
except Exception as e:
|
|
490
|
+
loggers.chatbot.error(f"Failed to parse incoming message: {e}; raw={raw!r}")
|
|
491
|
+
return
|
|
492
|
+
# --- auto‑acknowledge every server request (type == 1) ---
|
|
493
|
+
if isinstance(data, dict) and data.get("type") == 1 and "id" in data:
|
|
494
|
+
# reply with {"type": 2, "id": <same id>}
|
|
495
|
+
asyncio.create_task(self._send_ws_payload({"type": 2, "id": data["id"]}))
|
|
496
|
+
self.__resolve_future(data)
|
|
497
|
+
asyncio.create_task(self.__process_message(data))
|
|
498
|
+
|
|
499
|
+
async def add_participant_to_chat(
|
|
500
|
+
self, chat_id: str, user_id: str
|
|
501
|
+
) -> AddChatParticipantResponse:
|
|
502
|
+
"""
|
|
503
|
+
Adds a participant to the specified chat.
|
|
504
|
+
|
|
505
|
+
Source:
|
|
506
|
+
https://trueconf.com/docs/chatbot-connector/en/chats/#adding-a-participant-to-the-chat
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
chat_id (str): Identifier of the chat to add the participant to.
|
|
510
|
+
user_id (str): Identifier of the user to be added.
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
AddChatParticipantResponse: Object containing the result of the participant addition.
|
|
514
|
+
"""
|
|
515
|
+
|
|
516
|
+
if "@" not in user_id:
|
|
517
|
+
user_id = f"{user_id}@{self.server_name}"
|
|
518
|
+
|
|
519
|
+
call = AddChatParticipant(chat_id=chat_id, user_id=user_id)
|
|
520
|
+
return await self(call)
|
|
521
|
+
|
|
522
|
+
async def create_channel(self, title: str) -> CreateChannelResponse:
|
|
523
|
+
"""
|
|
524
|
+
Creates a new channel with the specified title.
|
|
525
|
+
|
|
526
|
+
Source:
|
|
527
|
+
https://trueconf.com/docs/chatbot-connector/en/chats/#creating-a-channel
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
title (str): Title of the new channel.
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
CreateChannelResponse: Object containing the result of the channel creation.
|
|
534
|
+
"""
|
|
535
|
+
|
|
536
|
+
loggers.chatbot.info(f"✉️ Create channel with name {title}")
|
|
537
|
+
call = CreateChannel(title=title)
|
|
538
|
+
return await self(call)
|
|
539
|
+
|
|
540
|
+
async def create_group_chat(self, title: str) -> CreateGroupChatResponse:
|
|
541
|
+
"""
|
|
542
|
+
Creates a new group chat with the specified title.
|
|
543
|
+
|
|
544
|
+
Source:
|
|
545
|
+
https://trueconf.com/docs/chatbot-connector/en/chats/#creating-a-group-chat
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
title (str): Title of the new group chat.
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
CreateGroupChatResponse: Object containing the result of the group chat creation.
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
loggers.chatbot.info(f"✉️ Create group chat with name {title}")
|
|
555
|
+
call = CreateGroupChat(title=title)
|
|
556
|
+
return await self(call)
|
|
557
|
+
|
|
558
|
+
async def create_personal_chat(self, user_id: str) -> CreateP2PChatResponse:
|
|
559
|
+
"""
|
|
560
|
+
Creates a personal (P2P) chat with a user by their identifier.
|
|
561
|
+
|
|
562
|
+
Source:
|
|
563
|
+
https://trueconf.com/docs/chatbot-connector/en/chats/#creating-a-personal-chat-with-a-user
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
user_id (str): Identifier of the user. Can be with or without a domain.
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
CreateP2PChatResponse: Object containing the result of the personal chat creation.
|
|
570
|
+
|
|
571
|
+
Note:
|
|
572
|
+
Creating a personal chat (peer-to-peer) with a server user.
|
|
573
|
+
If the bot has never messaged this user before, a new chat will be created.
|
|
574
|
+
If the bot has previously sent messages to this user, the existing chat will be returned.
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
loggers.chatbot.info(f"✉️ Create personal chat with name {user_id}")
|
|
578
|
+
|
|
579
|
+
if "@" not in user_id:
|
|
580
|
+
user_id = f"{user_id}@{self.server_name}"
|
|
581
|
+
|
|
582
|
+
call = CreateP2PChat(user_id=user_id)
|
|
583
|
+
return await self(call)
|
|
584
|
+
|
|
585
|
+
async def delete_chat(self, chat_id: str) -> RemoveChatResponse:
|
|
586
|
+
"""
|
|
587
|
+
Deletes a chat by its identifier.
|
|
588
|
+
|
|
589
|
+
Source:
|
|
590
|
+
https://trueconf.com/docs/chatbot-connector/en/chats/#deleting-chat
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
chat_id: Identifier of the chat to be deleted.
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
RemoveChatResponse: Object containing the result of the chat deletion.
|
|
597
|
+
"""
|
|
598
|
+
|
|
599
|
+
call = RemoveChat(chat_id=chat_id)
|
|
600
|
+
return await self(call)
|
|
601
|
+
|
|
602
|
+
async def download_file_by_id(self, file_id, dest_path: str = None) -> Path | None:
|
|
603
|
+
"""
|
|
604
|
+
Downloads a file by its ID, waiting for the upload to complete if necessary.
|
|
605
|
+
|
|
606
|
+
If the file is already in the READY state, it will be downloaded immediately.
|
|
607
|
+
If the file is in the NOT_AVAILABLE state, the method will exit without downloading.
|
|
608
|
+
In other cases, the bot will wait for the upload to finish and then attempt to download the file.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
file_id (str): Unique identifier of the file on the server.
|
|
612
|
+
dest_path (str, optional): Path where the file should be saved.
|
|
613
|
+
If not specified, a temporary file will be created using `NamedTemporaryFile`
|
|
614
|
+
(with prefix `tc_dl_`, suffix set to the original file name, and `delete=False` to keep the file on disk).
|
|
615
|
+
|
|
616
|
+
Returns:
|
|
617
|
+
Path | None: Path to the downloaded file, or None if the download failed.
|
|
618
|
+
"""
|
|
619
|
+
|
|
620
|
+
info = await self.get_file_info(file_id)
|
|
621
|
+
|
|
622
|
+
if info.ready_state == FileReadyState.READY:
|
|
623
|
+
return await self.__download_file_from_server(
|
|
624
|
+
url=info.download_url,
|
|
625
|
+
file_name=info.name,
|
|
626
|
+
dest_path=dest_path,
|
|
627
|
+
verify=self.verify_ssl
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
if info.ready_state == FileReadyState.NOT_AVAILABLE:
|
|
631
|
+
loggers.chatbot.warning(f"File {file_id} is NOT_AVAILABLE")
|
|
632
|
+
return None
|
|
633
|
+
|
|
634
|
+
ok = await self.__wait_upload_complete(file_id, expected_size=info.size, timeout=None)
|
|
635
|
+
if not ok:
|
|
636
|
+
loggers.chatbot.error(f"Wait upload complete failed for {file_id}")
|
|
637
|
+
return None
|
|
638
|
+
|
|
639
|
+
for _ in range(20):
|
|
640
|
+
info = await self.get_file_info(file_id)
|
|
641
|
+
if info.ready_state == FileReadyState.READY:
|
|
642
|
+
break
|
|
643
|
+
await asyncio.sleep(1)
|
|
644
|
+
else:
|
|
645
|
+
loggers.chatbot.warning(f"File {file_id} didn’t reach READY in time")
|
|
646
|
+
return None
|
|
647
|
+
|
|
648
|
+
return await self.__download_file_from_server(
|
|
649
|
+
url=info.download_url,
|
|
650
|
+
file_name=info.name,
|
|
651
|
+
dest_path=dest_path,
|
|
652
|
+
verify=self.verify_ssl
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
async def edit_message(
|
|
656
|
+
self, message_id: str, text: str, parse_mode: ParseMode | str = ParseMode.TEXT
|
|
657
|
+
) -> EditMessageResponse:
|
|
658
|
+
"""
|
|
659
|
+
Edits a previously sent message.
|
|
660
|
+
|
|
661
|
+
Source:
|
|
662
|
+
https://trueconf.com/docs/chatbot-connector/en/messages/#editing-an-existing-message
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
message_id (str): Identifier of the message to be edited.
|
|
666
|
+
text (str): New text content for the message.
|
|
667
|
+
parse_mode (ParseMode | str, optional): Text formatting mode.
|
|
668
|
+
Defaults to plain text.
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
EditMessageResponse: Object containing the result of the message update.
|
|
672
|
+
"""
|
|
673
|
+
|
|
674
|
+
call = EditMessage(message_id=message_id, text=text, parse_mode=parse_mode)
|
|
675
|
+
return await self(call)
|
|
676
|
+
|
|
677
|
+
async def edit_survey(
|
|
678
|
+
self,
|
|
679
|
+
message_id: str,
|
|
680
|
+
title: str,
|
|
681
|
+
survey_campaign_id: str,
|
|
682
|
+
survey_type: SurveyType = SurveyType.NON_ANONYMOUS,
|
|
683
|
+
) -> EditSurveyResponse:
|
|
684
|
+
"""
|
|
685
|
+
Edits a previously sent survey.
|
|
686
|
+
|
|
687
|
+
Source:
|
|
688
|
+
https://trueconf.com/docs/chatbot-connector/en/surveys/#editing-a-poll-message
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
message_id (str): Identifier of the message containing the survey to edit.
|
|
692
|
+
title (str): New title of the survey.
|
|
693
|
+
survey_campaign_id (str): Identifier of the survey campaign.
|
|
694
|
+
survey_type (SurveyType, optional): Type of the survey (anonymous or non-anonymous). Defaults to non-anonymous.
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
EditSurveyResponse: Object containing the result of the survey update.
|
|
698
|
+
"""
|
|
699
|
+
|
|
700
|
+
call = EditSurvey(
|
|
701
|
+
message_id=message_id,
|
|
702
|
+
server=self.server,
|
|
703
|
+
path=survey_campaign_id,
|
|
704
|
+
title=title,
|
|
705
|
+
description=survey_type,
|
|
706
|
+
)
|
|
707
|
+
return await self(call)
|
|
708
|
+
|
|
709
|
+
async def forward_message(
|
|
710
|
+
self, chat_id: str, message_id: str
|
|
711
|
+
) -> ForwardMessageResponse:
|
|
712
|
+
"""
|
|
713
|
+
Forwards a message to the specified chat.
|
|
714
|
+
|
|
715
|
+
Source:
|
|
716
|
+
https://trueconf.com/docs/chatbot-connector/en/messages/#forwarding-a-message-to-another-chat
|
|
717
|
+
|
|
718
|
+
Args:
|
|
719
|
+
chat_id (str): Identifier of the chat to forward the message to.
|
|
720
|
+
message_id (str): Identifier of the message to be forwarded.
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
ForwardMessageResponse: Object containing the result of the message forwarding.
|
|
724
|
+
"""
|
|
725
|
+
|
|
726
|
+
call = ForwardMessage(chat_id=chat_id, message_id=message_id)
|
|
727
|
+
return await self(call)
|
|
728
|
+
|
|
729
|
+
async def get_chats(
|
|
730
|
+
self, count: int = 10, page: int = 1
|
|
731
|
+
) -> GetChatsResponse:
|
|
732
|
+
"""
|
|
733
|
+
Retrieves a paginated list of chats available to the bot.
|
|
734
|
+
|
|
735
|
+
Source:
|
|
736
|
+
https://trueconf.com/docs/chatbot-connector/en/chats/#retrieving-the-list-of-chats
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
count (int, optional): Number of chats per page. Defaults to 10.
|
|
740
|
+
page (int, optional): Page number. Must be greater than 0. Defaults to 1.
|
|
741
|
+
|
|
742
|
+
Returns:
|
|
743
|
+
GetChatsResponse: Object containing the result of the chat list request.
|
|
744
|
+
|
|
745
|
+
Raises:
|
|
746
|
+
ValueError: If the page number is less than 1.
|
|
747
|
+
"""
|
|
748
|
+
|
|
749
|
+
if page < 1:
|
|
750
|
+
raise ValueError("Argument <page> must be greater than 0")
|
|
751
|
+
loggers.chatbot.info(f"✉️ Get info all chats by ")
|
|
752
|
+
call = GetChats(count=count, page=page)
|
|
753
|
+
return await self(call)
|
|
754
|
+
|
|
755
|
+
async def get_chat_by_id(self, chat_id: str) -> GetChatByIdResponse:
|
|
756
|
+
"""
|
|
757
|
+
Retrieves information about a chat by its identifier.
|
|
758
|
+
|
|
759
|
+
Source:
|
|
760
|
+
https://trueconf.com/docs/chatbot-connector/en/chats/#retrieving-chat-information-by-id
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
chat_id (str): Identifier of the chat.
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
GetChatByIDResponse: Object containing information about the chat.
|
|
767
|
+
"""
|
|
768
|
+
|
|
769
|
+
loggers.chatbot.info(f"✉️ Get info chat by {chat_id}")
|
|
770
|
+
call = GetChatByID(chat_id=chat_id)
|
|
771
|
+
return await self(call)
|
|
772
|
+
|
|
773
|
+
async def get_chat_participants(
|
|
774
|
+
self,
|
|
775
|
+
chat_id: str,
|
|
776
|
+
page_size: int,
|
|
777
|
+
page_number: int
|
|
778
|
+
) -> GetChatParticipantsResponse:
|
|
779
|
+
"""
|
|
780
|
+
Retrieves a paginated list of chat participants.
|
|
781
|
+
|
|
782
|
+
Source:
|
|
783
|
+
https://trueconf.com/docs/chatbot-connector/en/chats/#retrieving-the-list-of-chat-participants
|
|
784
|
+
|
|
785
|
+
Args:
|
|
786
|
+
chat_id (str): Identifier of the chat.
|
|
787
|
+
page_size (int): Number of participants per page.
|
|
788
|
+
page_number (int): Page number.
|
|
789
|
+
|
|
790
|
+
Returns:
|
|
791
|
+
GetChatParticipantsResponse: Object containing the result of the participant list request.
|
|
792
|
+
"""
|
|
793
|
+
|
|
794
|
+
call = GetChatParticipants(
|
|
795
|
+
chat_id=chat_id, page_size=page_size, page_number=page_number
|
|
796
|
+
)
|
|
797
|
+
return await self(call)
|
|
798
|
+
|
|
799
|
+
async def get_chat_history(
|
|
800
|
+
self,
|
|
801
|
+
chat_id: str,
|
|
802
|
+
count: int,
|
|
803
|
+
from_message_id: str | None = None,
|
|
804
|
+
) -> GetChatHistoryResponse:
|
|
805
|
+
"""
|
|
806
|
+
Retrieves the message history of the specified chat.
|
|
807
|
+
|
|
808
|
+
Source:
|
|
809
|
+
https://trueconf.com/docs/chatbot-connector/en/messages/#retrieving-chat-history
|
|
810
|
+
|
|
811
|
+
Args:
|
|
812
|
+
chat_id (str): Identifier of the chat.
|
|
813
|
+
count (int): Number of messages to retrieve.
|
|
814
|
+
from_message_id (str | None, optional): Identifier of the message to start retrieving history from.
|
|
815
|
+
If not specified, the history will be loaded from the most recent message.
|
|
816
|
+
|
|
817
|
+
Returns:
|
|
818
|
+
GetChatHistoryResponse: Object containing the result of the chat history request.
|
|
819
|
+
|
|
820
|
+
Raises:
|
|
821
|
+
ValueError: If the count number is less than 1.
|
|
822
|
+
"""
|
|
823
|
+
|
|
824
|
+
if count < 1:
|
|
825
|
+
raise ValueError("Argument <count> must be greater than 0")
|
|
826
|
+
|
|
827
|
+
call = GetChatHistory(
|
|
828
|
+
chat_id=chat_id, count=count, from_message_id=from_message_id
|
|
829
|
+
)
|
|
830
|
+
return await self(call)
|
|
831
|
+
|
|
832
|
+
async def get_file_info(self, file_id: str) -> GetFileInfoResponse:
|
|
833
|
+
"""
|
|
834
|
+
Retrieves information about a file by its identifier.
|
|
835
|
+
|
|
836
|
+
Source:
|
|
837
|
+
https://trueconf.com/docs/chatbot-connector/en/files/#retrieving-file-information-and-downloading-the-file
|
|
838
|
+
|
|
839
|
+
Args:
|
|
840
|
+
file_id (str): Identifier of the file.
|
|
841
|
+
|
|
842
|
+
Returns:
|
|
843
|
+
GetFileInfoResponse: Object containing information about the file.
|
|
844
|
+
"""
|
|
845
|
+
|
|
846
|
+
call = GetFileInfo(file_id=file_id)
|
|
847
|
+
return await self(call)
|
|
848
|
+
|
|
849
|
+
async def get_message_by_id(
|
|
850
|
+
self, message_id: str
|
|
851
|
+
) -> GetMessageByIdResponse:
|
|
852
|
+
"""
|
|
853
|
+
Retrieves a message by its identifier.
|
|
854
|
+
|
|
855
|
+
Source:
|
|
856
|
+
https://trueconf.com/docs/chatbot-connector/en/messages/#retrieving-a-message-by-its-id
|
|
857
|
+
|
|
858
|
+
Args:
|
|
859
|
+
message_id (str): Identifier of the message to retrieve.
|
|
860
|
+
|
|
861
|
+
Returns:
|
|
862
|
+
GetMessageByIdResponse: Object containing the retrieved message data.
|
|
863
|
+
"""
|
|
864
|
+
|
|
865
|
+
call = GetMessageById(message_id=message_id)
|
|
866
|
+
return await self(call)
|
|
867
|
+
|
|
868
|
+
async def get_user_display_name(
|
|
869
|
+
self, user_id: str
|
|
870
|
+
) -> GetUserDisplayNameResponse:
|
|
871
|
+
"""
|
|
872
|
+
Retrieves the display name of a user by their TrueConf ID.
|
|
873
|
+
|
|
874
|
+
Source:
|
|
875
|
+
https://trueconf.com/docs/chatbot-connector/en/contacts/#retrieving-the-display-name-of-a-user-by-their-trueconf-id
|
|
876
|
+
|
|
877
|
+
Args:
|
|
878
|
+
user_id (str): User's TrueConf ID. Can be specified with or without a domain.
|
|
879
|
+
|
|
880
|
+
Returns:
|
|
881
|
+
GetUserDisplayNameResponse: Object containing the user's display name.
|
|
882
|
+
"""
|
|
883
|
+
|
|
884
|
+
if "@" not in user_id:
|
|
885
|
+
user_id = f"{user_id}@{self.server_name}"
|
|
886
|
+
|
|
887
|
+
call = GetUserDisplayName(user_id=user_id)
|
|
888
|
+
return await self(call)
|
|
889
|
+
|
|
890
|
+
async def has_chat_participant(
|
|
891
|
+
self,
|
|
892
|
+
chat_id: str,
|
|
893
|
+
user_id: str
|
|
894
|
+
) -> HasChatParticipantResponse:
|
|
895
|
+
"""
|
|
896
|
+
Checks whether the specified user is a participant in the chat.
|
|
897
|
+
|
|
898
|
+
Source:
|
|
899
|
+
https://trueconf.com/docs/chatbot-connector/en/chats/#checking-participant-presence-in-chat
|
|
900
|
+
|
|
901
|
+
Args:
|
|
902
|
+
chat_id (str): Identifier of the chat.
|
|
903
|
+
user_id (str): Identifier of the user. Can be with or without a domain.
|
|
904
|
+
|
|
905
|
+
Returns:
|
|
906
|
+
HasChatParticipantResponse: Object containing the result of the check.
|
|
907
|
+
"""
|
|
908
|
+
|
|
909
|
+
if "@" not in user_id:
|
|
910
|
+
user_id = f"{user_id}@{self.server_name}"
|
|
911
|
+
|
|
912
|
+
call = HasChatParticipant(chat_id=chat_id, user_id=user_id)
|
|
913
|
+
return await self(call)
|
|
914
|
+
|
|
915
|
+
async def remove_message(
|
|
916
|
+
self, message_id: str, for_all: bool = False
|
|
917
|
+
) -> RemoveMessageResponse:
|
|
918
|
+
"""
|
|
919
|
+
Removes a message by its identifier.
|
|
920
|
+
|
|
921
|
+
Source:
|
|
922
|
+
https://trueconf.com/docs/chatbot-connector/en/messages/#deleting-a-message
|
|
923
|
+
|
|
924
|
+
Args:
|
|
925
|
+
message_id (str): Identifier of the message to be removed.
|
|
926
|
+
for_all (bool, optional): If True, the message will be removed for all participants.
|
|
927
|
+
Default to False (the message is removed only for the bot).
|
|
928
|
+
|
|
929
|
+
Returns:
|
|
930
|
+
RemoveMessageResponse: Object containing the result of the message deletion.
|
|
931
|
+
"""
|
|
932
|
+
|
|
933
|
+
call = RemoveMessage(message_id=message_id, for_all=for_all)
|
|
934
|
+
return await self(call)
|
|
935
|
+
|
|
936
|
+
async def remove_participant_from_chat(
|
|
937
|
+
self, chat_id: str, user_id: str
|
|
938
|
+
) -> RemoveChatParticipantResponse:
|
|
939
|
+
"""
|
|
940
|
+
Removes a participant from the specified chat.
|
|
941
|
+
|
|
942
|
+
Source:
|
|
943
|
+
https://trueconf.com/docs/chatbot-connector/en/chats/#removing-a-participant-from-the-chat
|
|
944
|
+
|
|
945
|
+
Args:
|
|
946
|
+
chat_id (str): Identifier of the chat to remove the participant from.
|
|
947
|
+
user_id (str): Identifier of the user to be removed.
|
|
948
|
+
|
|
949
|
+
Returns:
|
|
950
|
+
RemoveChatParticipantResponse: Object containing the result of the participant removal.
|
|
951
|
+
"""
|
|
952
|
+
|
|
953
|
+
call = RemoveChatParticipant(chat_id=chat_id, user_id=user_id)
|
|
954
|
+
return await self(call)
|
|
955
|
+
|
|
956
|
+
async def reply_message(
|
|
957
|
+
self,
|
|
958
|
+
chat_id: str,
|
|
959
|
+
message_id: str,
|
|
960
|
+
text: str,
|
|
961
|
+
parse_mode: ParseMode | str = ParseMode.TEXT,
|
|
962
|
+
) -> SendMessageResponse:
|
|
963
|
+
"""
|
|
964
|
+
Sends a reply to an existing message in the chat.
|
|
965
|
+
|
|
966
|
+
Source:
|
|
967
|
+
https://trueconf.com/docs/chatbot-connector/en/messages/#reply-to-an-existing-message
|
|
968
|
+
|
|
969
|
+
Args:
|
|
970
|
+
chat_id (str): Identifier of the chat where the reply will be sent.
|
|
971
|
+
message_id (str): Identifier of the message to reply to.
|
|
972
|
+
text (str): Text content of the reply.
|
|
973
|
+
parse_mode (ParseMode | str, optional): Text formatting mode.
|
|
974
|
+
Defaults to plain text.
|
|
975
|
+
|
|
976
|
+
Returns:
|
|
977
|
+
SendMessageResponse: Object containing the result of the message delivery.
|
|
978
|
+
"""
|
|
979
|
+
|
|
980
|
+
call = SendMessage(
|
|
981
|
+
chat_id=chat_id,
|
|
982
|
+
reply_message_id=message_id,
|
|
983
|
+
text=text,
|
|
984
|
+
parse_mode=parse_mode,
|
|
985
|
+
)
|
|
986
|
+
return await self(call)
|
|
987
|
+
|
|
988
|
+
async def run(self, handle_signals: bool = True) -> None:
|
|
989
|
+
"""
|
|
990
|
+
Runs the bot and waits until it stops. Supports handling termination signals (SIGINT, SIGTERM).
|
|
991
|
+
|
|
992
|
+
Args:
|
|
993
|
+
handle_signals (bool, optional): Whether to handle termination signals. Defaults to True.
|
|
994
|
+
|
|
995
|
+
Returns:
|
|
996
|
+
None
|
|
997
|
+
"""
|
|
998
|
+
|
|
999
|
+
if handle_signals:
|
|
1000
|
+
loop = asyncio.get_running_loop()
|
|
1001
|
+
try:
|
|
1002
|
+
loop.add_signal_handler(
|
|
1003
|
+
signal.SIGINT, lambda: asyncio.create_task(self.shutdown())
|
|
1004
|
+
)
|
|
1005
|
+
loop.add_signal_handler(
|
|
1006
|
+
signal.SIGTERM, lambda: asyncio.create_task(self.shutdown())
|
|
1007
|
+
)
|
|
1008
|
+
except NotImplementedError:
|
|
1009
|
+
pass
|
|
1010
|
+
|
|
1011
|
+
await self.start()
|
|
1012
|
+
await self.connected_event.wait()
|
|
1013
|
+
await self.authorized_event.wait()
|
|
1014
|
+
await self.stopped_event.wait()
|
|
1015
|
+
|
|
1016
|
+
async def send_document(self, chat_id: str, file_path: str) -> SendFileResponse:
|
|
1017
|
+
"""
|
|
1018
|
+
Sends a document to the specified chat.
|
|
1019
|
+
|
|
1020
|
+
Files of any format are supported. A preview is automatically generated for the following file types:
|
|
1021
|
+
.jpg, .jpeg, .png, .webp, .bmp, .gif, .tiff, .pdf
|
|
1022
|
+
|
|
1023
|
+
Source:
|
|
1024
|
+
https://trueconf.com/docs/chatbot-connector/en/files/#file-transfer
|
|
1025
|
+
|
|
1026
|
+
Args:
|
|
1027
|
+
chat_id (str): Identifier of the chat to send the document to.
|
|
1028
|
+
file_path (str): Path to the document file.
|
|
1029
|
+
|
|
1030
|
+
Returns:
|
|
1031
|
+
SendFileResponse: Object containing the result of the file upload.
|
|
1032
|
+
"""
|
|
1033
|
+
|
|
1034
|
+
loggers.chatbot.info(f"✉️ Sending file to {chat_id}")
|
|
1035
|
+
|
|
1036
|
+
temporal_file_id = await self.__upload_file_to_server(
|
|
1037
|
+
file_path=file_path,
|
|
1038
|
+
verify=self.verify_ssl,
|
|
1039
|
+
)
|
|
1040
|
+
|
|
1041
|
+
call = SendFile(chat_id=chat_id, temporal_file_id=temporal_file_id)
|
|
1042
|
+
return await self(call)
|
|
1043
|
+
|
|
1044
|
+
async def send_message(
|
|
1045
|
+
self,
|
|
1046
|
+
chat_id: str,
|
|
1047
|
+
text: str,
|
|
1048
|
+
parse_mode: ParseMode | str = ParseMode.TEXT
|
|
1049
|
+
) -> SendMessageResponse:
|
|
1050
|
+
"""
|
|
1051
|
+
Sends a message to the specified chat.
|
|
1052
|
+
|
|
1053
|
+
Source:
|
|
1054
|
+
https://trueconf.com/docs/chatbot-connector/en/messages/#sending-a-text-message-in-chat
|
|
1055
|
+
|
|
1056
|
+
Args:
|
|
1057
|
+
chat_id (str): Identifier of the chat to send the message to.
|
|
1058
|
+
text (str): Text content of the message.
|
|
1059
|
+
parse_mode (ParseMode | str, optional): Text formatting mode.
|
|
1060
|
+
Defaults to plain text.
|
|
1061
|
+
|
|
1062
|
+
Returns:
|
|
1063
|
+
SendMessageResponse: Object containing the result of the message delivery.
|
|
1064
|
+
"""
|
|
1065
|
+
|
|
1066
|
+
loggers.chatbot.info(f"✉️ Sending message to {chat_id}")
|
|
1067
|
+
call = SendMessage(chat_id=chat_id, text=text, parse_mode=parse_mode)
|
|
1068
|
+
return await self(call)
|
|
1069
|
+
|
|
1070
|
+
async def send_photo(self, chat_id: str, file_path: str, preview_path: str | None) -> SendFileResponse:
|
|
1071
|
+
"""
|
|
1072
|
+
Sends a photo to the specified chat with preview (optional).
|
|
1073
|
+
|
|
1074
|
+
Supported image formats: .jpg, .jpeg, .png, .webp, .bmp, .gif, .tiff
|
|
1075
|
+
|
|
1076
|
+
Source:
|
|
1077
|
+
https://trueconf.com/docs/chatbot-connector/en/files/#file-transfer
|
|
1078
|
+
|
|
1079
|
+
Args:
|
|
1080
|
+
chat_id (str): Identifier of the chat to send the photo to.
|
|
1081
|
+
file_path (str): Path to the image file.
|
|
1082
|
+
preview_path (str | None): Path to the preview image.
|
|
1083
|
+
|
|
1084
|
+
Returns:
|
|
1085
|
+
SendFileResponse: Object containing the result of the file upload.
|
|
1086
|
+
|
|
1087
|
+
Examples:
|
|
1088
|
+
>>> bot.send_photo(chat_id="a1s2d3f4f5g6", file_path="/path/to/image.jpg", preview_path="/path/to/preview.webp")
|
|
1089
|
+
"""
|
|
1090
|
+
|
|
1091
|
+
loggers.chatbot.info(f"✉️ Sending photo to {chat_id}")
|
|
1092
|
+
|
|
1093
|
+
temporal_file_id = await self.__upload_file_to_server(
|
|
1094
|
+
file_path=file_path,
|
|
1095
|
+
preview_path=preview_path,
|
|
1096
|
+
verify=self.verify_ssl,
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
call = SendFile(chat_id=chat_id, temporal_file_id=temporal_file_id)
|
|
1100
|
+
return await self(call)
|
|
1101
|
+
|
|
1102
|
+
async def send_sticker(
|
|
1103
|
+
self, chat_id: str, file_path: str
|
|
1104
|
+
) -> SendFileResponse:
|
|
1105
|
+
"""
|
|
1106
|
+
Sends a WebP-format sticker to the specified chat.
|
|
1107
|
+
|
|
1108
|
+
Source:
|
|
1109
|
+
https://trueconf.com/docs/chatbot-connector/en/files/#file-transfer
|
|
1110
|
+
|
|
1111
|
+
Args:
|
|
1112
|
+
chat_id (str): Identifier of the chat to send the sticker to.
|
|
1113
|
+
file_path (str): Path to the sticker file in WebP format.
|
|
1114
|
+
|
|
1115
|
+
Returns:
|
|
1116
|
+
SendFileResponse: Object containing the result of the file upload.
|
|
1117
|
+
|
|
1118
|
+
Raises:
|
|
1119
|
+
TypeError: If the file does not have the MIME type 'image/webp'.
|
|
1120
|
+
"""
|
|
1121
|
+
|
|
1122
|
+
if mimetypes.guess_type(file_path)[0] != "image/webp":
|
|
1123
|
+
raise TypeError("File type not supported. File type must be 'image/webp'")
|
|
1124
|
+
|
|
1125
|
+
loggers.chatbot.info(f"✉️ Sending file to {chat_id}")
|
|
1126
|
+
|
|
1127
|
+
temporal_file_id = await self.__upload_file_to_server(
|
|
1128
|
+
file_path=file_path, preview_mimetype="sticker/webp", verify=self.verify_ssl
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
call = SendFile(chat_id=chat_id, temporal_file_id=temporal_file_id)
|
|
1132
|
+
return await self(call)
|
|
1133
|
+
|
|
1134
|
+
async def send_survey(
|
|
1135
|
+
self,
|
|
1136
|
+
chat_id: str,
|
|
1137
|
+
title: str,
|
|
1138
|
+
survey_campaign_id: str,
|
|
1139
|
+
reply_message_id: str = None,
|
|
1140
|
+
survey_type: SurveyType = SurveyType.NON_ANONYMOUS,
|
|
1141
|
+
) -> SendSurveyResponse:
|
|
1142
|
+
"""
|
|
1143
|
+
Sends a survey to the specified chat.
|
|
1144
|
+
|
|
1145
|
+
Source:
|
|
1146
|
+
https://trueconf.com/docs/chatbot-connector/en/surveys/#sending-a-poll-message-in-chat
|
|
1147
|
+
|
|
1148
|
+
Args:
|
|
1149
|
+
chat_id (str): Identifier of the chat to send the survey to.
|
|
1150
|
+
title (str): Title of the survey displayed in the chat.
|
|
1151
|
+
survey_campaign_id (str): Identifier of the survey campaign.
|
|
1152
|
+
reply_message_id (str, optional): Identifier of the message being replied to.
|
|
1153
|
+
survey_type (SurveyType, optional): Type of the survey (anonymous or non-anonymous). Defaults to non-anonymous.
|
|
1154
|
+
|
|
1155
|
+
Returns:
|
|
1156
|
+
SendSurveyResponse: Object containing the result of the survey submission.
|
|
1157
|
+
"""
|
|
1158
|
+
|
|
1159
|
+
secret = await generate_secret_for_survey(title=title)
|
|
1160
|
+
|
|
1161
|
+
call = SendSurvey(
|
|
1162
|
+
chat_id=chat_id,
|
|
1163
|
+
server=self.server,
|
|
1164
|
+
reply_message_id=reply_message_id,
|
|
1165
|
+
path=survey_campaign_id,
|
|
1166
|
+
title=title,
|
|
1167
|
+
description=survey_type,
|
|
1168
|
+
secret=secret,
|
|
1169
|
+
)
|
|
1170
|
+
return await self(call)
|
|
1171
|
+
|
|
1172
|
+
async def start(self) -> None:
|
|
1173
|
+
"""
|
|
1174
|
+
Starts the bot by connecting to the server and listening for incoming events.
|
|
1175
|
+
|
|
1176
|
+
Note:
|
|
1177
|
+
This method is safe to call multiple times — subsequent calls are ignored
|
|
1178
|
+
if the connection is already active.
|
|
1179
|
+
|
|
1180
|
+
Returns:
|
|
1181
|
+
None
|
|
1182
|
+
"""
|
|
1183
|
+
|
|
1184
|
+
if self._connect_task and not self._connect_task.done():
|
|
1185
|
+
return
|
|
1186
|
+
self._stop = False
|
|
1187
|
+
self._connect_task = asyncio.create_task(self.__connect_and_listen())
|
|
1188
|
+
|
|
1189
|
+
async def shutdown(self) -> None:
|
|
1190
|
+
"""
|
|
1191
|
+
Gracefully shuts down the bot, cancels the connection task, and closes active sessions.
|
|
1192
|
+
|
|
1193
|
+
This method:
|
|
1194
|
+
- Cancels the connection task if it is still active;
|
|
1195
|
+
- Closes the WebSocket session or `self.session` if they are open;
|
|
1196
|
+
- Clears the connection and authorization events;
|
|
1197
|
+
- Sets the `stopped_event` flag.
|
|
1198
|
+
|
|
1199
|
+
Returns:
|
|
1200
|
+
None
|
|
1201
|
+
"""
|
|
1202
|
+
|
|
1203
|
+
self._stop = True
|
|
1204
|
+
if self._connect_task and not self._connect_task.done():
|
|
1205
|
+
self._connect_task.cancel()
|
|
1206
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
1207
|
+
await self._connect_task
|
|
1208
|
+
self._connect_task = None
|
|
1209
|
+
|
|
1210
|
+
try:
|
|
1211
|
+
if self._session:
|
|
1212
|
+
with contextlib.suppress(Exception):
|
|
1213
|
+
await self._session.close()
|
|
1214
|
+
elif self._ws:
|
|
1215
|
+
with contextlib.suppress(Exception):
|
|
1216
|
+
await self._ws.close()
|
|
1217
|
+
finally:
|
|
1218
|
+
self._ws = None
|
|
1219
|
+
self.connected_event.clear()
|
|
1220
|
+
self.authorized_event.clear()
|
|
1221
|
+
loggers.chatbot.info("🛑 ChatBot stopped")
|
|
1222
|
+
self.stopped_event.set()
|
|
1223
|
+
# sys.exit()
|
|
1224
|
+
|
|
1225
|
+
async def subscribe_file_progress(
|
|
1226
|
+
self, file_id: str
|
|
1227
|
+
) -> SubscribeFileProgressResponse:
|
|
1228
|
+
"""
|
|
1229
|
+
Subscribes to file transfer progress updates.
|
|
1230
|
+
|
|
1231
|
+
Source:
|
|
1232
|
+
https://trueconf.com/docs/chatbot-connector/en/files/#subscription-to-file-upload-progress-on-the-server
|
|
1233
|
+
|
|
1234
|
+
Args:
|
|
1235
|
+
file_id (str): Identifier of the file.
|
|
1236
|
+
|
|
1237
|
+
Returns:
|
|
1238
|
+
SubscribeFileProgressResponse: Object containing the result of the subscription.
|
|
1239
|
+
|
|
1240
|
+
Note:
|
|
1241
|
+
If the file is in the UPLOADING status, you can subscribe to the upload process
|
|
1242
|
+
to be notified when the file becomes available.
|
|
1243
|
+
"""
|
|
1244
|
+
|
|
1245
|
+
call = SubscribeFileProgress(file_id=file_id)
|
|
1246
|
+
return await self(call)
|
|
1247
|
+
|
|
1248
|
+
async def unsubscribe_file_progress(
|
|
1249
|
+
self, file_id: str
|
|
1250
|
+
) -> UnsubscribeFileProgressResponse:
|
|
1251
|
+
"""
|
|
1252
|
+
Unsubscribes from receiving file upload progress events.
|
|
1253
|
+
|
|
1254
|
+
Source:
|
|
1255
|
+
https://trueconf.com/docs/chatbot-connector/en/files/#unsubscribe-from-receiving-upload-event-notifications
|
|
1256
|
+
|
|
1257
|
+
Args:
|
|
1258
|
+
file_id (str): Identifier of the file.
|
|
1259
|
+
|
|
1260
|
+
Returns:
|
|
1261
|
+
UnsubscribeFileProgressResponse: Object containing the result of the unsubscription.
|
|
1262
|
+
|
|
1263
|
+
Note:
|
|
1264
|
+
If necessary, you can unsubscribe from file upload events that were previously subscribed to
|
|
1265
|
+
using the `subscribe_file_progress()` method.
|
|
1266
|
+
"""
|
|
1267
|
+
|
|
1268
|
+
call = UnsubscribeFileProgress(file_id=file_id)
|
|
1269
|
+
return await self(call)
|