maxapi-python 2.4.1__py3-none-any.whl → 2.5.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.
- {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/METADATA +17 -9
- {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/RECORD +74 -45
- {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/WHEEL +1 -1
- pymax/__init__.py +5 -1
- pymax/_data/apk_fingerprints.json +272 -0
- pymax/api/auth/service.py +1 -0
- pymax/api/auth/types.py +3 -0
- pymax/api/bots/payloads.py +8 -0
- pymax/api/bots/service.py +13 -1
- pymax/api/chats/enums.py +1 -0
- pymax/api/chats/payloads.py +19 -0
- pymax/api/chats/service.py +34 -0
- pymax/api/facade.py +2 -0
- pymax/api/messages/enums.py +1 -0
- pymax/api/messages/payloads.py +47 -1
- pymax/api/messages/service.py +308 -24
- pymax/api/response.py +1 -1
- pymax/api/stories/__init__.py +2 -0
- pymax/api/stories/enums.py +19 -0
- pymax/api/stories/payloads.py +64 -0
- pymax/api/stories/service.py +198 -0
- pymax/api/uploads/payloads.py +10 -1
- pymax/api/uploads/service.py +94 -7
- pymax/app.py +16 -4
- pymax/auth/qr.py +11 -1
- pymax/base.py +70 -25
- pymax/client.py +0 -1
- pymax/config.py +38 -32
- pymax/di/__init__.py +2 -0
- pymax/di/context.py +18 -0
- pymax/di/resolver.py +38 -0
- pymax/dispatch/__init__.py +7 -0
- pymax/dispatch/dispatcher.py +143 -40
- pymax/dispatch/enums.py +1 -0
- pymax/dispatch/mapping.py +23 -3
- pymax/dispatch/middleware.py +32 -0
- pymax/dispatch/resolvers.py +4 -0
- pymax/dispatch/router.py +86 -34
- pymax/filters/__init__.py +15 -0
- pymax/filters/base.py +218 -0
- pymax/filters/containers/__init__.py +2 -0
- pymax/filters/containers/chat.py +240 -0
- pymax/filters/containers/message.py +147 -0
- pymax/filters/filters.py +211 -0
- pymax/filters/magic.py +13 -0
- pymax/filters/presets.py +88 -0
- pymax/filters/refs.py +56 -0
- pymax/formatting/markdown.py +55 -1
- pymax/fsm/__init__.py +3 -0
- pymax/fsm/context.py +30 -0
- pymax/fsm/protocol.py +14 -0
- pymax/fsm/state.py +70 -0
- pymax/fsm/storage.py +151 -0
- pymax/infra/base.py +2 -0
- pymax/infra/bots.py +20 -0
- pymax/infra/chat.py +28 -0
- pymax/infra/message.py +195 -6
- pymax/infra/stories.py +205 -0
- pymax/integrations/__init__.py +0 -0
- pymax/integrations/dishka.py +109 -0
- pymax/protocol/enums.py +2 -0
- pymax/protocol/tcp/payload.py +0 -2
- pymax/types/domain/__init__.py +23 -1
- pymax/types/domain/attachments/__init__.py +1 -1
- pymax/types/domain/attachments/keyboards/__init__.py +1 -1
- pymax/types/domain/attachments/keyboards/enums.py +12 -0
- pymax/types/domain/attachments/keyboards/inline.py +74 -3
- pymax/types/domain/callback.py +19 -0
- pymax/types/domain/comment.py +24 -0
- pymax/types/domain/message.py +91 -22
- pymax/types/domain/story.py +126 -0
- pymax/types/events/reaction.py +28 -4
- pymax/versions/catalog.py +1 -1
- {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/licenses/LICENSE +0 -0
pymax/infra/message.py
CHANGED
|
@@ -2,6 +2,7 @@ from pymax.api.messages import DateTimeUnion
|
|
|
2
2
|
from pymax.api.messages.enums import ItemType
|
|
3
3
|
from pymax.api.messages.service import SendAttachments
|
|
4
4
|
from pymax.types import (
|
|
5
|
+
CommentsInfoUpdate,
|
|
5
6
|
FileRequest,
|
|
6
7
|
Message,
|
|
7
8
|
PollState,
|
|
@@ -336,15 +337,11 @@ class MessageMixin(IClientProtocol):
|
|
|
336
337
|
message_id=message_id,
|
|
337
338
|
)
|
|
338
339
|
|
|
339
|
-
async def read_message(self, message_id: int
|
|
340
|
+
async def read_message(self, message_id: int, chat_id: int) -> ReadState:
|
|
340
341
|
"""Отмечает сообщение как прочитанное.
|
|
341
342
|
|
|
342
|
-
У Max различается wire-формат ``message_id`` для отметки прочтения:
|
|
343
|
-
TCP-клиент ожидает ``int``, WebSocket-клиент - ``str``.
|
|
344
|
-
|
|
345
343
|
Args:
|
|
346
|
-
message_id: ID
|
|
347
|
-
``str`` для ``WebClient``.
|
|
344
|
+
message_id: ID сообщения как ``int`` для ``Client`` и ``WebClient``.
|
|
348
345
|
chat_id: ID чата.
|
|
349
346
|
|
|
350
347
|
Returns:
|
|
@@ -379,3 +376,195 @@ class MessageMixin(IClientProtocol):
|
|
|
379
376
|
poll_id=poll_id,
|
|
380
377
|
answer_ids=answer_ids,
|
|
381
378
|
)
|
|
379
|
+
|
|
380
|
+
async def send_comment(
|
|
381
|
+
self,
|
|
382
|
+
chat_id: int,
|
|
383
|
+
post_id: int,
|
|
384
|
+
text: str | None = None,
|
|
385
|
+
reply_to: int | None = None,
|
|
386
|
+
attachments: SendAttachments = None,
|
|
387
|
+
*,
|
|
388
|
+
notify: bool = True,
|
|
389
|
+
) -> Message:
|
|
390
|
+
"""Отправляет комментарий к посту канала.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
chat_id: ID канала.
|
|
394
|
+
post_id: ID поста в канале.
|
|
395
|
+
text: Текст комментария с поддержкой Markdown.
|
|
396
|
+
reply_to: ID комментария, на который нужно ответить.
|
|
397
|
+
attachments: Вложения комментария.
|
|
398
|
+
notify: Отправить уведомление о комментарии.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Опубликованный комментарий как ``Message``.
|
|
402
|
+
"""
|
|
403
|
+
return await self._app.api.messages.send_comment(
|
|
404
|
+
chat_id=chat_id,
|
|
405
|
+
post_id=post_id,
|
|
406
|
+
text=text,
|
|
407
|
+
reply_to=reply_to,
|
|
408
|
+
attachments=attachments,
|
|
409
|
+
notify=notify,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
async def fetch_comments(
|
|
413
|
+
self,
|
|
414
|
+
chat_id: int,
|
|
415
|
+
post_id: int,
|
|
416
|
+
from_: int | None = None,
|
|
417
|
+
backward: int = 30,
|
|
418
|
+
get_messages: bool = True,
|
|
419
|
+
forward: int = 0,
|
|
420
|
+
) -> list[Message]:
|
|
421
|
+
"""Возвращает страницу комментариев поста канала.
|
|
422
|
+
|
|
423
|
+
``chat_id`` задает канал, ``post_id`` — пост внутри него.
|
|
424
|
+
``backward`` и ``forward`` задают количество комментариев по сторонам
|
|
425
|
+
от ``from_``. При ``from_=None`` используется начальная позиция Max.
|
|
426
|
+
"""
|
|
427
|
+
return await self._app.api.messages.fetch_comments(
|
|
428
|
+
chat_id=chat_id,
|
|
429
|
+
post_id=post_id,
|
|
430
|
+
from_=from_,
|
|
431
|
+
backward=backward,
|
|
432
|
+
get_messages=get_messages,
|
|
433
|
+
forward=forward,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
async def get_comments(
|
|
437
|
+
self,
|
|
438
|
+
chat_id: int,
|
|
439
|
+
post_id: int,
|
|
440
|
+
message_ids: list[int],
|
|
441
|
+
) -> list[Message]:
|
|
442
|
+
"""Возвращает конкретные комментарии поста по их ID.
|
|
443
|
+
|
|
444
|
+
``chat_id`` задает канал, ``post_id`` — пост, а ``message_ids`` —
|
|
445
|
+
ID запрашиваемых комментариев.
|
|
446
|
+
"""
|
|
447
|
+
return await self._app.api.messages.get_comments(
|
|
448
|
+
chat_id=chat_id,
|
|
449
|
+
post_id=post_id,
|
|
450
|
+
message_ids=message_ids,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
async def get_comment(
|
|
454
|
+
self,
|
|
455
|
+
chat_id: int,
|
|
456
|
+
post_id: int,
|
|
457
|
+
message_id: int,
|
|
458
|
+
) -> Message | None:
|
|
459
|
+
"""Возвращает комментарий по ID или ``None``, если он не найден."""
|
|
460
|
+
return await self._app.api.messages.get_comment(
|
|
461
|
+
chat_id=chat_id,
|
|
462
|
+
post_id=post_id,
|
|
463
|
+
message_id=message_id,
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
async def edit_comment(
|
|
467
|
+
self,
|
|
468
|
+
chat_id: int,
|
|
469
|
+
post_id: int,
|
|
470
|
+
message_id: int,
|
|
471
|
+
text: str | None = None,
|
|
472
|
+
attachments: SendAttachments = None,
|
|
473
|
+
) -> Message:
|
|
474
|
+
"""Редактирует текст и вложения комментария. Текст поддерживает markdown."""
|
|
475
|
+
return await self._app.api.messages.edit_comment(
|
|
476
|
+
chat_id=chat_id,
|
|
477
|
+
post_id=post_id,
|
|
478
|
+
message_id=message_id,
|
|
479
|
+
text=text,
|
|
480
|
+
attachments=attachments,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
async def delete_comment(
|
|
484
|
+
self,
|
|
485
|
+
chat_id: int,
|
|
486
|
+
post_id: int,
|
|
487
|
+
message_ids: list[int],
|
|
488
|
+
for_me: bool = False,
|
|
489
|
+
) -> bool:
|
|
490
|
+
"""Удаляет комментарии поста. По умолчанию удаляет для всех."""
|
|
491
|
+
return await self._app.api.messages.delete_comment(
|
|
492
|
+
chat_id=chat_id,
|
|
493
|
+
post_id=post_id,
|
|
494
|
+
message_ids=message_ids,
|
|
495
|
+
for_me=for_me,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
async def add_comment_reaction(
|
|
499
|
+
self,
|
|
500
|
+
chat_id: int,
|
|
501
|
+
post_id: int,
|
|
502
|
+
message_id: int,
|
|
503
|
+
reaction: str,
|
|
504
|
+
) -> ReactionInfo | None:
|
|
505
|
+
"""Добавляет или заменяет свою emoji-реакцию на комментарии."""
|
|
506
|
+
return await self._app.api.messages.add_comment_reaction(
|
|
507
|
+
chat_id=chat_id,
|
|
508
|
+
post_id=post_id,
|
|
509
|
+
message_id=message_id,
|
|
510
|
+
reaction=reaction,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
async def remove_comment_reaction(
|
|
514
|
+
self,
|
|
515
|
+
chat_id: int,
|
|
516
|
+
post_id: int,
|
|
517
|
+
message_id: int,
|
|
518
|
+
) -> ReactionInfo | None:
|
|
519
|
+
"""Удаляет свою реакцию с комментария."""
|
|
520
|
+
return await self._app.api.messages.remove_comment_reaction(
|
|
521
|
+
chat_id=chat_id,
|
|
522
|
+
post_id=post_id,
|
|
523
|
+
message_id=message_id,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
async def subscribe_comments(
|
|
527
|
+
self,
|
|
528
|
+
chat_id: int,
|
|
529
|
+
post_id: int,
|
|
530
|
+
subscribe: bool = True,
|
|
531
|
+
) -> None:
|
|
532
|
+
"""Подписывается на события ветки комментариев.
|
|
533
|
+
|
|
534
|
+
Для поддержания подписки повторяйте вызов раз в минуту.
|
|
535
|
+
``subscribe=False`` отменяет подписку.
|
|
536
|
+
"""
|
|
537
|
+
return await self._app.api.messages.subscribe_comments(
|
|
538
|
+
chat_id=chat_id,
|
|
539
|
+
post_id=post_id,
|
|
540
|
+
subscribe=subscribe,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
async def get_comments_info(
|
|
544
|
+
self,
|
|
545
|
+
chat_id: int,
|
|
546
|
+
post_ids: list[int],
|
|
547
|
+
) -> list[CommentsInfoUpdate]:
|
|
548
|
+
"""Возвращает список обновлений счетчиков комментариев постов."""
|
|
549
|
+
return await self._app.api.messages.get_comments_info(
|
|
550
|
+
chat_id=chat_id,
|
|
551
|
+
post_ids=post_ids,
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
async def delete_user_comments(
|
|
555
|
+
self,
|
|
556
|
+
chat_id: int,
|
|
557
|
+
post_id: int,
|
|
558
|
+
user_id: int,
|
|
559
|
+
message_id: int,
|
|
560
|
+
) -> bool:
|
|
561
|
+
"""Удаляет комментарии автора в ветке указанного поста.
|
|
562
|
+
|
|
563
|
+
``message_id`` — ID комментария этого автора.
|
|
564
|
+
"""
|
|
565
|
+
return await self._app.api.messages.delete_user_comments(
|
|
566
|
+
chat_id=chat_id,
|
|
567
|
+
post_id=post_id,
|
|
568
|
+
user_id=user_id,
|
|
569
|
+
message_id=message_id,
|
|
570
|
+
)
|
pymax/infra/stories.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
from pymax.api.stories import StoryStatsFilter
|
|
2
|
+
from pymax.files import Photo, Video
|
|
3
|
+
from pymax.types.domain import Story, StoryDetailedStats, StoryPreview
|
|
4
|
+
|
|
5
|
+
from .protocol import IClientProtocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StoriesMixin(IClientProtocol):
|
|
9
|
+
"""Методы клиента для публикации, чтения и удаления сторис."""
|
|
10
|
+
|
|
11
|
+
async def send_story(
|
|
12
|
+
self,
|
|
13
|
+
media: Photo | Video,
|
|
14
|
+
*,
|
|
15
|
+
duration: int | None = None,
|
|
16
|
+
link: str | None = None,
|
|
17
|
+
) -> Story:
|
|
18
|
+
"""Публикует фото или видео в сторис на 48 часов.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
media: Фото или видео. Принимает локальный файл, URL или байты
|
|
22
|
+
через ``Photo`` или ``Video``.
|
|
23
|
+
duration: Положительная длительность видео в миллисекундах.
|
|
24
|
+
Обязательна для ``Video``; автоматически не определяется.
|
|
25
|
+
link: Необязательная ссылка в нижней части кадра.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Опубликованная сторис.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ValueError: Если длительность видео не задана или неположительна.
|
|
32
|
+
TypeError: Если передан неподдерживаемый тип медиа.
|
|
33
|
+
"""
|
|
34
|
+
return await self._app.api.stories.send_story(
|
|
35
|
+
media=media,
|
|
36
|
+
duration=duration,
|
|
37
|
+
link=link,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
async def get_stories(self, owner_id: int, owner_type: int = 0) -> list[Story]:
|
|
41
|
+
"""Возвращает сторис указанного владельца.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
owner_id: ID владельца, чьи сторис нужно получить.
|
|
45
|
+
owner_type: Тип из ``StoryPreview.owner.type``. ``0`` — пользователь.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Список сторис. Если сторис нет, возвращается пустой список.
|
|
49
|
+
"""
|
|
50
|
+
return await self._app.api.stories.get_stories(owner_id=owner_id, owner_type=owner_type)
|
|
51
|
+
|
|
52
|
+
async def delete_story(self, story_id: int) -> bool:
|
|
53
|
+
"""Удаляет сторис по ее ID.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
story_id: ID удаляемой сторис.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
``True``, если запрос завершился без API-ошибки.
|
|
60
|
+
"""
|
|
61
|
+
return await self._app.api.stories.delete_story(story_id=story_id)
|
|
62
|
+
|
|
63
|
+
async def get_stories_feed(
|
|
64
|
+
self,
|
|
65
|
+
cursor: str = "0",
|
|
66
|
+
count: int = 15,
|
|
67
|
+
) -> tuple[list[StoryPreview], str]:
|
|
68
|
+
"""Возвращает страницу превью владельцев сторис.
|
|
69
|
+
|
|
70
|
+
Превью не содержит ID отдельных сторис. Для их получения используйте
|
|
71
|
+
``get_stories(preview.owner.owner_id, preview.owner.type)``.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
cursor: Курсор страницы. ``"0"`` запрашивает первую страницу.
|
|
75
|
+
count: Количество превью в запрашиваемой странице.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
``(previews, next_cursor)``. Передайте ``next_cursor`` в следующий
|
|
79
|
+
вызов. Пустая строка означает, что дальше страниц нет.
|
|
80
|
+
"""
|
|
81
|
+
return await self._app.api.stories.get_stories_feed(cursor=cursor, count=count)
|
|
82
|
+
|
|
83
|
+
async def get_stories_by_id(
|
|
84
|
+
self,
|
|
85
|
+
owner_id: int,
|
|
86
|
+
story_ids: list[int],
|
|
87
|
+
owner_type: int = 0,
|
|
88
|
+
) -> list[Story]:
|
|
89
|
+
"""Возвращает конкретные сторис владельца по их ID.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
owner_id: ID владельца сторис.
|
|
93
|
+
story_ids: ID запрашиваемых сторис.
|
|
94
|
+
owner_type: Тип из ``StoryPreview.owner.type``. ``0`` — пользователь.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Список найденных сторис.
|
|
98
|
+
"""
|
|
99
|
+
return await self._app.api.stories.get_stories_by_id(
|
|
100
|
+
owner_id=owner_id,
|
|
101
|
+
story_ids=story_ids,
|
|
102
|
+
owner_type=owner_type,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def get_story_detailed_stats(
|
|
106
|
+
self,
|
|
107
|
+
story_id: int,
|
|
108
|
+
filter: StoryStatsFilter,
|
|
109
|
+
marker: int = 0,
|
|
110
|
+
) -> tuple[list[StoryDetailedStats], int]:
|
|
111
|
+
"""Возвращает страницу подробной статистики сторис.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
story_id: ID сторис.
|
|
115
|
+
filter: ``StoryStatsFilter.VIEWS`` — просмотры,
|
|
116
|
+
``StoryStatsFilter.REACTIONS`` — реакции.
|
|
117
|
+
marker: Маркер страницы. ``0`` запрашивает первую страницу.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
``(stats, next_marker)``. Нулевой маркер означает конец списка.
|
|
121
|
+
"""
|
|
122
|
+
return await self._app.api.stories.get_story_detailed_stats(
|
|
123
|
+
story_id=story_id,
|
|
124
|
+
filter=filter,
|
|
125
|
+
marker=marker,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
async def read_story(self, story_id: int, owner_id: int, owner_type: int = 0) -> bool:
|
|
129
|
+
"""Отмечает сторис просмотренной.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
story_id: ID просмотренной сторис.
|
|
133
|
+
owner_id: ID владельца сторис.
|
|
134
|
+
owner_type: Тип из ``StoryPreview.owner.type``. ``0`` — пользователь.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
``True``, если запрос завершился без API-ошибки.
|
|
138
|
+
"""
|
|
139
|
+
return await self._app.api.stories.read_story(
|
|
140
|
+
story_id=story_id,
|
|
141
|
+
owner_id=owner_id,
|
|
142
|
+
owner_type=owner_type,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
async def edit_story(self, story_id: int, settings: int) -> Story:
|
|
146
|
+
"""Меняет настройки опубликованной сторис.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
story_id: ID редактируемой сторис.
|
|
150
|
+
settings: Битовая маска настроек в формате Max.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Обновленная сторис.
|
|
154
|
+
"""
|
|
155
|
+
return await self._app.api.stories.edit_story(story_id=story_id, settings=settings)
|
|
156
|
+
|
|
157
|
+
async def add_story_reaction(
|
|
158
|
+
self,
|
|
159
|
+
owner_id: int,
|
|
160
|
+
story_id: int,
|
|
161
|
+
reaction: str,
|
|
162
|
+
reaction_type: int = 0,
|
|
163
|
+
owner_type: int = 0,
|
|
164
|
+
) -> bool:
|
|
165
|
+
"""Добавляет или заменяет свою реакцию на сторис.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
owner_id: ID владельца сторис.
|
|
169
|
+
story_id: ID сторис.
|
|
170
|
+
reaction: Эмодзи или строковый ID реакции.
|
|
171
|
+
reaction_type: ``0`` для эмодзи, ``1`` для реакции по ID.
|
|
172
|
+
owner_type: Тип из ``StoryPreview.owner.type``. ``0`` — пользователь.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Значение ``success`` из ответа Max.
|
|
176
|
+
"""
|
|
177
|
+
return await self._app.api.stories.add_story_reaction(
|
|
178
|
+
owner_id=owner_id,
|
|
179
|
+
story_id=story_id,
|
|
180
|
+
reaction=reaction,
|
|
181
|
+
reaction_type=reaction_type,
|
|
182
|
+
owner_type=owner_type,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
async def remove_story_reaction(
|
|
186
|
+
self,
|
|
187
|
+
owner_id: int,
|
|
188
|
+
story_id: int,
|
|
189
|
+
owner_type: int = 0,
|
|
190
|
+
) -> bool:
|
|
191
|
+
"""Снимает свою реакцию со сторис.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
owner_id: ID владельца сторис.
|
|
195
|
+
story_id: ID сторис.
|
|
196
|
+
owner_type: Тип из ``StoryPreview.owner.type``. ``0`` — пользователь.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Значение ``success`` из ответа Max.
|
|
200
|
+
"""
|
|
201
|
+
return await self._app.api.stories.remove_story_reaction(
|
|
202
|
+
owner_id=owner_id,
|
|
203
|
+
story_id=story_id,
|
|
204
|
+
owner_type=owner_type,
|
|
205
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from pymax import Router
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from dishka import AsyncContainer
|
|
5
|
+
from dishka.integrations.base import is_dishka_injected, wrap_injection
|
|
6
|
+
except ImportError as e:
|
|
7
|
+
raise RuntimeError(
|
|
8
|
+
"Dishka integration requires the 'dishka' extra. "
|
|
9
|
+
"Install it with `uv add 'maxapi-python[dishka]'`"
|
|
10
|
+
) from e
|
|
11
|
+
|
|
12
|
+
from collections.abc import Callable, Generator
|
|
13
|
+
from typing import Any, ParamSpec, TypeVar
|
|
14
|
+
from weakref import WeakKeyDictionary
|
|
15
|
+
|
|
16
|
+
from pymax.base import BaseClient
|
|
17
|
+
|
|
18
|
+
P = ParamSpec("P")
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
_containers: WeakKeyDictionary[BaseClient[Any], AsyncContainer] = WeakKeyDictionary()
|
|
22
|
+
ClientT = TypeVar("ClientT", bound=BaseClient)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_client(
|
|
26
|
+
args: tuple[Any, ...],
|
|
27
|
+
kwargs: dict[str, Any],
|
|
28
|
+
) -> BaseClient[Any]:
|
|
29
|
+
client = kwargs.get("client")
|
|
30
|
+
|
|
31
|
+
if isinstance(client, BaseClient):
|
|
32
|
+
return client
|
|
33
|
+
|
|
34
|
+
for arg in reversed(args):
|
|
35
|
+
if isinstance(arg, BaseClient):
|
|
36
|
+
return arg
|
|
37
|
+
|
|
38
|
+
raise RuntimeError("Cannot resolve PyMax client for Dishka injection")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_container(
|
|
42
|
+
args: tuple[Any, ...],
|
|
43
|
+
kwargs: dict[str, Any],
|
|
44
|
+
) -> AsyncContainer:
|
|
45
|
+
client = _get_client(args, kwargs)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
return _containers[client]
|
|
49
|
+
except KeyError:
|
|
50
|
+
raise RuntimeError(
|
|
51
|
+
"Dishka is not configured for this client. Call setup_dishka(container, client)."
|
|
52
|
+
) from None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def inject(func: Callable[P, T]) -> Callable[P, T]:
|
|
56
|
+
"""Внедряет параметры ``FromDishka[T]`` в обработчик события.
|
|
57
|
+
|
|
58
|
+
Размещайте ``@inject`` под декоратором регистрации обработчика,
|
|
59
|
+
например ``@router.on_message()``. Контейнер для переданного в обработчик
|
|
60
|
+
клиента должен быть зарегистрирован через ``setup_dishka()``.
|
|
61
|
+
"""
|
|
62
|
+
return wrap_injection(
|
|
63
|
+
func=func,
|
|
64
|
+
is_async=True,
|
|
65
|
+
container_getter=_get_container,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def inject_router(router: Router[ClientT]) -> None:
|
|
70
|
+
"""Оборачивает уже зарегистрированные обработчики роутера и его детей.
|
|
71
|
+
|
|
72
|
+
Пропускает обработчики, уже обернутые через ``inject``. Не затрагивает
|
|
73
|
+
``on_start``, ``on_error``, ``on_disconnect`` и последующие регистрации.
|
|
74
|
+
"""
|
|
75
|
+
for current_router in _iter_routers(router):
|
|
76
|
+
for entries in current_router.handlers.values():
|
|
77
|
+
for entry in entries:
|
|
78
|
+
if not is_dishka_injected(entry.callback):
|
|
79
|
+
entry.callback = inject(entry.callback)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _iter_routers(router: Router[ClientT]) -> Generator[Router[ClientT] | Any, Any, None]:
|
|
83
|
+
yield router
|
|
84
|
+
|
|
85
|
+
for child in router.children:
|
|
86
|
+
yield from _iter_routers(child)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def setup_dishka(
|
|
90
|
+
container: AsyncContainer,
|
|
91
|
+
client: BaseClient[Any],
|
|
92
|
+
*,
|
|
93
|
+
auto_inject: bool = False,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Привязывает контейнер Dishka к клиенту.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
container: Контейнер, из которого обработчики получают зависимости.
|
|
99
|
+
client: Клиент, для которого включается интеграция.
|
|
100
|
+
auto_inject: Обернуть уже зарегистрированные обработчики всего дерева
|
|
101
|
+
роутеров клиента. По умолчанию используется явный ``@inject``.
|
|
102
|
+
|
|
103
|
+
Интеграция не открывает отдельный scope для события, не добавляет событие
|
|
104
|
+
или клиента в контекст Dishka и не закрывает контейнер автоматически.
|
|
105
|
+
"""
|
|
106
|
+
_containers[client] = container
|
|
107
|
+
|
|
108
|
+
if auto_inject:
|
|
109
|
+
inject_router(client._router)
|
pymax/protocol/enums.py
CHANGED
pymax/protocol/tcp/payload.py
CHANGED
|
@@ -128,8 +128,6 @@ class TcpPayloadDecoder:
|
|
|
128
128
|
except ValueError:
|
|
129
129
|
logger.debug("tcp Zstd payload decompression failed", exc_info=True)
|
|
130
130
|
raise
|
|
131
|
-
elif flags > 0x7F:
|
|
132
|
-
raise ValueError(f"invalid TCP compression factor: {flags}")
|
|
133
131
|
elif flags > 0:
|
|
134
132
|
if self.compression is None:
|
|
135
133
|
raise ValueError("LZ4-compressed TCP payload without a decoder")
|
pymax/types/domain/__init__.py
CHANGED
|
@@ -1,15 +1,37 @@
|
|
|
1
1
|
from .attachments import *
|
|
2
2
|
from .bots import InitData
|
|
3
|
+
from .callback import CallbackResponse
|
|
3
4
|
from .chat import Chat
|
|
5
|
+
from .comment import CommentsInfo, CommentsInfoUpdate
|
|
4
6
|
from .error import MaxApiError
|
|
5
7
|
from .folder import Folder, FolderList, FolderUpdate
|
|
6
8
|
from .handshake import HandshakeResponse
|
|
7
9
|
from .login import Login2Flags, Login2Response, LoginResponse
|
|
8
10
|
from .member import Member
|
|
9
|
-
from .message import
|
|
11
|
+
from .message import (
|
|
12
|
+
ForwardLink,
|
|
13
|
+
LinkType,
|
|
14
|
+
Message,
|
|
15
|
+
ReactionCounter,
|
|
16
|
+
ReactionInfo,
|
|
17
|
+
ReadState,
|
|
18
|
+
ReplyLink,
|
|
19
|
+
)
|
|
10
20
|
from .name import Name
|
|
11
21
|
from .presence import Presence
|
|
12
22
|
from .profile import Profile
|
|
13
23
|
from .session import Session
|
|
24
|
+
from .story import (
|
|
25
|
+
Story,
|
|
26
|
+
StoryClickableLink,
|
|
27
|
+
StoryCoordinates,
|
|
28
|
+
StoryDetailedStats,
|
|
29
|
+
StoryLayer,
|
|
30
|
+
StoryMedia,
|
|
31
|
+
StoryOwner,
|
|
32
|
+
StoryPhotoMedia,
|
|
33
|
+
StoryPreview,
|
|
34
|
+
StoryVideoMedia,
|
|
35
|
+
)
|
|
14
36
|
from .sync import SyncOverrides, SyncState
|
|
15
37
|
from .user import ContactInfo, User
|
|
@@ -4,7 +4,7 @@ from .contact import ContactAttachment
|
|
|
4
4
|
from .control import ControlAttachment
|
|
5
5
|
from .enums import AttachmentType, PollFlags
|
|
6
6
|
from .file import FileAttachment, FileRequest
|
|
7
|
-
from .keyboards import InlineKeyboardAttachment
|
|
7
|
+
from .keyboards import Button, ButtonType, InlineKeyboardAttachment
|
|
8
8
|
from .photo import PhotoAttachment
|
|
9
9
|
from .poll import Poll, PollAnswer, PollAttachment, PollState
|
|
10
10
|
from .share import ShareAttachment
|
|
@@ -1 +1 @@
|
|
|
1
|
-
from .inline import InlineKeyboardAttachment
|
|
1
|
+
from .inline import Button, ButtonType, InlineKeyboardAttachment
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ButtonType(str, Enum):
|
|
5
|
+
CALLBACK = "CALLBACK"
|
|
6
|
+
LINK = "LINK"
|
|
7
|
+
CHAT = "CHAT"
|
|
8
|
+
REQUEST_GEO_LOCATION = "REQUEST_GEO_LOCATION"
|
|
9
|
+
REQUEST_CONTACT = "REQUEST_CONTACT"
|
|
10
|
+
OPEN_APP = "OPEN_APP"
|
|
11
|
+
MESSAGE = "MESSAGE"
|
|
12
|
+
CLIPBOARD = "CLIPBOARD"
|