pararamio-aio 2.1.1__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.
- pararamio_aio/__init__.py +78 -0
- pararamio_aio/_core/__init__.py +125 -0
- pararamio_aio/_core/_types.py +120 -0
- pararamio_aio/_core/base.py +143 -0
- pararamio_aio/_core/client_protocol.py +90 -0
- pararamio_aio/_core/constants/__init__.py +7 -0
- pararamio_aio/_core/constants/base.py +9 -0
- pararamio_aio/_core/constants/endpoints.py +84 -0
- pararamio_aio/_core/cookie_decorator.py +208 -0
- pararamio_aio/_core/cookie_manager.py +1222 -0
- pararamio_aio/_core/endpoints.py +67 -0
- pararamio_aio/_core/exceptions/__init__.py +6 -0
- pararamio_aio/_core/exceptions/auth.py +91 -0
- pararamio_aio/_core/exceptions/base.py +124 -0
- pararamio_aio/_core/models/__init__.py +17 -0
- pararamio_aio/_core/models/base.py +66 -0
- pararamio_aio/_core/models/chat.py +92 -0
- pararamio_aio/_core/models/post.py +65 -0
- pararamio_aio/_core/models/user.py +54 -0
- pararamio_aio/_core/py.typed +2 -0
- pararamio_aio/_core/utils/__init__.py +73 -0
- pararamio_aio/_core/utils/async_requests.py +417 -0
- pararamio_aio/_core/utils/auth_flow.py +202 -0
- pararamio_aio/_core/utils/authentication.py +235 -0
- pararamio_aio/_core/utils/captcha.py +92 -0
- pararamio_aio/_core/utils/helpers.py +336 -0
- pararamio_aio/_core/utils/http_client.py +199 -0
- pararamio_aio/_core/utils/requests.py +424 -0
- pararamio_aio/_core/validators.py +78 -0
- pararamio_aio/_types.py +29 -0
- pararamio_aio/client.py +989 -0
- pararamio_aio/constants/__init__.py +16 -0
- pararamio_aio/cookie_manager.py +15 -0
- pararamio_aio/exceptions/__init__.py +31 -0
- pararamio_aio/exceptions/base.py +1 -0
- pararamio_aio/file_operations.py +232 -0
- pararamio_aio/models/__init__.py +32 -0
- pararamio_aio/models/activity.py +127 -0
- pararamio_aio/models/attachment.py +141 -0
- pararamio_aio/models/base.py +83 -0
- pararamio_aio/models/bot.py +274 -0
- pararamio_aio/models/chat.py +722 -0
- pararamio_aio/models/deferred_post.py +174 -0
- pararamio_aio/models/file.py +103 -0
- pararamio_aio/models/group.py +361 -0
- pararamio_aio/models/poll.py +275 -0
- pararamio_aio/models/post.py +643 -0
- pararamio_aio/models/team.py +403 -0
- pararamio_aio/models/user.py +239 -0
- pararamio_aio/py.typed +2 -0
- pararamio_aio/utils/__init__.py +18 -0
- pararamio_aio/utils/authentication.py +383 -0
- pararamio_aio/utils/requests.py +75 -0
- pararamio_aio-2.1.1.dist-info/METADATA +269 -0
- pararamio_aio-2.1.1.dist-info/RECORD +57 -0
- pararamio_aio-2.1.1.dist-info/WHEEL +5 -0
- pararamio_aio-2.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,722 @@
|
|
1
|
+
"""Async Chat model."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import uuid
|
6
|
+
from datetime import datetime
|
7
|
+
from typing import TYPE_CHECKING, Any
|
8
|
+
from urllib.parse import quote_plus
|
9
|
+
|
10
|
+
# Imports from core
|
11
|
+
from pararamio_aio._core import (
|
12
|
+
POSTS_LIMIT,
|
13
|
+
PararamioLimitExceededException,
|
14
|
+
PararamioRequestException,
|
15
|
+
PararamModelNotLoaded,
|
16
|
+
validate_post_load_range,
|
17
|
+
)
|
18
|
+
from pararamio_aio._core.utils.helpers import join_ids
|
19
|
+
|
20
|
+
from .base import BaseModel
|
21
|
+
from .post import Post
|
22
|
+
|
23
|
+
if TYPE_CHECKING:
|
24
|
+
from ..client import AsyncPararamio
|
25
|
+
from .file import File
|
26
|
+
|
27
|
+
__all__ = ("Chat",)
|
28
|
+
|
29
|
+
|
30
|
+
class Chat(BaseModel): # pylint: disable=too-many-public-methods,duplicate-code
|
31
|
+
"""Async Chat model with explicit loading."""
|
32
|
+
|
33
|
+
def __init__(self, client: AsyncPararamio, id: int, title: str | None = None, **kwargs):
|
34
|
+
"""Initialize async chat.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
client: AsyncPararamio client
|
38
|
+
id: Chat ID
|
39
|
+
title: Optional chat title
|
40
|
+
**kwargs: Additional chat data
|
41
|
+
"""
|
42
|
+
# Only pass non-None values to avoid polluting _data
|
43
|
+
init_data: dict[str, Any] = {"id": id}
|
44
|
+
if title is not None:
|
45
|
+
init_data["title"] = title
|
46
|
+
init_data.update(kwargs)
|
47
|
+
|
48
|
+
super().__init__(client, **init_data)
|
49
|
+
self.id = id
|
50
|
+
|
51
|
+
@property
|
52
|
+
def title(self) -> str | None:
|
53
|
+
"""Get chat title."""
|
54
|
+
return self._data.get("title")
|
55
|
+
|
56
|
+
@property
|
57
|
+
def description(self) -> str | None:
|
58
|
+
"""Get chat description."""
|
59
|
+
if not self.is_loaded() and "description" not in self._data:
|
60
|
+
raise PararamModelNotLoaded(
|
61
|
+
"Chat data has not been loaded. Use load() to fetch chat data first."
|
62
|
+
)
|
63
|
+
return self._data.get("description")
|
64
|
+
|
65
|
+
@property
|
66
|
+
def posts_count(self) -> int:
|
67
|
+
"""Get total posts count."""
|
68
|
+
if "posts_count" not in self._data:
|
69
|
+
raise PararamModelNotLoaded(
|
70
|
+
"Chat data has not been loaded. Use load() to fetch chat data first."
|
71
|
+
)
|
72
|
+
return self._data["posts_count"]
|
73
|
+
|
74
|
+
def is_loaded(self) -> bool:
|
75
|
+
"""Check if chat data has been loaded.
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
True if chat data has been loaded, False otherwise
|
79
|
+
"""
|
80
|
+
# Check for essential fields that are only present after loading
|
81
|
+
return "posts_count" in self._data and "time_created" in self._data
|
82
|
+
|
83
|
+
@property
|
84
|
+
def is_private(self) -> bool:
|
85
|
+
"""Check if chat is private message."""
|
86
|
+
return self._data.get("pm", False)
|
87
|
+
|
88
|
+
@property
|
89
|
+
def time_created(self) -> datetime | None:
|
90
|
+
"""Get chat creation time."""
|
91
|
+
if not self.is_loaded() and "time_created" not in self._data:
|
92
|
+
raise PararamModelNotLoaded(
|
93
|
+
"Chat data has not been loaded. Use load() to fetch chat data first."
|
94
|
+
)
|
95
|
+
return self._data.get("time_created")
|
96
|
+
|
97
|
+
@property
|
98
|
+
def time_updated(self) -> datetime | None:
|
99
|
+
"""Get chat last update time."""
|
100
|
+
if not self.is_loaded() and "time_updated" not in self._data:
|
101
|
+
raise PararamModelNotLoaded(
|
102
|
+
"Chat data has not been loaded. Use load() to fetch chat data first."
|
103
|
+
)
|
104
|
+
return self._data.get("time_updated")
|
105
|
+
|
106
|
+
@property
|
107
|
+
def author_id(self) -> int | None:
|
108
|
+
"""Get chat author ID."""
|
109
|
+
if not self.is_loaded() and "author_id" not in self._data:
|
110
|
+
raise PararamModelNotLoaded(
|
111
|
+
"Chat data has not been loaded. Use load() to fetch chat data first."
|
112
|
+
)
|
113
|
+
return self._data.get("author_id")
|
114
|
+
|
115
|
+
@property
|
116
|
+
def organization_id(self) -> int | None:
|
117
|
+
"""Get organization ID."""
|
118
|
+
if not self.is_loaded() and "organization_id" not in self._data:
|
119
|
+
raise PararamModelNotLoaded(
|
120
|
+
"Chat data has not been loaded. Use load() to fetch chat data first."
|
121
|
+
)
|
122
|
+
return self._data.get("organization_id")
|
123
|
+
|
124
|
+
@property
|
125
|
+
def is_favorite(self) -> bool:
|
126
|
+
"""Check if chat is favorite."""
|
127
|
+
if not self.is_loaded() and "is_favorite" not in self._data:
|
128
|
+
raise PararamModelNotLoaded(
|
129
|
+
"Chat data has not been loaded. Use load() to fetch chat data first."
|
130
|
+
)
|
131
|
+
return self._data.get("is_favorite", False)
|
132
|
+
|
133
|
+
@property
|
134
|
+
def last_read_post_no(self) -> int:
|
135
|
+
"""Get last read post number."""
|
136
|
+
if not self.is_loaded() and "last_read_post_no" not in self._data:
|
137
|
+
raise PararamModelNotLoaded(
|
138
|
+
"Chat data has not been loaded. Use load() to fetch chat data first."
|
139
|
+
)
|
140
|
+
return self._data.get("last_read_post_no", 0)
|
141
|
+
|
142
|
+
@property
|
143
|
+
def thread_users(self) -> list[int]:
|
144
|
+
"""Get thread user IDs."""
|
145
|
+
if not self.is_loaded() and "thread_users" not in self._data:
|
146
|
+
raise PararamModelNotLoaded(
|
147
|
+
"Chat data has not been loaded. Use load() to fetch chat data first."
|
148
|
+
)
|
149
|
+
return self._data.get("thread_users", [])
|
150
|
+
|
151
|
+
@property
|
152
|
+
def thread_admins(self) -> list[int]:
|
153
|
+
"""Get thread admin IDs."""
|
154
|
+
if not self.is_loaded() and "thread_admins" not in self._data:
|
155
|
+
raise PararamModelNotLoaded(
|
156
|
+
"Chat data has not been loaded. Use load() to fetch chat data first."
|
157
|
+
)
|
158
|
+
return self._data.get("thread_admins", [])
|
159
|
+
|
160
|
+
async def load(self) -> Chat:
|
161
|
+
"""Load full chat data from API.
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
Self with updated data
|
165
|
+
"""
|
166
|
+
# Use the same endpoint as sync version
|
167
|
+
url = f"/core/chat?ids={self.id}"
|
168
|
+
response = await self.client.api_get(url)
|
169
|
+
if response and "chats" in response:
|
170
|
+
chats = response.get("chats", [])
|
171
|
+
if chats:
|
172
|
+
self._data.update(chats[0])
|
173
|
+
return self
|
174
|
+
raise PararamioRequestException(f"failed to load data for chat id {self.id}")
|
175
|
+
|
176
|
+
async def load_posts(
|
177
|
+
self,
|
178
|
+
start_post_no: int = -50,
|
179
|
+
end_post_no: int = -1,
|
180
|
+
limit: int = POSTS_LIMIT,
|
181
|
+
) -> list[Post]:
|
182
|
+
"""Load posts from chat.
|
183
|
+
|
184
|
+
Args:
|
185
|
+
start_post_no: Start post number (negative for from end)
|
186
|
+
end_post_no: End post number (negative for from end)
|
187
|
+
limit: Maximum posts to load
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
List of posts
|
191
|
+
"""
|
192
|
+
validate_post_load_range(start_post_no, end_post_no)
|
193
|
+
|
194
|
+
url = f"/msg/post?chat_id={self.id}&range={start_post_no}x{end_post_no}"
|
195
|
+
|
196
|
+
absolute = abs(end_post_no - start_post_no)
|
197
|
+
if start_post_no < 0:
|
198
|
+
absolute = 1
|
199
|
+
if absolute >= limit:
|
200
|
+
raise PararamioLimitExceededException(f"max post load limit is {limit - 1}")
|
201
|
+
|
202
|
+
response = await self.client.api_get(url)
|
203
|
+
posts_data = response.get("posts", [])
|
204
|
+
|
205
|
+
if not posts_data:
|
206
|
+
return []
|
207
|
+
|
208
|
+
posts = []
|
209
|
+
for post_data in posts_data:
|
210
|
+
post = Post.from_dict(self.client, self, post_data)
|
211
|
+
posts.append(post)
|
212
|
+
|
213
|
+
return posts
|
214
|
+
|
215
|
+
async def get_recent_posts(self, count: int = 50) -> list[Post]:
|
216
|
+
"""Get recent posts from chat.
|
217
|
+
|
218
|
+
Args:
|
219
|
+
count: Number of recent posts to get
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
List of recent posts
|
223
|
+
"""
|
224
|
+
return await self.load_posts(start_post_no=-count, end_post_no=-1)
|
225
|
+
|
226
|
+
async def send_message(
|
227
|
+
self,
|
228
|
+
text: str,
|
229
|
+
reply_to_post_no: int | None = None,
|
230
|
+
quote: str | None = None,
|
231
|
+
) -> Post:
|
232
|
+
"""Send a message to this chat.
|
233
|
+
|
234
|
+
Args:
|
235
|
+
text: Message text
|
236
|
+
reply_to_post_no: Optional post number to reply to
|
237
|
+
quote: Optional quote text
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
Created post
|
241
|
+
"""
|
242
|
+
url = f"/msg/post/{self.id}"
|
243
|
+
data: dict[str, Any] = {
|
244
|
+
"uuid": str(uuid.uuid4().hex),
|
245
|
+
"text": text,
|
246
|
+
}
|
247
|
+
|
248
|
+
if reply_to_post_no:
|
249
|
+
data["reply_no"] = reply_to_post_no
|
250
|
+
if quote:
|
251
|
+
data["quote"] = quote
|
252
|
+
|
253
|
+
response = await self.client.api_post(url, data)
|
254
|
+
post_no = response["post_no"]
|
255
|
+
|
256
|
+
# Create post object directly like sync version does
|
257
|
+
|
258
|
+
post = Post(self.client, self, post_no)
|
259
|
+
# Load the post data
|
260
|
+
await post.load()
|
261
|
+
return post
|
262
|
+
|
263
|
+
async def upload_file(
|
264
|
+
self,
|
265
|
+
file_data: bytes,
|
266
|
+
filename: str,
|
267
|
+
content_type: str | None = None,
|
268
|
+
reply_to_post_no: int | None = None,
|
269
|
+
) -> File:
|
270
|
+
"""Upload a file to this chat.
|
271
|
+
|
272
|
+
Args:
|
273
|
+
file_data: File content as bytes
|
274
|
+
filename: File name
|
275
|
+
content_type: Optional MIME type
|
276
|
+
reply_to_post_no: Optional post number to attach to
|
277
|
+
|
278
|
+
Returns:
|
279
|
+
Uploaded file object
|
280
|
+
"""
|
281
|
+
# This is a simplified implementation
|
282
|
+
# In reality, you'd need to handle multipart/form-data upload
|
283
|
+
raise NotImplementedError("File upload not implemented in this example")
|
284
|
+
|
285
|
+
async def mark_read(self, post_no: int | None = None) -> bool:
|
286
|
+
"""Mark posts as read.
|
287
|
+
|
288
|
+
Args:
|
289
|
+
post_no: Optional specific post number, or None for all
|
290
|
+
|
291
|
+
Returns:
|
292
|
+
True if successful
|
293
|
+
"""
|
294
|
+
url = f"/msg/lastread/{self.id}"
|
295
|
+
data: dict[str, Any] = {"read_all": True} if post_no is None else {"post_no": post_no}
|
296
|
+
|
297
|
+
response = await self.client.api_post(url, data)
|
298
|
+
|
299
|
+
# Update local data
|
300
|
+
if "post_no" in response:
|
301
|
+
self._data["last_read_post_no"] = response["post_no"]
|
302
|
+
if "posts_count" in response:
|
303
|
+
self._data["posts_count"] = response["posts_count"]
|
304
|
+
|
305
|
+
return True
|
306
|
+
|
307
|
+
async def add_users(self, user_ids: list[int]) -> bool:
|
308
|
+
"""Add users to chat.
|
309
|
+
|
310
|
+
Args:
|
311
|
+
user_ids: List of user IDs to add
|
312
|
+
|
313
|
+
Returns:
|
314
|
+
True if successful
|
315
|
+
"""
|
316
|
+
url = f"/core/chat/{self.id}/user/{join_ids(user_ids)}"
|
317
|
+
response = await self.client.api_post(url)
|
318
|
+
return "chat_id" in response
|
319
|
+
|
320
|
+
async def remove_users(self, user_ids: list[int]) -> bool:
|
321
|
+
"""Remove users from chat.
|
322
|
+
|
323
|
+
Args:
|
324
|
+
user_ids: List of user IDs to remove
|
325
|
+
|
326
|
+
Returns:
|
327
|
+
True if successful
|
328
|
+
"""
|
329
|
+
url = f"/core/chat/{self.id}/user/{join_ids(user_ids)}"
|
330
|
+
response = await self.client.api_delete(url)
|
331
|
+
return "chat_id" in response
|
332
|
+
|
333
|
+
async def add_admins(self, user_ids: list[int]) -> bool:
|
334
|
+
"""Add admins to chat.
|
335
|
+
|
336
|
+
Args:
|
337
|
+
user_ids: List of user IDs to make admins
|
338
|
+
|
339
|
+
Returns:
|
340
|
+
True if successful
|
341
|
+
"""
|
342
|
+
url = f"/core/chat/{self.id}/admin/{join_ids(user_ids)}"
|
343
|
+
response = await self.client.api_post(url)
|
344
|
+
return "chat_id" in response
|
345
|
+
|
346
|
+
async def remove_admins(self, user_ids: list[int]) -> bool:
|
347
|
+
"""Remove admins from chat.
|
348
|
+
|
349
|
+
Args:
|
350
|
+
user_ids: List of user IDs to remove admin rights
|
351
|
+
|
352
|
+
Returns:
|
353
|
+
True if successful
|
354
|
+
"""
|
355
|
+
url = f"/core/chat/{self.id}/admin/{join_ids(user_ids)}"
|
356
|
+
response = await self.client.api_delete(url)
|
357
|
+
return "chat_id" in response
|
358
|
+
|
359
|
+
async def update_settings(self, **kwargs) -> bool:
|
360
|
+
"""Update chat settings.
|
361
|
+
|
362
|
+
Args:
|
363
|
+
**kwargs: Settings to update (title, description, etc.)
|
364
|
+
|
365
|
+
Returns:
|
366
|
+
True if successful
|
367
|
+
"""
|
368
|
+
url = f"/core/chat/{self.id}"
|
369
|
+
response = await self.client.api_put(url, kwargs)
|
370
|
+
return "chat_id" in response
|
371
|
+
|
372
|
+
async def delete(self) -> bool:
|
373
|
+
"""Delete this chat.
|
374
|
+
|
375
|
+
Returns:
|
376
|
+
True if successful
|
377
|
+
"""
|
378
|
+
url = f"/core/chat/{self.id}"
|
379
|
+
response = await self.client.api_delete(url)
|
380
|
+
return "chat_id" in response
|
381
|
+
|
382
|
+
async def favorite(self) -> bool:
|
383
|
+
"""Add chat to favorites.
|
384
|
+
|
385
|
+
Returns:
|
386
|
+
True if successful
|
387
|
+
"""
|
388
|
+
url = f"/core/chat/{self.id}/favorite"
|
389
|
+
response = await self.client.api_post(url)
|
390
|
+
return "chat_id" in response
|
391
|
+
|
392
|
+
async def unfavorite(self) -> bool:
|
393
|
+
"""Remove chat from favorites.
|
394
|
+
|
395
|
+
Returns:
|
396
|
+
True if successful
|
397
|
+
"""
|
398
|
+
url = f"/core/chat/{self.id}/unfavorite"
|
399
|
+
response = await self.client.api_post(url)
|
400
|
+
return "chat_id" in response
|
401
|
+
|
402
|
+
def __eq__(self, other) -> bool:
|
403
|
+
"""Check equality with another chat."""
|
404
|
+
if not isinstance(other, Chat):
|
405
|
+
return False
|
406
|
+
return self.id == other.id
|
407
|
+
|
408
|
+
def __str__(self) -> str:
|
409
|
+
"""String representation."""
|
410
|
+
return f"{self.id} - {self.title or 'Untitled'}"
|
411
|
+
|
412
|
+
async def enter(self) -> bool:
|
413
|
+
"""Enter/join the chat.
|
414
|
+
|
415
|
+
Returns:
|
416
|
+
True if successful
|
417
|
+
"""
|
418
|
+
url = f"/core/chat/{self.id}/enter"
|
419
|
+
response = await self.client.api_post(url)
|
420
|
+
return response.get("result") == "OK"
|
421
|
+
|
422
|
+
async def quit(self) -> bool:
|
423
|
+
"""Quit/leave the chat.
|
424
|
+
|
425
|
+
Returns:
|
426
|
+
True if successful
|
427
|
+
"""
|
428
|
+
url = f"/core/chat/{self.id}/quit"
|
429
|
+
response = await self.client.api_post(url)
|
430
|
+
return response.get("result") == "OK"
|
431
|
+
|
432
|
+
async def hide(self) -> bool:
|
433
|
+
"""Hide chat from list.
|
434
|
+
|
435
|
+
Returns:
|
436
|
+
True if successful
|
437
|
+
"""
|
438
|
+
url = f"/core/chat/{self.id}/hide"
|
439
|
+
response = await self.client.api_post(url)
|
440
|
+
return response.get("result") == "OK"
|
441
|
+
|
442
|
+
async def show(self) -> bool:
|
443
|
+
"""Show hidden chat.
|
444
|
+
|
445
|
+
Returns:
|
446
|
+
True if successful
|
447
|
+
"""
|
448
|
+
url = f"/core/chat/{self.id}/show"
|
449
|
+
response = await self.client.api_post(url)
|
450
|
+
return response.get("result") == "OK"
|
451
|
+
|
452
|
+
async def add_groups(self, group_ids: list[int]) -> bool:
|
453
|
+
"""Add groups to chat.
|
454
|
+
|
455
|
+
Args:
|
456
|
+
group_ids: List of group IDs to add
|
457
|
+
|
458
|
+
Returns:
|
459
|
+
True if successful
|
460
|
+
"""
|
461
|
+
url = f"/core/chat/{self.id}/group/{join_ids(group_ids)}"
|
462
|
+
response = await self.client.api_post(url)
|
463
|
+
return response.get("result") == "OK"
|
464
|
+
|
465
|
+
async def delete_groups(self, group_ids: list[int]) -> bool:
|
466
|
+
"""Remove groups from chat.
|
467
|
+
|
468
|
+
Args:
|
469
|
+
group_ids: List of group IDs to remove
|
470
|
+
|
471
|
+
Returns:
|
472
|
+
True if successful
|
473
|
+
"""
|
474
|
+
url = f"/core/chat/{self.id}/group/{join_ids(group_ids)}"
|
475
|
+
response = await self.client.api_delete(url)
|
476
|
+
return response.get("result") == "OK"
|
477
|
+
|
478
|
+
async def transfer(self, org_id: int) -> bool:
|
479
|
+
"""Transfer chat ownership to organization.
|
480
|
+
|
481
|
+
Args:
|
482
|
+
org_id: Organization ID
|
483
|
+
|
484
|
+
Returns:
|
485
|
+
True if successful
|
486
|
+
"""
|
487
|
+
url = f"/core/chat/{self.id}/transfer/{org_id}"
|
488
|
+
response = await self.client.api_post(url)
|
489
|
+
return response.get("result") == "OK"
|
490
|
+
|
491
|
+
async def set_custom_title(self, title: str) -> bool:
|
492
|
+
"""Set custom chat title.
|
493
|
+
|
494
|
+
Args:
|
495
|
+
title: New title
|
496
|
+
|
497
|
+
Returns:
|
498
|
+
True if successful
|
499
|
+
"""
|
500
|
+
url = f"/core/chat/{self.id}/custom_title"
|
501
|
+
response = await self.client.api_post(url, {"title": title})
|
502
|
+
return response.get("result") == "OK"
|
503
|
+
|
504
|
+
async def edit(self, **kwargs) -> bool:
|
505
|
+
"""Edit chat properties.
|
506
|
+
|
507
|
+
Args:
|
508
|
+
**kwargs: Chat properties to update
|
509
|
+
|
510
|
+
Returns:
|
511
|
+
True if successful
|
512
|
+
"""
|
513
|
+
url = f"/core/chat/{self.id}"
|
514
|
+
response = await self.client.api_put(url, kwargs)
|
515
|
+
return response.get("result") == "OK"
|
516
|
+
|
517
|
+
async def read_status(self) -> dict[str, Any]:
|
518
|
+
"""Get read status info.
|
519
|
+
|
520
|
+
Returns:
|
521
|
+
Read status data
|
522
|
+
"""
|
523
|
+
url = f"/core/chat/{self.id}/read_status"
|
524
|
+
return await self.client.api_get(url)
|
525
|
+
|
526
|
+
async def sync_chats(self) -> dict[str, Any]:
|
527
|
+
"""Sync chat data.
|
528
|
+
|
529
|
+
Returns:
|
530
|
+
Sync data
|
531
|
+
"""
|
532
|
+
url = "/core/chat/sync"
|
533
|
+
return await self.client.api_get(url)
|
534
|
+
|
535
|
+
async def post_search(self, query: str, limit: int = 50) -> list[Post]:
|
536
|
+
"""Search posts within chat.
|
537
|
+
|
538
|
+
Note: This endpoint is not in the official documentation but works in practice.
|
539
|
+
|
540
|
+
Args:
|
541
|
+
query: Search query
|
542
|
+
limit: Maximum results (API requires minimum 10)
|
543
|
+
|
544
|
+
Returns:
|
545
|
+
List of matching posts
|
546
|
+
"""
|
547
|
+
|
548
|
+
# API requires limit to be at least 10
|
549
|
+
api_limit = max(limit, 10) if limit else None
|
550
|
+
|
551
|
+
url = f"/posts/search?q={quote_plus(query)}&chat_ids={self.id}"
|
552
|
+
if api_limit:
|
553
|
+
url += f"&limit={api_limit}"
|
554
|
+
|
555
|
+
response = await self.client.api_get(url)
|
556
|
+
|
557
|
+
posts = []
|
558
|
+
posts_data = response.get("posts", [])
|
559
|
+
|
560
|
+
# Apply client-side limit if requested limit is less than API minimum (10)
|
561
|
+
if limit and limit < 10 and limit < len(posts_data):
|
562
|
+
posts_data = posts_data[:limit]
|
563
|
+
elif limit and limit < len(posts_data):
|
564
|
+
posts_data = posts_data[:limit]
|
565
|
+
|
566
|
+
for post_data in posts_data:
|
567
|
+
post_no = post_data.get("post_no")
|
568
|
+
if post_no:
|
569
|
+
post = Post(self.client, self, post_no)
|
570
|
+
posts.append(post)
|
571
|
+
|
572
|
+
return posts
|
573
|
+
|
574
|
+
async def lazy_posts_load(self, start: int = 0, end: int | None = None) -> list[Post]:
|
575
|
+
"""Load posts lazily (async version of lazy loading).
|
576
|
+
|
577
|
+
Args:
|
578
|
+
start: Start index
|
579
|
+
end: End index
|
580
|
+
|
581
|
+
Returns:
|
582
|
+
List of posts
|
583
|
+
"""
|
584
|
+
# Convert to load_posts parameters
|
585
|
+
if end is None:
|
586
|
+
end = start + 50 # Default limit
|
587
|
+
return await self.load_posts(start_post_no=start, end_post_no=end)
|
588
|
+
|
589
|
+
@classmethod
|
590
|
+
async def create_private_chat(cls, client: AsyncPararamio, user_id: int) -> Chat:
|
591
|
+
"""Create private chat with user.
|
592
|
+
|
593
|
+
Args:
|
594
|
+
client: Pararamio client
|
595
|
+
user_id: User ID
|
596
|
+
|
597
|
+
Returns:
|
598
|
+
Created chat
|
599
|
+
"""
|
600
|
+
url = f"/core/chat/pm/{user_id}"
|
601
|
+
response = await client.api_post(url)
|
602
|
+
chat_id = response["chat_id"]
|
603
|
+
|
604
|
+
chat = await client.get_chat_by_id(chat_id)
|
605
|
+
if chat is None:
|
606
|
+
raise ValueError(f"Failed to create or get chat {chat_id}")
|
607
|
+
return chat
|
608
|
+
|
609
|
+
@classmethod
|
610
|
+
async def search_posts(
|
611
|
+
cls,
|
612
|
+
client: AsyncPararamio,
|
613
|
+
q: str,
|
614
|
+
*,
|
615
|
+
order_type: str = "time",
|
616
|
+
page: int = 1,
|
617
|
+
chat_ids: list[int] | None = None,
|
618
|
+
limit: int | None = POSTS_LIMIT,
|
619
|
+
) -> tuple[int, list[Post]]:
|
620
|
+
"""Search for posts across chats.
|
621
|
+
|
622
|
+
Uses chat_ids parameter for chat filtering.
|
623
|
+
Note: This endpoint is not in the official documentation but works in practice.
|
624
|
+
|
625
|
+
Args:
|
626
|
+
client: AsyncPararamio client
|
627
|
+
q: Search query
|
628
|
+
order_type: Sort order type (default: 'time')
|
629
|
+
page: Page number (default: 1)
|
630
|
+
chat_ids: Optional list of chat IDs to search within
|
631
|
+
limit: Maximum results (API requires minimum 10)
|
632
|
+
|
633
|
+
Returns:
|
634
|
+
Tuple of (total_count, list_of_posts)
|
635
|
+
"""
|
636
|
+
url = cls._build_search_url(q, order_type, page, chat_ids, limit)
|
637
|
+
response = await client.api_get(url)
|
638
|
+
|
639
|
+
if "posts" not in response:
|
640
|
+
raise PararamioRequestException("failed to perform search")
|
641
|
+
|
642
|
+
posts_data = response["posts"]
|
643
|
+
# Apply client-side limit if requested limit is less than API minimum (10)
|
644
|
+
if limit and limit < 10 and limit < len(posts_data):
|
645
|
+
posts_data = posts_data[:limit]
|
646
|
+
|
647
|
+
posts = cls._create_posts_from_data(client, posts_data)
|
648
|
+
return response.get("count", len(posts)), posts
|
649
|
+
|
650
|
+
@classmethod
|
651
|
+
def _build_search_url(
|
652
|
+
cls,
|
653
|
+
q: str,
|
654
|
+
order_type: str,
|
655
|
+
page: int,
|
656
|
+
chat_ids: list[int] | None,
|
657
|
+
limit: int | None,
|
658
|
+
) -> str:
|
659
|
+
"""Build search URL with parameters."""
|
660
|
+
url = f"/posts/search?q={quote_plus(q)}"
|
661
|
+
if order_type:
|
662
|
+
url += f"&order_type={order_type}"
|
663
|
+
if page:
|
664
|
+
url += f"&page={page}"
|
665
|
+
|
666
|
+
# API requires limit to be at least 10
|
667
|
+
api_limit = max(limit or POSTS_LIMIT, 10) if limit else None
|
668
|
+
if api_limit:
|
669
|
+
url += f"&limit={api_limit}"
|
670
|
+
|
671
|
+
# Handle chat_ids parameter if provided
|
672
|
+
if chat_ids is not None:
|
673
|
+
url += f"&chat_ids={','.join(map(str, chat_ids))}"
|
674
|
+
|
675
|
+
return url
|
676
|
+
|
677
|
+
@classmethod
|
678
|
+
def _create_posts_from_data(
|
679
|
+
cls, client: AsyncPararamio, posts_data: list[dict[str, Any]]
|
680
|
+
) -> list[Post]:
|
681
|
+
"""Create post objects from search results data."""
|
682
|
+
|
683
|
+
created_chats = {}
|
684
|
+
posts = []
|
685
|
+
|
686
|
+
for post_data in posts_data:
|
687
|
+
chat_id = post_data.get("thread_id")
|
688
|
+
post_no = post_data.get("post_no")
|
689
|
+
|
690
|
+
if chat_id and post_no:
|
691
|
+
# Create chat object if not already created
|
692
|
+
if chat_id not in created_chats:
|
693
|
+
created_chats[chat_id] = cls(client, id=chat_id)
|
694
|
+
|
695
|
+
post = Post(client, created_chats[chat_id], post_no)
|
696
|
+
posts.append(post)
|
697
|
+
|
698
|
+
return posts
|
699
|
+
|
700
|
+
async def create_post(self, text: str, **kwargs) -> Post:
|
701
|
+
"""Create post in chat (alias for send_message).
|
702
|
+
|
703
|
+
Args:
|
704
|
+
text: Post text
|
705
|
+
**kwargs: Additional parameters
|
706
|
+
|
707
|
+
Returns:
|
708
|
+
Created post
|
709
|
+
"""
|
710
|
+
return await self.send_message(text, **kwargs)
|
711
|
+
|
712
|
+
async def post(self, text: str, **kwargs) -> Post:
|
713
|
+
"""Create post in chat (backward compatibility alias for send_message).
|
714
|
+
|
715
|
+
Args:
|
716
|
+
text: Post text
|
717
|
+
**kwargs: Additional parameters
|
718
|
+
|
719
|
+
Returns:
|
720
|
+
Created post
|
721
|
+
"""
|
722
|
+
return await self.send_message(text, **kwargs)
|