Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/api/router.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ Router API
:members:
:show-inheritance:

.. autoclass:: pymax.dispatch.ErrorScope
:members:

.. autoclass:: pymax.dispatch.ErrorContext
:members:

.. autodata:: pymax.ClientRouter

.. autodata:: pymax.WebRouter
16 changes: 16 additions & 0 deletions docs/chats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,22 @@ login/sync, а также методы для загрузки, создания
``leave()`` зависит от типа чата: для группы вызывает выход из группы, для
канала - выход из канала. Из личного диалога выйти нельзя.

Удалить чат
-----------

.. code-block:: python

await client.delete_chat(chat_id=123456)

Через объект ``Chat`` PyMax использует ``chat.last_event_time``:

.. code-block:: python

chat = await client.get_chat(123456)
await chat.delete(for_all=True)

После успешного удаления чат убирается из локального кеша ``client.chats``.

Invite-ссылки
-------------

Expand Down
29 changes: 26 additions & 3 deletions docs/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,21 @@ PyMax потеряет token и попросит авторизацию снов
сессии можно удалить файл ``work_dir/session_name``; тогда потребуется новая
авторизация.

Повторная авторизация
---------------------

Если нужно сбросить текущую локальную сессию и пройти авторизацию заново,
используйте ``relogin()``:

.. code-block:: python

await client.relogin()

``relogin()`` удаляет загруженную сессию из store, закрывает текущий runtime и
по умолчанию сразу запускает клиента снова. Если token был передан через
``ExtraConfig(token=...)``, он тоже сбрасывается; это можно отключить через
``drop_config_token=False``.

Reconnect
---------

Expand All @@ -242,6 +257,14 @@ Reconnect
а новый ``App`` снова получает тот же root router. ``on_start`` вызывается
после каждого успешного reconnect.

Перед повторным подключением можно зарегистрировать ``on_disconnect``:

.. code-block:: python

@client.on_disconnect()
async def disconnected(exc: Exception, reconnect: bool, delay: float) -> None:
print("connection lost:", exc, reconnect, delay)

Отключить reconnect:

.. code-block:: python
Expand Down Expand Up @@ -283,12 +306,12 @@ Debug-логи показывают handshake, login, входящие собы

Чаты
``get_chat()``, ``fetch_chats()``, создание групп, invite-ссылки,
участники, настройки групп и выход из групп/каналов.
участники, настройки групп, удаление чатов и выход из групп/каналов.

Пользователи
``get_user()``, ``get_users()``, ``fetch_users()``, ``search_by_phone()``,
``add_contact()``, ``remove_contact()`` и ``get_chat_id()``. Подробнее:
:doc:`users`.
``add_contact()``, ``remove_contact()``, ``import_contacts()`` и
``get_chat_id()``. Подробнее: :doc:`users`.

Аккаунт
``change_profile()``, папки чатов, активные сессии, ``logout()`` и
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ PyMax - асинхронная Python-библиотека для Max API. Он
:maxdepth: 1
:caption: Новости

release-2-3-0
release-2-2-0
release-2-1-3
release-2-1-2
Expand Down
3 changes: 2 additions & 1 deletion docs/messages.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Messages

Через клиент то же редактирование доступно как
``client.edit_message(chat_id, message_id, text, ...)``.
Новые вложения передаются через ``attachments``.

Отправлять сообщения
--------------------
Expand Down Expand Up @@ -99,7 +100,7 @@ Messages
Служебные события
-----------------

В ``2.2.0`` доступны отдельные обработчики набора текста, присутствия,
Начиная с ``2.2.0`` доступны отдельные обработчики набора текста, присутствия,
прочтения и реакций:

.. code-block:: python
Expand Down
41 changes: 41 additions & 0 deletions docs/release-2-3-0.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
PyMax 2.3.0
===========

Изменения относительно ``2.2.0``.

Добавлено
---------

