maxbot-api-client-python 1.1.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.
@@ -0,0 +1,445 @@
1
+ from maxbot_api_client_python.client import Client, decode, adecode
2
+ from maxbot_api_client_python.types.constants import Paths
3
+ from maxbot_api_client_python.types import models
4
+
5
+ class Chats:
6
+ def __init__(self, client: Client):
7
+ self.client = client
8
+
9
+ def GetChats(self, **kwargs) -> models.GetChatsResp:
10
+ """
11
+ Returns information about chats that the bot participated in: a result list and a marker pointing to the next page.
12
+
13
+ Example:
14
+ response = api.chats.GetChats(
15
+ count=20
16
+ )
17
+ """
18
+ req = models.GetChatsReq(**kwargs)
19
+ return decode(self.client, "GET", Paths.CHATS, models.GetChatsResp, query=req.model_dump(exclude_none=True))
20
+
21
+ def GetChat(self, **kwargs) -> models.ChatInfo:
22
+ """
23
+ Returns info about a specific chat.
24
+
25
+ Example:
26
+ response = api.chats.GetChat(
27
+ chat_id=123456789
28
+ )
29
+ """
30
+ req = models.GetChatReq(**kwargs)
31
+ path = Paths.CHATS_ID.format(req.chat_id)
32
+ return decode(self.client, "GET", path, models.ChatInfo)
33
+
34
+ def EditChat(self, **kwargs) -> models.ChatInfo:
35
+ """
36
+ Modifies the properties of a chat, such as its title, icon, or notification settings.
37
+
38
+ Example:
39
+ response = api.chats.EditChat(
40
+ chat_id=123456789,
41
+ title="Updated Chat Title",
42
+ notify=True
43
+ )
44
+ """
45
+ req = models.EditChatReq(**kwargs)
46
+ path = Paths.CHATS_ID.format(req.chat_id)
47
+ return decode(self.client, "PATCH", path, models.ChatInfo, payload=req)
48
+
49
+ def DeleteChat(self, **kwargs) -> models.SimpleQueryResult:
50
+ """
51
+ Permanently deletes a chat for the bot.
52
+
53
+ Example:
54
+ response = api.chats.DeleteChat(
55
+ chat_id=123456789
56
+ )
57
+ """
58
+ req = models.DeleteChatReq(**kwargs)
59
+ path = Paths.CHATS_ID.format(req.chat_id)
60
+ return decode(self.client, "DELETE", path, models.SimpleQueryResult)
61
+
62
+ def SendAction(self, **kwargs) -> models.SimpleQueryResult:
63
+ """
64
+ Broadcasts a temporary status action (e.g., "typing...", "recording video...") to the chat participants.
65
+
66
+ Example:
67
+ response = api.chats.SendAction(
68
+ chat_id=123456789,
69
+ action="mark_seen"
70
+ )
71
+ """
72
+ req = models.SendActionReq(**kwargs)
73
+ path = Paths.CHATS_ACTIONS.format(req.chat_id)
74
+ return decode(self.client, "POST", path, models.SimpleQueryResult, payload=req)
75
+
76
+ def GetPinnedMessage(self, **kwargs) -> models.Message:
77
+ """
78
+ Retrieves the currently pinned message in the specified chat.
79
+
80
+ Example:
81
+ response = api.chats.GetPinnedMessage(
82
+ chat_id=123456789
83
+ )
84
+ """
85
+ req = models.GetPinnedMessageReq(**kwargs)
86
+ path = Paths.CHATS_PIN.format(req.chat_id)
87
+ return decode(self.client, "GET", path, models.Message)
88
+
89
+ def PinMessage(self, **kwargs) -> models.SimpleQueryResult:
90
+ """
91
+ Pins a specific message in the chat.
92
+ You can optionally specify whether to notify chat members about the new pinned message.
93
+
94
+ Example:
95
+ response = api.chats.PinMessage(
96
+ chat_id=123456789,
97
+ message_id="mid:987654321...",
98
+ notify=True
99
+ )
100
+ """
101
+ req = models.PinMessageReq(**kwargs)
102
+ path = Paths.CHATS_PIN.format(req.chat_id)
103
+ return decode(self.client, "PUT", path, models.SimpleQueryResult, payload=req)
104
+
105
+ def UnpinMessage(self, **kwargs) -> models.SimpleQueryResult:
106
+ """
107
+ Removes the pinned message from the specified chat.
108
+
109
+ Example:
110
+ response = api.chats.UnpinMessage(
111
+ chat_id=123456789
112
+ )
113
+ """
114
+ req = models.UnpinMessageReq(**kwargs)
115
+ path = Paths.CHATS_PIN.format(req.chat_id)
116
+ return decode(self.client, "DELETE", path, models.SimpleQueryResult)
117
+
118
+ def GetChatMembership(self, **kwargs) -> models.ChatMember:
119
+ """
120
+ Returns chat membership info for the current bot.
121
+
122
+ Example:
123
+ response = api.chats.GetChatMembership(
124
+ chat_id=123456789
125
+ )
126
+ """
127
+ req = models.GetChatMembershipReq(**kwargs)
128
+ path = Paths.CHATS_MEMBERS_ME.format(req.chat_id)
129
+ return decode(self.client, "GET", path, models.ChatMember)
130
+
131
+ def LeaveChat(self, **kwargs) -> models.SimpleQueryResult:
132
+ """
133
+ Removes the bot from chat members.
134
+
135
+ Example:
136
+ response = api.chats.LeaveChat(
137
+ chat_id=123456789
138
+ )
139
+ """
140
+ req = models.LeaveChatReq(**kwargs)
141
+ path = Paths.CHATS_MEMBERS_ME.format(req.chat_id)
142
+ return decode(self.client, "DELETE", path, models.SimpleQueryResult)
143
+
144
+ def GetChatAdmins(self, **kwargs) -> models.GetChatAdminsResp:
145
+ """
146
+ Retrieves a list of administrators for the specified group chat.
147
+
148
+ Example:
149
+ response = api.chats.GetChatAdmins(
150
+ chat_id=123456789
151
+ )
152
+ """
153
+ req = models.GetChatAdminsReq(**kwargs)
154
+ path = Paths.CHATS_MEMBERS_ADMIN.format(req.chat_id)
155
+ return decode(self.client, "GET", path, models.GetChatAdminsResp)
156
+
157
+ def SetChatAdmins(self, **kwargs) -> models.SimpleQueryResult:
158
+ """
159
+ Assigns administrator rights to specific users in a group chat.
160
+
161
+ Example:
162
+ response = api.chats.SetChatAdmins(
163
+ chat_id=123456789,
164
+ admins=[
165
+ ChatAdmin(user_id=98765, role="admin"),
166
+ ChatAdmin(user_id=43210, role="admin")
167
+ ]
168
+ )
169
+ """
170
+ req = models.SetChatAdminsReq(**kwargs)
171
+ path = Paths.CHATS_MEMBERS_ADMIN.format(req.chat_id)
172
+ return decode(self.client, "POST", path, models.SimpleQueryResult, payload=req)
173
+
174
+ def DeleteAdmin(self, **kwargs) -> models.SimpleQueryResult:
175
+ """
176
+ Revokes administrator rights from a specific user in a group chat.
177
+
178
+ Example:
179
+ response = api.chats.DeleteAdmin(
180
+ chat_id=123456789,
181
+ user_id=98765
182
+ )
183
+ """
184
+ req = models.DeleteAdminReq(**kwargs)
185
+ path = Paths.CHATS_MEMBERS_ADMIN_ID.format(req.chat_id, req.user_id)
186
+ return decode(self.client, "DELETE", path, models.SimpleQueryResult)
187
+
188
+ def GetChatMembers(self, **kwargs) -> models.GetChatAdminsResp:
189
+ """
190
+ Returns users participated in the chat.
191
+
192
+ Example:
193
+ response = api.chats.GetChatMembers(
194
+ chat_id=123456789,
195
+ count=20
196
+ )
197
+ """
198
+ req = models.GetChatMembersReq(**kwargs)
199
+ path = Paths.CHATS_MEMBERS.format(req.chat_id)
200
+ return decode(self.client, "GET", path, models.GetChatAdminsResp, query=req.model_dump(exclude_none=True))
201
+
202
+ def AddMembers(self, **kwargs) -> models.AddMembersResp:
203
+ """
204
+ Adds one or more users to a group chat.
205
+
206
+ Example:
207
+ response = api.chats.AddMembers(
208
+ chat_id=123456789,
209
+ user_ids=[11111, 22222]
210
+ )
211
+ """
212
+ req = models.AddMembersReq(**kwargs)
213
+ path = Paths.CHATS_MEMBERS.format(req.chat_id)
214
+ return decode(self.client, "POST", path, models.AddMembersResp, payload=req)
215
+
216
+ def DeleteMember(self, **kwargs) -> models.SimpleQueryResult:
217
+ """
218
+ Removes a specific user from a group chat.
219
+ You can optionally block the user from rejoining.
220
+
221
+ Example:
222
+ response = api.chats.DeleteMember(
223
+ chat_id=123456789,
224
+ user_id=98765,
225
+ block=True
226
+ )
227
+ """
228
+ req = models.DeleteMemberReq(**kwargs)
229
+ path = Paths.CHATS_MEMBERS.format(req.chat_id)
230
+ return decode(self.client, "DELETE", path, models.SimpleQueryResult, query=req.model_dump(exclude_none=True))
231
+
232
+ async def GetChatsAsync(self, **kwargs) -> models.GetChatsResp:
233
+ """
234
+ Async version of GetChats.
235
+
236
+ Example:
237
+ response = await api.chats.GetChatsAsync(
238
+ count=20
239
+ )
240
+ """
241
+ req = models.GetChatsReq(**kwargs)
242
+ return await adecode(self.client, "GET", Paths.CHATS, models.GetChatsResp, query=req.model_dump(exclude_none=True))
243
+
244
+ async def GetChatAsync(self, **kwargs) -> models.ChatInfo:
245
+ """
246
+ Async version of GetChat.
247
+
248
+ Example:
249
+ response = await api.chats.GetChatAsync(
250
+ chat_id=123456789
251
+ )
252
+ """
253
+ req = models.GetChatReq(**kwargs)
254
+ path = Paths.CHATS_ID.format(req.chat_id)
255
+ return await adecode(self.client, "GET", path, models.ChatInfo)
256
+
257
+ async def EditChatAsync(self, **kwargs) -> models.ChatInfo:
258
+ """
259
+ Async version of EditChat.
260
+
261
+ Example:
262
+ response = await api.chats.EditChatAsync(
263
+ chat_id=123456789,
264
+ title="Updated Chat Title"
265
+ )
266
+ """
267
+ req = models.EditChatReq(**kwargs)
268
+ path = Paths.CHATS_ID.format(req.chat_id)
269
+ return await adecode(self.client, "PATCH", path, models.ChatInfo, payload=req)
270
+
271
+ async def DeleteChatAsync(self, **kwargs) -> models.SimpleQueryResult:
272
+ """
273
+ Async version of DeleteChat.
274
+
275
+ Example:
276
+ response = await api.chats.DeleteChatAsync(
277
+ chat_id=123456789
278
+ )
279
+ """
280
+ req = models.DeleteChatReq(**kwargs)
281
+ path = Paths.CHATS_ID.format(req.chat_id)
282
+ return await adecode(self.client, "DELETE", path, models.SimpleQueryResult)
283
+
284
+ async def SendActionAsync(self, **kwargs) -> models.SimpleQueryResult:
285
+ """
286
+ Async version of SendAction.
287
+
288
+ Example:
289
+ response = await api.chats.SendActionAsync(
290
+ chat_id=123456789,
291
+ action="typing"
292
+ )
293
+ """
294
+ req = models.SendActionReq(**kwargs)
295
+ path = Paths.CHATS_ACTIONS.format(req.chat_id)
296
+ return await adecode(self.client, "POST", path, models.SimpleQueryResult, payload=req)
297
+
298
+ async def GetPinnedMessageAsync(self, **kwargs) -> models.Message:
299
+ """
300
+ Async version of GetPinnedMessage.
301
+
302
+ Example:
303
+ response = await api.chats.GetPinnedMessageAsync(
304
+ chat_id=123456789
305
+ )
306
+ """
307
+ req = models.GetPinnedMessageReq(**kwargs)
308
+ path = Paths.CHATS_PIN.format(req.chat_id)
309
+ return await adecode(self.client, "GET", path, models.Message)
310
+
311
+ async def PinMessageAsync(self, **kwargs) -> models.SimpleQueryResult:
312
+ """
313
+ Async version of PinMessage.
314
+
315
+ Example:
316
+ response = await api.chats.PinMessageAsync(
317
+ chat_id=123456789,
318
+ message_id="mid:987654321..."
319
+ )
320
+ """
321
+ req = models.PinMessageReq(**kwargs)
322
+ path = Paths.CHATS_PIN.format(req.chat_id)
323
+ return await adecode(self.client, "PUT", path, models.SimpleQueryResult, payload=req)
324
+
325
+ async def UnpinMessageAsync(self, **kwargs) -> models.SimpleQueryResult:
326
+ """
327
+ Async version of UnpinMessage.
328
+
329
+ Example:
330
+ response = await api.chats.UnpinMessageAsync(
331
+ chat_id=123456789
332
+ )
333
+ """
334
+ req = models.UnpinMessageReq(**kwargs)
335
+ path = Paths.CHATS_PIN.format(req.chat_id)
336
+ return await adecode(self.client, "DELETE", path, models.SimpleQueryResult)
337
+
338
+ async def GetChatMembershipAsync(self, **kwargs) -> models.ChatMember:
339
+ """
340
+ Async version of GetChatMembership.
341
+
342
+ Example:
343
+ response = await api.chats.GetChatMembershipAsync(
344
+ chat_id=123456789
345
+ )
346
+ """
347
+ req = models.GetChatMembershipReq(**kwargs)
348
+ path = Paths.CHATS_MEMBERS_ME.format(req.chat_id)
349
+ return await adecode(self.client, "GET", path, models.ChatMember)
350
+
351
+ async def LeaveChatAsync(self, **kwargs) -> models.SimpleQueryResult:
352
+ """
353
+ Async version of LeaveChat.
354
+
355
+ Example:
356
+ response = await api.chats.LeaveChatAsync(
357
+ chat_id=123456789
358
+ )
359
+ """
360
+ req = models.LeaveChatReq(**kwargs)
361
+ path = Paths.CHATS_MEMBERS_ME.format(req.chat_id)
362
+ return await adecode(self.client, "DELETE", path, models.SimpleQueryResult)
363
+
364
+ async def GetChatAdminsAsync(self, **kwargs) -> models.GetChatAdminsResp:
365
+ """
366
+ Async version of GetChatAdmins.
367
+
368
+ Example:
369
+ response = await api.chats.GetChatAdminsAsync(
370
+ chat_id=123456789
371
+ )
372
+ """
373
+ req = models.GetChatAdminsReq(**kwargs)
374
+ path = Paths.CHATS_MEMBERS_ADMIN.format(req.chat_id)
375
+ return await adecode(self.client, "GET", path, models.GetChatAdminsResp)
376
+
377
+ async def SetChatAdminsAsync(self, **kwargs) -> models.SimpleQueryResult:
378
+ """
379
+ Async version of SetChatAdmins.
380
+
381
+ Example:
382
+ response = await api.chats.SetChatAdminsAsync(
383
+ chat_id=123456789,
384
+ admins=[ChatAdmin(user_id=98765, role="admin")]
385
+ )
386
+ """
387
+ req = models.SetChatAdminsReq(**kwargs)
388
+ path = Paths.CHATS_MEMBERS_ADMIN.format(req.chat_id)
389
+ return await adecode(self.client, "POST", path, models.SimpleQueryResult, payload=req)
390
+
391
+ async def DeleteAdminAsync(self, **kwargs) -> models.SimpleQueryResult:
392
+ """
393
+ Async version of DeleteAdmin.
394
+
395
+ Example:
396
+ response = await api.chats.DeleteAdminAsync(
397
+ chat_id=123456789,
398
+ user_id=98765
399
+ )
400
+ """
401
+ req = models.DeleteAdminReq(**kwargs)
402
+ path = Paths.CHATS_MEMBERS_ADMIN_ID.format(req.chat_id, req.user_id)
403
+ return await adecode(self.client, "DELETE", path, models.SimpleQueryResult)
404
+
405
+ async def GetChatMembersAsync(self, **kwargs) -> models.GetChatAdminsResp:
406
+ """
407
+ Async version of GetChatMembers.
408
+
409
+ Example:
410
+ response = await api.chats.GetChatMembersAsync(
411
+ chat_id=123456789,
412
+ count=20
413
+ )
414
+ """
415
+ req = models.GetChatMembersReq(**kwargs)
416
+ path = Paths.CHATS_MEMBERS.format(req.chat_id)
417
+ return await adecode(self.client, "GET", path, models.GetChatAdminsResp, query=req.model_dump(exclude_none=True))
418
+
419
+ async def AddMembersAsync(self, **kwargs) -> models.AddMembersResp:
420
+ """
421
+ Async version of AddMembers.
422
+
423
+ Example:
424
+ response = await api.chats.AddMembersAsync(
425
+ chat_id=123456789,
426
+ user_ids=[11111, 22222]
427
+ )
428
+ """
429
+ req = models.AddMembersReq(**kwargs)
430
+ path = Paths.CHATS_MEMBERS.format(req.chat_id)
431
+ return await adecode(self.client, "POST", path, models.AddMembersResp, payload=req)
432
+
433
+ async def DeleteMemberAsync(self, **kwargs) -> models.SimpleQueryResult:
434
+ """
435
+ Async version of DeleteMember.
436
+
437
+ Example:
438
+ response = await api.chats.DeleteMemberAsync(
439
+ chat_id=123456789,
440
+ user_id=98765
441
+ )
442
+ """
443
+ req = models.DeleteMemberReq(**kwargs)
444
+ path = Paths.CHATS_MEMBERS.format(req.chat_id)
445
+ return await adecode(self.client, "DELETE", path, models.SimpleQueryResult, query=req.model_dump(exclude_none=True))
@@ -0,0 +1,259 @@
1
+ import aiofiles, asyncio, httpx, logging, mimetypes, os, time
2
+ from typing import Any, List, Optional
3
+ from urllib.parse import urlparse
4
+ from pathlib import Path
5
+
6
+ from maxbot_api_client_python.tools.uploads import Uploads
7
+ from maxbot_api_client_python.client import Client, decode, adecode
8
+ from maxbot_api_client_python.types.constants import AttachmentType, Paths, UploadType
9
+ from maxbot_api_client_python.types.models import Message, SendFileReq, SendMessageReq, UploadFileReq, Attachment
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class Helpers:
14
+ def __init__(self, client: Client):
15
+ self.client = client
16
+ self.uploads = Uploads(client)
17
+
18
+ def SendFile(self, **kwargs) -> Optional[Message]:
19
+ """
20
+ A helper that simplifies sending files to a chat.
21
+ It automatically determines whether the provided file_source is a direct URL or a local file path.
22
+
23
+ Example:
24
+ # Sending a file via URL:
25
+ response = api.helpers.SendFile(
26
+ chat_id=123456789,
27
+ text="Check out this image!",
28
+ file_source="https://example.com/image.png"
29
+ )
30
+
31
+ # Sending a local file:
32
+ response = api.helpers.SendFile(
33
+ chat_id=123456789,
34
+ text="Here is the report.",
35
+ file_source="/local/path/to/report.pdf"
36
+ )
37
+ """
38
+ req = SendFileReq(**kwargs)
39
+ if self._is_url(req.file_source):
40
+ return self.sendFileByUrl(req)
41
+ return self.sendFileByUpload(req)
42
+
43
+ def sendFileByUrl(self, req: SendFileReq) -> Optional[Message]:
44
+ ext = self._get_extension(req.file_source)
45
+ upload_type = self._determine_upload_type(ext)
46
+
47
+ if upload_type == UploadType.IMAGE:
48
+ attachment = Attachment(type=AttachmentType.IMAGE, payload={"url": req.file_source})
49
+ return self.sendFileInternal(req, attachment)
50
+
51
+ temp_path = None
52
+ try:
53
+ temp_path = self._download_temp_file(req.file_source)
54
+ req.file_source = temp_path
55
+ return self.sendFileByUpload(req)
56
+ except Exception as e:
57
+ logger.error(f"File processing/upload error: {e}")
58
+ return None
59
+ finally:
60
+ if temp_path and os.path.exists(temp_path):
61
+ os.remove(temp_path)
62
+
63
+ def sendFileByUpload(self, req: SendFileReq) -> Optional[Message]:
64
+ ext = self._get_extension(req.file_source)
65
+ upload_type = self._determine_upload_type(ext)
66
+
67
+ upload_req = UploadFileReq(type=upload_type, file_path=req.file_source)
68
+ upload_resp = self.uploads.UploadFile(**upload_req.model_dump_json(indent=4))
69
+
70
+ if not upload_resp or not upload_resp.token:
71
+ logger.error("Upload failed: No token returned.")
72
+ return None
73
+
74
+ attachment = self._build_attachment_from_token(upload_type, upload_resp.token, Path(req.file_source).name)
75
+ return self.sendFileInternal(req, attachment)
76
+
77
+ def sendFileInternal(self, req: SendFileReq, attachment: Attachment) -> Message:
78
+ attachments: List[Attachment] = req.attachments or []
79
+ attachments.append(attachment)
80
+
81
+ request_data = SendMessageReq(
82
+ user_id=req.user_id,
83
+ chat_id=req.chat_id,
84
+ text=req.text,
85
+ format=req.format,
86
+ attachments=attachments,
87
+ notify=req.notify,
88
+ link=req.link,
89
+ disable_link_preview=req.disable_link_preview
90
+ )
91
+
92
+ last_err: Optional[Exception] = None
93
+ for _ in range(self.client.max_retries):
94
+ try:
95
+ return decode(self.client, "POST", Paths.MESSAGES, Message, query=request_data.model_dump(exclude_none=True), payload=request_data)
96
+ except Exception as e:
97
+ last_err = e
98
+ if "not.ready" in str(e).lower():
99
+ time.sleep(self.client.retry_delay_sec)
100
+ continue
101
+ break
102
+
103
+ if last_err:
104
+ raise last_err
105
+ raise Exception("Unknown error in sendFileInternal")
106
+
107
+ async def SendFileAsync(self, **kwargs) -> Optional[Message]:
108
+ """
109
+ Async version of SendFile.
110
+
111
+ Example:
112
+ # Sending a local file asynchronously:
113
+ response = await api.helpers.SendFileAsync(
114
+ chat_id=123456789,
115
+ text="Here is the report.",
116
+ file_source="/local/path/to/report.pdf"
117
+ )
118
+ """
119
+ req = SendFileReq(**kwargs)
120
+ if self._is_url(req.file_source):
121
+ return await self.sendFileByUrlAsync(req)
122
+ return await self.sendFileByUploadAsync(req)
123
+
124
+ async def sendFileByUrlAsync(self, req: SendFileReq) -> Optional[Message]:
125
+ ext = self._get_extension(req.file_source)
126
+ upload_type = self._determine_upload_type(ext)
127
+
128
+ if upload_type == UploadType.IMAGE:
129
+ attachment = Attachment(type=AttachmentType.IMAGE, payload={"url": req.file_source})
130
+ return await self.sendFileInternalAsync(req, attachment)
131
+
132
+ temp_path = None
133
+ try:
134
+ temp_path = await self._download_temp_file_async(req.file_source)
135
+ req.file_source = temp_path
136
+ return await self.sendFileByUploadAsync(req)
137
+ except Exception as e:
138
+ logger.error(f"Async file processing/upload error: {e}")
139
+ return None
140
+ finally:
141
+ if temp_path and os.path.exists(temp_path):
142
+ await asyncio.to_thread(os.remove, temp_path)
143
+
144
+ async def sendFileByUploadAsync(self, req: SendFileReq) -> Optional[Message]:
145
+ ext = self._get_extension(req.file_source)
146
+ u_type = self._determine_upload_type(ext)
147
+
148
+ upload_req = UploadFileReq(type=u_type, file_path=req.file_source)
149
+ upload_resp = await self.uploads.UploadFileAsync(**upload_req.model_dump_json(indent=4))
150
+
151
+ if not upload_resp or not upload_resp.token:
152
+ return None
153
+
154
+ attachment = self._build_attachment_from_token(u_type, upload_resp.token, Path(req.file_source).name)
155
+ return await self.sendFileInternalAsync(req, attachment)
156
+
157
+ async def sendFileInternalAsync(self, req: SendFileReq, attachment: Attachment) -> Message:
158
+ attachments: List[Attachment] = req.attachments or []
159
+ attachments.append(attachment)
160
+
161
+ request_data = SendMessageReq(
162
+ **req.model_dump(exclude={"file_source", "attachments"}),
163
+ attachments=attachments
164
+ )
165
+
166
+ last_err: Optional[Exception] = None
167
+ for _ in range(self.client.max_retries):
168
+ try:
169
+ return await adecode(
170
+ self.client, "POST", Paths.MESSAGES, Message,
171
+ query=request_data.model_dump(exclude_none=True),
172
+ payload=request_data
173
+ )
174
+ except Exception as e:
175
+ last_err = e
176
+ if "not.ready" in str(e).lower():
177
+ await asyncio.sleep(self.client.retry_delay_sec)
178
+ continue
179
+ break
180
+
181
+ if last_err:
182
+ raise last_err
183
+ raise Exception("Unknown error in sendFileInternalAsync")
184
+
185
+ def _is_url(self, source: str) -> bool:
186
+ try:
187
+ result = urlparse(source)
188
+ return all([result.scheme, result.netloc])
189
+ except ValueError:
190
+ return False
191
+
192
+ def _get_extension(self, source: str) -> str:
193
+ parsed = urlparse(source)
194
+ path = parsed.path if parsed.scheme and parsed.netloc else source
195
+ return Path(path).suffix.lower()
196
+
197
+ def _determine_upload_type(self, ext: str) -> UploadType:
198
+ if ext in {".jpg", ".jpeg", ".png", ".webp"}:
199
+ return UploadType.IMAGE
200
+ if ext in {".mp4", ".avi", ".mov"}:
201
+ return UploadType.VIDEO
202
+ if ext in {".mp3", ".ogg", ".wav"}:
203
+ return UploadType.AUDIO
204
+ return UploadType.FILE
205
+
206
+ def _build_attachment_from_token(self, u_type: UploadType, token: str, filename: str) -> Attachment:
207
+ payload: dict[str, Any] = {"token": token}
208
+ if u_type == UploadType.FILE:
209
+ payload["filename"] = filename
210
+ return Attachment(type=AttachmentType(u_type.value), payload=payload)
211
+
212
+ def _download_temp_file(self, url_str: str) -> str:
213
+ headers = {"User-Agent": "maxbot-client/1.0"}
214
+ with httpx.Client() as client:
215
+ with client.stream("GET", url_str, headers=headers, follow_redirects=True) as resp:
216
+ resp.raise_for_status()
217
+
218
+ content_disp = resp.headers.get("Content-Disposition", "")
219
+ filename = None
220
+ if "filename=" in content_disp:
221
+ filename = content_disp.split("filename=")[1].strip('"')
222
+
223
+ if not filename:
224
+ content_type = resp.headers.get("Content-Type", "")
225
+ ext = mimetypes.guess_extension(content_type.split(";")[0])
226
+ if ext:
227
+ filename = f"file{ext}"
228
+
229
+ if not filename:
230
+ filename = Path(urlparse(url_str).path).name or "temp_file.bin"
231
+
232
+ temp_path = f"temp_{int(time.time())}_{filename}"
233
+
234
+ with open(temp_path, "wb") as f:
235
+ for chunk in resp.iter_bytes(chunk_size=8192):
236
+ f.write(chunk)
237
+ return temp_path
238
+
239
+ async def _download_temp_file_async(self, url_str: str) -> str:
240
+ headers = {"User-Agent": "maxbot-client/1.0"}
241
+ async with httpx.AsyncClient() as client:
242
+ async with client.stream("GET", url_str, headers=headers, follow_redirects=True) as resp:
243
+ resp.raise_for_status()
244
+
245
+ content_type = resp.headers.get("Content-Type", "").split(";")[0]
246
+ extension = mimetypes.guess_extension(content_type) or ".bin"
247
+
248
+ url_path_name = Path(urlparse(url_str).path).name
249
+ if url_path_name == "uc" or not url_path_name:
250
+ filename = f"file{extension}"
251
+ else:
252
+ filename = url_path_name if "." in url_path_name else f"{url_path_name}{extension}"
253
+
254
+ temp_path = f"temp_{int(time.time())}_{filename}"
255
+
256
+ async with aiofiles.open(temp_path, "wb") as f:
257
+ async for chunk in resp.aiter_bytes(chunk_size=8192):
258
+ await f.write(chunk)
259
+ return temp_path