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.
Files changed (57) hide show
  1. pararamio_aio/__init__.py +78 -0
  2. pararamio_aio/_core/__init__.py +125 -0
  3. pararamio_aio/_core/_types.py +120 -0
  4. pararamio_aio/_core/base.py +143 -0
  5. pararamio_aio/_core/client_protocol.py +90 -0
  6. pararamio_aio/_core/constants/__init__.py +7 -0
  7. pararamio_aio/_core/constants/base.py +9 -0
  8. pararamio_aio/_core/constants/endpoints.py +84 -0
  9. pararamio_aio/_core/cookie_decorator.py +208 -0
  10. pararamio_aio/_core/cookie_manager.py +1222 -0
  11. pararamio_aio/_core/endpoints.py +67 -0
  12. pararamio_aio/_core/exceptions/__init__.py +6 -0
  13. pararamio_aio/_core/exceptions/auth.py +91 -0
  14. pararamio_aio/_core/exceptions/base.py +124 -0
  15. pararamio_aio/_core/models/__init__.py +17 -0
  16. pararamio_aio/_core/models/base.py +66 -0
  17. pararamio_aio/_core/models/chat.py +92 -0
  18. pararamio_aio/_core/models/post.py +65 -0
  19. pararamio_aio/_core/models/user.py +54 -0
  20. pararamio_aio/_core/py.typed +2 -0
  21. pararamio_aio/_core/utils/__init__.py +73 -0
  22. pararamio_aio/_core/utils/async_requests.py +417 -0
  23. pararamio_aio/_core/utils/auth_flow.py +202 -0
  24. pararamio_aio/_core/utils/authentication.py +235 -0
  25. pararamio_aio/_core/utils/captcha.py +92 -0
  26. pararamio_aio/_core/utils/helpers.py +336 -0
  27. pararamio_aio/_core/utils/http_client.py +199 -0
  28. pararamio_aio/_core/utils/requests.py +424 -0
  29. pararamio_aio/_core/validators.py +78 -0
  30. pararamio_aio/_types.py +29 -0
  31. pararamio_aio/client.py +989 -0
  32. pararamio_aio/constants/__init__.py +16 -0
  33. pararamio_aio/cookie_manager.py +15 -0
  34. pararamio_aio/exceptions/__init__.py +31 -0
  35. pararamio_aio/exceptions/base.py +1 -0
  36. pararamio_aio/file_operations.py +232 -0
  37. pararamio_aio/models/__init__.py +32 -0
  38. pararamio_aio/models/activity.py +127 -0
  39. pararamio_aio/models/attachment.py +141 -0
  40. pararamio_aio/models/base.py +83 -0
  41. pararamio_aio/models/bot.py +274 -0
  42. pararamio_aio/models/chat.py +722 -0
  43. pararamio_aio/models/deferred_post.py +174 -0
  44. pararamio_aio/models/file.py +103 -0
  45. pararamio_aio/models/group.py +361 -0
  46. pararamio_aio/models/poll.py +275 -0
  47. pararamio_aio/models/post.py +643 -0
  48. pararamio_aio/models/team.py +403 -0
  49. pararamio_aio/models/user.py +239 -0
  50. pararamio_aio/py.typed +2 -0
  51. pararamio_aio/utils/__init__.py +18 -0
  52. pararamio_aio/utils/authentication.py +383 -0
  53. pararamio_aio/utils/requests.py +75 -0
  54. pararamio_aio-2.1.1.dist-info/METADATA +269 -0
  55. pararamio_aio-2.1.1.dist-info/RECORD +57 -0
  56. pararamio_aio-2.1.1.dist-info/WHEEL +5 -0
  57. 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)