* Несколько ``on_start``-обработчиков на одном клиенте или роутере. Все
зарегистрированные callbacks запускаются после успешного login.
* ``on_error()`` для централизованной обработки ошибок из handler-ов,
фильтров, ``on_start`` и login на этапе запуска.
* ``ErrorScope.GLOBAL`` и ``ErrorScope.LOCAL`` для выбора области действия
error-handler-а.
* ``on_disconnect()`` для реакции на сетевое отключение перед reconnect. В
callback передаются исходная ошибка, флаг reconnect и задержка.
* ``relogin()`` для удаления текущей локальной сессии и повторной авторизации.
* ``delete_chat()`` на клиенте и ``Chat.delete()`` на bound-объекте чата.
* ``import_contacts()`` и ``ContactInfo`` для импорта контактов из телефонной
книги.
* ``SessionStore.delete_all_sessions()`` и соответствующий метод в
``StoreProtocol``.

Изменилось
----------

* ``edit_message()`` и ``Message.edit()`` принимают новые вложения только через
``attachments=[...]``.
* Тип ``attachments`` для отправки и редактирования сообщений теперь принимает
любую ``Sequence`` из ``Photo``, ``File`` и ``Video``.
* Обработанные login-ошибки на этапе ``start`` больше не приводят к запуску
``on_start``.

Миграция
--------

* В ``edit_message()`` и ``Message.edit()`` используйте ``attachments=[...]``.
Параметр ``attachment`` удален.
* Если error-handler зарегистрирован и успешно отработал, исходная ошибка
считается обработанной. Если сам error-handler падает, исходная ошибка
продолжает распространяться.
49 changes: 48 additions & 1 deletion docs/router.rst
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,54 @@ Handler всегда вызывается как ``handler(event, client)``. Э
print(client.me)

Если включен reconnect, ``on_start`` будет вызван после каждого успешного
переподключения.
переподключения. На одном клиенте или роутере можно зарегистрировать несколько
``on_start``-обработчиков; PyMax запустит каждый из них.

Ошибки handler-ов
-----------------

``on_error`` перехватывает ошибки из фильтров, handler-ов, ``on_start`` и login
на этапе запуска.

.. code-block:: python

from pymax import ApiError, Client, ClientRouter
from pymax.dispatch import ErrorContext, ErrorScope

client = Client(phone="+79990000000", work_dir="cache")


@client.on_error(scope=ErrorScope.GLOBAL)
async def on_err(e: Exception, ctx: ErrorContext[Client]) -> None:
if isinstance(e, ApiError) and e.message == "FAIL_LOGIN_TOKEN":
await ctx.client.relogin()


router = ClientRouter()


@router.on_error(scope=ErrorScope.LOCAL)
async def router_error(e: Exception, ctx: ErrorContext[Client]) -> None:
print("router failed:", e)

``ErrorScope.GLOBAL`` получает ошибки из всего дерева подключенных роутеров.
``ErrorScope.LOCAL`` получает только ошибки того router-а, на котором
зарегистрирован error-handler.

Если error-handler успешно отработал, исходная ошибка считается обработанной.
Если упал сам error-handler, исходная ошибка продолжит распространяться.

Отключение
----------

``on_disconnect`` вызывается при сетевой ошибке перед reconnect или перед
пробросом ошибки, если reconnect отключен.

.. code-block:: python

@client.on_disconnect()
async def disconnected(exc: Exception, reconnect: bool, delay: float) -> None:
print(exc, reconnect, delay)

Raw events
----------
Expand Down
6 changes: 6 additions & 0 deletions docs/types/contact_info.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ContactInfo
===========

.. autoclass:: pymax.types.domain.user.ContactInfo
:members:
:show-inheritance:
4 changes: 4 additions & 0 deletions docs/types/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ Types
``User`` и ``Profile``
Пользователи и профиль текущего аккаунта.

``ContactInfo``
Контакт телефонной книги для ``import_contacts()``.

``PhotoAttachment``, ``VideoAttachment``, ``FileAttachment`` и другие
Входящие вложения в ``message.attaches``.

Expand Down Expand Up @@ -95,6 +98,7 @@ API reference
element
name
user
contact_info
profile
session
folder
Expand Down
19 changes: 19 additions & 0 deletions docs/users.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,25 @@ PyMax хранит контакты, которые Max вернул на login/
await user.add_contact()
await user.remove_contact()

Импортировать контакты из телефонной книги:

.. code-block:: python

from pymax.types import ContactInfo

contacts = await client.import_contacts(
[
ContactInfo(
phone="+79990000000",
first_name="Ada",
last_name="Lovelace",
)
]
)

``last_name`` хранится в ``ContactInfo``, но текущий payload импорта Max
использует только телефон и имя.

Личный чат
----------

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "maxapi-python"
version = "2.2.0"
version = "2.3.0"
description = "Python wrapper для API мессенджера Max"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion src/pymax/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "2.2.0"
__version__ = "2.3.0"


from .auth import (
Expand Down
6 changes: 6 additions & 0 deletions src/pymax/api/chats/payloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,9 @@ class JoinRequestActionPayload(CamelModel):
type: str = "JOIN_REQUEST" # TODO: ENUMM!!!
show_history: bool | None = True
operation: ChatMemberOperation


class DeleteChatPayload(CamelModel):
chat_id: int
last_event_time: int
for_all: bool = True
16 changes: 16 additions & 0 deletions src/pymax/api/chats/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
CreateGroupAttach,
CreateGroupMessage,
CreateGroupPayload,
DeleteChatPayload,
FetchChatsPayload,
FetchJoinRequests,
GetChatInfoPayload,
Expand Down Expand Up @@ -362,3 +363,18 @@ async def decline_join_request(
chat_id=chat_id,
user_ids=[user_id],
)

async def delete_chat(
self,
chat_id: int,
last_event_time: int | None = None,
for_all: bool = True,
) -> None:
frame = DeleteChatPayload(
chat_id=chat_id,
last_event_time=last_event_time or int(time.time() * 1000),
for_all=for_all,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

await self.app.invoke(Opcode.CHAT_DELETE, frame.to_payload())
self._remove_cached_chat(chat_id)
14 changes: 3 additions & 11 deletions src/pymax/api/messages/service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import time
from collections.abc import Sequence
from typing import TYPE_CHECKING, TypeAlias

from pymax.api.binding import bind_api_model, bind_api_models
Expand Down Expand Up @@ -52,7 +53,7 @@
from pymax.app import App

SendAttachment: TypeAlias = Photo | File | Video
SendAttachments: TypeAlias = list[SendAttachment] | None
SendAttachments: TypeAlias = Sequence[SendAttachment] | None

logger = get_logger(__name__)

Expand Down Expand Up @@ -169,24 +170,15 @@ async def edit_message(
chat_id: int,
message_id: int,
text: str,
attachment: SendAttachment | None = None,
attachments: SendAttachments = None,
) -> Message:
if attachment is not None and attachments:
logger.warning("both attachment and attachments provided; using attachments")
attachment = None

edit_attachments = attachments
if attachment is not None:
edit_attachments = [attachment]

clean_text, elements = Formatter.format_markdown(text)
frame = EditMessagePayload(
chat_id=chat_id,
message_id=message_id,
text=clean_text,
elements=elements,
attachments=await self._upload_attachments(edit_attachments),
attachments=await self._upload_attachments(attachments),
)

response = await self.app.invoke(Opcode.MSG_EDIT, frame.to_payload())
Expand Down
22 changes: 22 additions & 0 deletions src/pymax/api/users/payloads.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from collections.abc import Iterable

from pymax.api.models import CamelModel
from pymax.types.domain import ContactInfo

from .enums import ContactAction

Expand All @@ -14,3 +17,22 @@ class SearchByPhonePayload(CamelModel):
class ContactActionPayload(CamelModel):
contact_id: int
action: ContactAction


class _ContactPayload(CamelModel):
first_name: str


class ImportContactsPayload(CamelModel):
contact_list: dict[str, _ContactPayload] # phone -> contact payload

@classmethod
def from_contacts(cls, contacts: Iterable[ContactInfo]) -> "ImportContactsPayload":
return cls(
contact_list={
contact.phone: _ContactPayload(
first_name=contact.first_name,
)
for contact in contacts
}
)
Loading