maxapi-python 2.3.0__tar.gz → 2.3.1__tar.gz
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.3.0 → maxapi_python-2.3.1}/PKG-INFO +2 -1
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/client.rst +3 -3
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/index.rst +1 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/messages.rst +12 -0
- maxapi_python-2.3.1/docs/release-2-3-1.rst +23 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/pyproject.toml +2 -1
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/__init__.py +1 -1
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/messages/payloads.py +21 -1
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/messages/service.py +39 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/message.py +27 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/tcp/compression.py +18 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/tcp/payload.py +20 -4
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/tcp/protocol.py +5 -1
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/message.py +29 -2
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/user.py +6 -4
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/api/test_message_service.py +42 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/domain/test_bound_models.py +15 -4
- maxapi_python-2.3.1/tests/domain/test_user_models.py +35 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/protocol/test_protocols.py +45 -1
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/uv.lock +93 -1
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.github/ISSUE_TEMPLATE/refactor.md +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.github/pull_request_template.md +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.github/workflows/publish.yml +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.github/workflows/tests.yml +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.gitignore +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.pre-commit-config.yaml +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/LICENSE +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/README.md +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/_static/.gitkeep +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/account.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/api/auth.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/api/client-client.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/api/client-config.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/api/client-web.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/api/client.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/api/files.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/api/router.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/auth.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/chats.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/conf.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/examples.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/faq.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/files.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/formatting.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/getting-started.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/release-2-1-0.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/release-2-1-1.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/release-2-1-2.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/release-2-1-3.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/release-2-2-0.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/release-2-3-0.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/router.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/troubleshooting.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/audio_attachment.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/call_attachment.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/chat.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/contact_attachment.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/contact_info.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/control_attachment.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/element.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/enums.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/file_attachment.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/folder.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/folder_list.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/folder_update.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/index.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/inline_keyboard_attachment.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/message.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/message_delete_event.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/message_read_event.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/name.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/photo_attachment.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/presence_event.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/profile.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/reaction_counter.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/reaction_info.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/reaction_update_event.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/read_state.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/session.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/share_attachment.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/sticker_attachment.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/sync_overrides.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/sync_state.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/typing_event.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/user.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/video_attachment.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/users.rst +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/auth/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/auth/enums.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/auth/payloads.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/auth/service.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/auth/types.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/binding.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/bots/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/bots/payloads.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/bots/service.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/chats/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/chats/enums.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/chats/payloads.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/chats/service.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/facade.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/messages/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/messages/enums.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/models.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/response.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/self/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/self/enums.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/self/payloads.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/self/service.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/session/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/session/enums.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/session/payloads.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/session/service.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/uploads/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/uploads/models.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/uploads/payloads.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/uploads/service.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/users/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/users/enums.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/users/payloads.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/users/service.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/app.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/base.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/email.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/models.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/providers.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/qr.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/service.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/sms.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/base.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/client.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/client_web.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/config.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/connection/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/connection/connection.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/connection/pending.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/connection/readers/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/connection/readers/base.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/connection/readers/tcp.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/connection/readers/ws.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/dispatch/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/dispatch/dispatcher.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/dispatch/enums.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/dispatch/mapping.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/dispatch/resolvers.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/dispatch/router.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/exceptions.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/files/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/files/base.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/files/file.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/files/photo.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/files/static.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/files/video.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/formatting/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/formatting/markdown.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/auth.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/base.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/bots.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/chat.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/protocol.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/self.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/user.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/logging.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/base.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/enums.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/models.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/tcp/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/tcp/framing.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/ws/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/ws/protocol.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/py.typed +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/routers.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/session/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/session/models.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/session/protocol.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/session/store.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/telemetry/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/telemetry/navigation.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/telemetry/payloads.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/telemetry/service.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/transport/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/transport/base.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/transport/tcp.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/transport/websocket.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/audio.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/call.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/contact.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/control.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/enums.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/file.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/keyboards/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/keyboards/inline.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/photo.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/share.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/sticker.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/unknown.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/video.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/auth.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/base.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/bots.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/chat.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/element.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/enums.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/error.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/folder.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/login.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/member.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/name.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/presence.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/profile.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/session.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/sync.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/file.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/mark.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/message.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/presence.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/reaction.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/typing.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/video.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/__init__.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/api/test_auth_service.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/api/test_chat_user_self_session_services.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/api/test_upload_service.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/app/test_app_runtime.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/auth/test_auth_flows.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/conftest.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/connection/test_connection.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/connection/test_readers_and_transports.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/dispatch/test_dispatcher.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/domain/test_message_models.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/files/test_files_and_formatting.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/session/test_store.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/telemetry/test_telemetry.py +0 -0
- {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/test_logging.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maxapi-python
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.1
|
|
4
4
|
Summary: Python wrapper для API мессенджера Max
|
|
5
5
|
Project-URL: Homepage, https://github.com/MaxApiTeam/PyMax
|
|
6
6
|
Project-URL: Repository, https://github.com/MaxApiTeam/PyMax
|
|
@@ -31,6 +31,7 @@ Requires-Dist: pydantic>=2.10.0
|
|
|
31
31
|
Requires-Dist: python-socks[asyncio]>=2.8.1
|
|
32
32
|
Requires-Dist: qrcode>=8.2
|
|
33
33
|
Requires-Dist: websockets>=16.0
|
|
34
|
+
Requires-Dist: zstandard>=0.25.0
|
|
34
35
|
Description-Content-Type: text/markdown
|
|
35
36
|
|
|
36
37
|
# PyMax
|
|
@@ -300,9 +300,9 @@ Debug-логи показывают handshake, login, входящие собы
|
|
|
300
300
|
Клиент собирает несколько API-направлений:
|
|
301
301
|
|
|
302
302
|
Сообщения
|
|
303
|
-
``send_message()``, ``
|
|
304
|
-
``pin_message()``, ``read_message()``, реакции и
|
|
305
|
-
файлов/видео.
|
|
303
|
+
``send_message()``, ``forward_message()``, ``fetch_history()``,
|
|
304
|
+
``delete_message()``, ``pin_message()``, ``read_message()``, реакции и
|
|
305
|
+
получение URL для входящих файлов/видео.
|
|
306
306
|
|
|
307
307
|
Чаты
|
|
308
308
|
``get_chat()``, ``fetch_chats()``, создание групп, invite-ссылки,
|
|
@@ -70,6 +70,18 @@ Messages
|
|
|
70
70
|
async def on_message(message: Message, client: Client) -> None:
|
|
71
71
|
await message.answer("Ответ в тот же чат")
|
|
72
72
|
await message.reply("Ответ реплаем")
|
|
73
|
+
await message.forward(chat_id=654321)
|
|
74
|
+
|
|
75
|
+
Переслать сообщение напрямую через клиент можно с указанием исходного и
|
|
76
|
+
целевого чатов:
|
|
77
|
+
|
|
78
|
+
.. code-block:: python
|
|
79
|
+
|
|
80
|
+
await client.forward_message(
|
|
81
|
+
chat_id=654321,
|
|
82
|
+
message_id=987654,
|
|
83
|
+
source_chat_id=123456,
|
|
84
|
+
)
|
|
73
85
|
|
|
74
86
|
Ответ, реакции, удаление и прочтение
|
|
75
87
|
----------------------------------------
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
PyMax 2.3.1
|
|
2
|
+
===========
|
|
3
|
+
|
|
4
|
+
Изменения относительно ``2.3.0``.
|
|
5
|
+
|
|
6
|
+
Добавлено
|
|
7
|
+
---------
|
|
8
|
+
|
|
9
|
+
* ``forward_message()`` на клиенте и ``Message.forward()`` на bound-объекте
|
|
10
|
+
сообщения. Для пересылки между разными чатами укажите ``source_chat_id``.
|
|
11
|
+
|
|
12
|
+
Исправлено
|
|
13
|
+
----------
|
|
14
|
+
|
|
15
|
+
* Декодирование сжатых TCP payload-ов: коэффициенты LZ4 теперь обрабатываются
|
|
16
|
+
корректно, а payload-ы с флагом ``0xFF`` декодируются через Zstandard.
|
|
17
|
+
* Разбор профилей bot-аккаунтов, в которых ``gender`` приходит числом, а
|
|
18
|
+
``web_app`` — URL-строкой.
|
|
19
|
+
|
|
20
|
+
Зависимости
|
|
21
|
+
-----------
|
|
22
|
+
|
|
23
|
+
* Добавлена runtime-зависимость ``zstandard`` для декодирования TCP payload-ов.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "maxapi-python"
|
|
3
|
-
version = "2.3.
|
|
3
|
+
version = "2.3.1"
|
|
4
4
|
description = "Python wrapper для API мессенджера Max"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -31,6 +31,7 @@ dependencies = [
|
|
|
31
31
|
"python-socks[asyncio]>=2.8.1",
|
|
32
32
|
"qrcode>=8.2",
|
|
33
33
|
"websockets>=16.0",
|
|
34
|
+
"zstandard>=0.25.0",
|
|
34
35
|
]
|
|
35
36
|
|
|
36
37
|
[project.urls]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import Any, Literal
|
|
2
2
|
|
|
3
3
|
from pydantic import Field
|
|
4
4
|
|
|
@@ -46,6 +46,26 @@ class SendMessagePayload(CamelModel):
|
|
|
46
46
|
notify: bool = False
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
class ForwardLink(CamelModel):
|
|
50
|
+
type: Literal["FORWARD"] = "FORWARD"
|
|
51
|
+
message_id: str
|
|
52
|
+
chat_id: int
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ForwardMessagePayloadMessage(CamelModel):
|
|
56
|
+
cid: int
|
|
57
|
+
link: ForwardLink
|
|
58
|
+
attaches: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload] = Field(
|
|
59
|
+
default_factory=list
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ForwardMessagePayload(CamelModel):
|
|
64
|
+
chat_id: int
|
|
65
|
+
message: ForwardMessagePayloadMessage
|
|
66
|
+
notify: bool = True
|
|
67
|
+
|
|
68
|
+
|
|
49
69
|
class ChatHistoryPayload(CamelModel):
|
|
50
70
|
chat_id: int
|
|
51
71
|
forward: int
|
|
@@ -36,6 +36,9 @@ from .payloads import (
|
|
|
36
36
|
ChatHistoryPayload,
|
|
37
37
|
DeleteMessagePayload,
|
|
38
38
|
EditMessagePayload,
|
|
39
|
+
ForwardLink,
|
|
40
|
+
ForwardMessagePayload,
|
|
41
|
+
ForwardMessagePayloadMessage,
|
|
39
42
|
GetFilePayload,
|
|
40
43
|
GetMessagesPayload,
|
|
41
44
|
GetReactionsPayload,
|
|
@@ -139,6 +142,42 @@ class MessageService:
|
|
|
139
142
|
logger.info("message sent chat_id=%s", chat_id)
|
|
140
143
|
return message
|
|
141
144
|
|
|
145
|
+
async def forward_message(
|
|
146
|
+
self,
|
|
147
|
+
chat_id: int,
|
|
148
|
+
message_id: int | str,
|
|
149
|
+
source_chat_id: int | None = None,
|
|
150
|
+
*,
|
|
151
|
+
notify: bool = True,
|
|
152
|
+
) -> Message | None:
|
|
153
|
+
source_chat_id = chat_id if source_chat_id is None else source_chat_id
|
|
154
|
+
logger.info(
|
|
155
|
+
"forwarding message source_chat_id=%s chat_id=%s message_id=%s",
|
|
156
|
+
source_chat_id,
|
|
157
|
+
chat_id,
|
|
158
|
+
message_id,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
frame = ForwardMessagePayload(
|
|
162
|
+
chat_id=chat_id,
|
|
163
|
+
message=ForwardMessagePayloadMessage(
|
|
164
|
+
cid=-self._next_cid(),
|
|
165
|
+
link=ForwardLink(
|
|
166
|
+
message_id=str(message_id),
|
|
167
|
+
chat_id=source_chat_id,
|
|
168
|
+
),
|
|
169
|
+
),
|
|
170
|
+
notify=notify,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
response = await self.app.invoke(Opcode.MSG_SEND, frame.to_payload())
|
|
174
|
+
message = bind_api_model(
|
|
175
|
+
self.app,
|
|
176
|
+
require_payload_model(response, Message),
|
|
177
|
+
)
|
|
178
|
+
logger.info("message forwarded source_chat_id=%s chat_id=%s", source_chat_id, chat_id)
|
|
179
|
+
return message
|
|
180
|
+
|
|
142
181
|
async def get_messages(
|
|
143
182
|
self,
|
|
144
183
|
chat_id: int,
|
|
@@ -62,6 +62,33 @@ class MessageMixin(IClientProtocol):
|
|
|
62
62
|
message_id=message_id,
|
|
63
63
|
)
|
|
64
64
|
|
|
65
|
+
async def forward_message(
|
|
66
|
+
self,
|
|
67
|
+
chat_id: int,
|
|
68
|
+
message_id: int | str,
|
|
69
|
+
source_chat_id: int | None = None,
|
|
70
|
+
*,
|
|
71
|
+
notify: bool = True,
|
|
72
|
+
) -> Message | None:
|
|
73
|
+
"""Пересылает существующее сообщение в чат.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
chat_id: ID целевого чата.
|
|
77
|
+
message_id: ID пересылаемого сообщения.
|
|
78
|
+
source_chat_id: ID исходного чата. Если не указан, используется
|
|
79
|
+
целевой чат.
|
|
80
|
+
notify: Отправить ли получателям push-уведомление.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Пересланное сообщение или ``None``, если сервер не вернул его.
|
|
84
|
+
"""
|
|
85
|
+
return await self._app.api.messages.forward_message(
|
|
86
|
+
chat_id=chat_id,
|
|
87
|
+
message_id=message_id,
|
|
88
|
+
source_chat_id=source_chat_id,
|
|
89
|
+
notify=notify,
|
|
90
|
+
)
|
|
91
|
+
|
|
65
92
|
async def get_messages(
|
|
66
93
|
self,
|
|
67
94
|
chat_id: int,
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
|
|
3
|
+
import zstandard
|
|
4
|
+
|
|
5
|
+
|
|
1
6
|
class Lz4BlockCompression:
|
|
2
7
|
def decompress(self, src: bytes, max_output: int = 5 * 1024 * 1024) -> bytes:
|
|
3
8
|
dst = bytearray()
|
|
@@ -95,3 +100,16 @@ class Lz4BlockCompression:
|
|
|
95
100
|
dst.extend(src[lit_start : lit_start + lit_len])
|
|
96
101
|
|
|
97
102
|
return bytes(dst)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ZstdCompression:
|
|
106
|
+
def decompress(self, src: bytes, max_output: int = 5 * 1024 * 1024) -> bytes:
|
|
107
|
+
try:
|
|
108
|
+
with zstandard.ZstdDecompressor().stream_reader(BytesIO(src)) as reader:
|
|
109
|
+
result = reader.read(max_output + 1)
|
|
110
|
+
except zstandard.ZstdError as e:
|
|
111
|
+
raise ValueError("Zstd: failed to decompress payload") from e
|
|
112
|
+
|
|
113
|
+
if len(result) > max_output:
|
|
114
|
+
raise ValueError("Zstd: output too large")
|
|
115
|
+
return result
|
|
@@ -5,7 +5,7 @@ import msgpack
|
|
|
5
5
|
|
|
6
6
|
from pymax.logging import get_logger
|
|
7
7
|
|
|
8
|
-
from .compression import Lz4BlockCompression
|
|
8
|
+
from .compression import Lz4BlockCompression, ZstdCompression
|
|
9
9
|
|
|
10
10
|
logger = get_logger(__name__)
|
|
11
11
|
|
|
@@ -70,9 +70,11 @@ class TcpPayloadDecoder:
|
|
|
70
70
|
*,
|
|
71
71
|
serializer: MsgpackPayloadCodec,
|
|
72
72
|
compression: Lz4BlockCompression | None = None,
|
|
73
|
+
zstd_compression: ZstdCompression | None = None,
|
|
73
74
|
) -> None:
|
|
74
75
|
self.serializer = serializer
|
|
75
76
|
self.compression = compression
|
|
77
|
+
self.zstd_compression = zstd_compression
|
|
76
78
|
|
|
77
79
|
def _normalize_keys(self, obj: Any) -> Any:
|
|
78
80
|
if isinstance(obj, dict):
|
|
@@ -97,12 +99,26 @@ class TcpPayloadDecoder:
|
|
|
97
99
|
if not payload_bytes:
|
|
98
100
|
return {}
|
|
99
101
|
|
|
100
|
-
if flags
|
|
102
|
+
if flags == 0xFF:
|
|
103
|
+
if self.zstd_compression is None:
|
|
104
|
+
raise ValueError("Zstd-compressed TCP payload without a decoder")
|
|
105
|
+
try:
|
|
106
|
+
payload_bytes = self.zstd_compression.decompress(payload_bytes)
|
|
107
|
+
logger.debug("tcp payload decompressed with Zstd")
|
|
108
|
+
except ValueError:
|
|
109
|
+
logger.debug("tcp Zstd payload decompression failed", exc_info=True)
|
|
110
|
+
raise
|
|
111
|
+
elif flags > 0x7F:
|
|
112
|
+
raise ValueError(f"invalid TCP compression factor: {flags}")
|
|
113
|
+
elif flags > 0:
|
|
114
|
+
if self.compression is None:
|
|
115
|
+
raise ValueError("LZ4-compressed TCP payload without a decoder")
|
|
101
116
|
try:
|
|
102
117
|
payload_bytes = self.compression.decompress(payload_bytes)
|
|
103
|
-
logger.debug("tcp payload decompressed
|
|
118
|
+
logger.debug("tcp payload decompressed cof=%s", flags)
|
|
104
119
|
except ValueError:
|
|
105
|
-
logger.debug("tcp payload
|
|
120
|
+
logger.debug("tcp payload decompression failed cof=%s", flags, exc_info=True)
|
|
121
|
+
raise
|
|
106
122
|
|
|
107
123
|
result = self.serializer.decode(payload_bytes)
|
|
108
124
|
return self._normalize_keys(result)
|
|
@@ -7,6 +7,7 @@ from .payload import (
|
|
|
7
7
|
Lz4BlockCompression,
|
|
8
8
|
MsgpackPayloadCodec,
|
|
9
9
|
TcpPayloadDecoder,
|
|
10
|
+
ZstdCompression,
|
|
10
11
|
)
|
|
11
12
|
|
|
12
13
|
logger = get_logger(__name__)
|
|
@@ -20,8 +21,11 @@ class TcpProtocol(BaseProtocol):
|
|
|
20
21
|
self.framer = TcpPacketFramer()
|
|
21
22
|
self.serializer = MsgpackPayloadCodec()
|
|
22
23
|
self.compression = Lz4BlockCompression()
|
|
24
|
+
self.zstd_compression = ZstdCompression()
|
|
23
25
|
self.payload_decoder = TcpPayloadDecoder(
|
|
24
|
-
serializer=self.serializer,
|
|
26
|
+
serializer=self.serializer,
|
|
27
|
+
compression=self.compression,
|
|
28
|
+
zstd_compression=self.zstd_compression,
|
|
25
29
|
)
|
|
26
30
|
|
|
27
31
|
def encode(self, frame: OutboundFrame) -> bytes:
|
|
@@ -93,8 +93,9 @@ class Message(CamelModel):
|
|
|
93
93
|
|
|
94
94
|
Сообщения, полученные через клиент, обычно уже привязаны к сервису
|
|
95
95
|
сообщений. После этого можно вызывать удобные методы объекта:
|
|
96
|
-
:meth:`reply`, :meth:`answer`, :meth:`
|
|
97
|
-
:meth:`read`, :meth:`react`, :meth:`unreact` и
|
|
96
|
+
:meth:`reply`, :meth:`answer`, :meth:`forward`, :meth:`edit`, :meth:`pin`,
|
|
97
|
+
:meth:`delete`, :meth:`read`, :meth:`react`, :meth:`unreact` и
|
|
98
|
+
:meth:`get_reactions`.
|
|
98
99
|
|
|
99
100
|
Используйте ``Message`` в обработчиках ``on_message`` и при работе с
|
|
100
101
|
историей. Некоторые поля могут быть ``None``, потому что Max присылает
|
|
@@ -244,6 +245,32 @@ class Message(CamelModel):
|
|
|
244
245
|
notify=notify,
|
|
245
246
|
)
|
|
246
247
|
|
|
248
|
+
async def forward(
|
|
249
|
+
self,
|
|
250
|
+
chat_id: int,
|
|
251
|
+
*,
|
|
252
|
+
notify: bool = True,
|
|
253
|
+
) -> Message | None:
|
|
254
|
+
"""Пересылает это сообщение в другой чат.
|
|
255
|
+
|
|
256
|
+
:param chat_id: ID целевого чата.
|
|
257
|
+
:type chat_id: int
|
|
258
|
+
:param notify: Отправить ли получателям push-уведомление.
|
|
259
|
+
:type notify: bool
|
|
260
|
+
:returns: Пересланное сообщение или ``None``, если сервер его не вернул.
|
|
261
|
+
:rtype: Message | None
|
|
262
|
+
:raises RuntimeError: Если сообщение не привязано к сервису или не
|
|
263
|
+
содержит ``chat_id``.
|
|
264
|
+
"""
|
|
265
|
+
actions, source_chat_id = self._bound()
|
|
266
|
+
|
|
267
|
+
return await actions.forward_message(
|
|
268
|
+
chat_id=chat_id,
|
|
269
|
+
message_id=self.id,
|
|
270
|
+
source_chat_id=source_chat_id,
|
|
271
|
+
notify=notify,
|
|
272
|
+
)
|
|
273
|
+
|
|
247
274
|
async def pin(self, notify_pin: bool = True) -> bool:
|
|
248
275
|
"""Закрепляет это сообщение в чате.
|
|
249
276
|
|
|
@@ -49,11 +49,11 @@ class User(CamelModel):
|
|
|
49
49
|
:ivar description: Описание профиля.
|
|
50
50
|
:vartype description: str | None
|
|
51
51
|
:ivar gender: Пол пользователя.
|
|
52
|
-
:vartype gender: str | None
|
|
52
|
+
:vartype gender: str | int | None
|
|
53
53
|
:ivar link: Ссылка на профиль.
|
|
54
54
|
:vartype link: str | None
|
|
55
55
|
:ivar web_app: Данные связанного web-приложения, если есть.
|
|
56
|
-
:vartype web_app: dict[str, Any] | None
|
|
56
|
+
:vartype web_app: dict[str, Any] | str | None
|
|
57
57
|
:ivar menu_button: Данные кнопки меню профиля, если есть.
|
|
58
58
|
:vartype menu_button: dict[str, Any] | None
|
|
59
59
|
"""
|
|
@@ -71,9 +71,11 @@ class User(CamelModel):
|
|
|
71
71
|
phone: int | None = None
|
|
72
72
|
status: str | None = None
|
|
73
73
|
description: str | None = None
|
|
74
|
-
gender
|
|
74
|
+
# Bots may send ``gender`` as a numeric code and ``web_app`` as a URL
|
|
75
|
+
# string instead of an object; accept these so profile parsing won't fail.
|
|
76
|
+
gender: str | int | None = None
|
|
75
77
|
link: str | None = None
|
|
76
|
-
web_app: dict[str, Any] | None = None
|
|
78
|
+
web_app: dict[str, Any] | str | None = None
|
|
77
79
|
menu_button: dict[str, Any] | None = None
|
|
78
80
|
|
|
79
81
|
_actions: UserService | None = PrivateAttr(default=None)
|
|
@@ -56,6 +56,48 @@ async def test_send_message_raises_when_attachment_upload_fails() -> None:
|
|
|
56
56
|
assert app.calls == []
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
@pytest.mark.asyncio
|
|
60
|
+
async def test_forward_message_builds_payload_and_binds_result(
|
|
61
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
62
|
+
) -> None:
|
|
63
|
+
monkeypatch.setattr("pymax.api.messages.service.time.time", lambda: 1000.0)
|
|
64
|
+
app = FakeApp([frame(message_payload(55, 200, "forwarded"))])
|
|
65
|
+
|
|
66
|
+
result = await app.api.messages.forward_message(
|
|
67
|
+
chat_id=200,
|
|
68
|
+
message_id=116742887450236083,
|
|
69
|
+
source_chat_id=100,
|
|
70
|
+
notify=False,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
assert result is not None
|
|
74
|
+
assert result.id == 55
|
|
75
|
+
assert result._actions is app.api.messages
|
|
76
|
+
assert app.calls[0].opcode == Opcode.MSG_SEND
|
|
77
|
+
assert app.calls[0].payload == {
|
|
78
|
+
"chatId": 200,
|
|
79
|
+
"message": {
|
|
80
|
+
"cid": -1000001,
|
|
81
|
+
"link": {
|
|
82
|
+
"type": "FORWARD",
|
|
83
|
+
"messageId": "116742887450236083",
|
|
84
|
+
"chatId": 100,
|
|
85
|
+
},
|
|
86
|
+
"attaches": [],
|
|
87
|
+
},
|
|
88
|
+
"notify": False,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.asyncio
|
|
93
|
+
async def test_forward_message_defaults_source_to_target_chat() -> None:
|
|
94
|
+
app = FakeApp([frame(message_payload(55, 200, "forwarded"))])
|
|
95
|
+
|
|
96
|
+
await app.api.messages.forward_message(chat_id=200, message_id="55")
|
|
97
|
+
|
|
98
|
+
assert app.calls[0].payload["message"]["link"]["chatId"] == 200
|
|
99
|
+
|
|
100
|
+
|
|
59
101
|
@pytest.mark.asyncio
|
|
60
102
|
async def test_upload_attachments_handles_file_video_and_empty_lists() -> None:
|
|
61
103
|
app = FakeApp()
|
|
@@ -14,6 +14,10 @@ class MessageActions:
|
|
|
14
14
|
self.calls.append(("send_message", args, kwargs))
|
|
15
15
|
return "sent"
|
|
16
16
|
|
|
17
|
+
async def forward_message(self, *args, **kwargs):
|
|
18
|
+
self.calls.append(("forward_message", args, kwargs))
|
|
19
|
+
return "forwarded"
|
|
20
|
+
|
|
17
21
|
async def get_message(self, *args, **kwargs):
|
|
18
22
|
self.calls.append(("get_message", args, kwargs))
|
|
19
23
|
return "message"
|
|
@@ -106,6 +110,7 @@ async def test_message_bound_methods_delegate_with_chat_and_message_ids() -> Non
|
|
|
106
110
|
|
|
107
111
|
assert await message.reply("reply") == "sent"
|
|
108
112
|
assert await message.answer("answer", reply_to=9) == "sent"
|
|
113
|
+
assert await message.forward(200, notify=False) == "forwarded"
|
|
109
114
|
assert (
|
|
110
115
|
await message.edit(
|
|
111
116
|
"edited",
|
|
@@ -122,10 +127,16 @@ async def test_message_bound_methods_delegate_with_chat_and_message_ids() -> Non
|
|
|
122
127
|
|
|
123
128
|
assert actions.calls[0][2]["reply_to"] == 10
|
|
124
129
|
assert actions.calls[1][2]["reply_to"] == 9
|
|
125
|
-
assert actions.calls[2][2]
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
assert actions.calls[2][2] == {
|
|
131
|
+
"chat_id": 200,
|
|
132
|
+
"message_id": 10,
|
|
133
|
+
"source_chat_id": 100,
|
|
134
|
+
"notify": False,
|
|
135
|
+
}
|
|
136
|
+
assert actions.calls[3][2]["message_id"] == 10
|
|
137
|
+
assert actions.calls[3][2]["attachments"] == ["file"]
|
|
138
|
+
assert actions.calls[5][2]["message_ids"] == [10]
|
|
139
|
+
assert actions.calls[7][2]["message_id"] == "10"
|
|
129
140
|
|
|
130
141
|
|
|
131
142
|
@pytest.mark.asyncio
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from pymax.types.domain import User
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_user_parses_bot_gender_int_and_web_app_url() -> None:
|
|
5
|
+
"""Bot accounts send ``gender`` as a numeric code and ``web_app`` as a URL
|
|
6
|
+
string (observed for the "Алиса AI" bot); the profile must still parse."""
|
|
7
|
+
payload = {
|
|
8
|
+
"id": 6738397,
|
|
9
|
+
"names": [{"name": "Алиса AI", "type": "NICK"}],
|
|
10
|
+
"gender": 1,
|
|
11
|
+
"webApp": "https://alice.yandex.ru/max_onboarding",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
user = User.model_validate(payload)
|
|
15
|
+
|
|
16
|
+
assert user.gender == 1
|
|
17
|
+
assert user.web_app == "https://alice.yandex.ru/max_onboarding"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_user_parses_human_without_optional_fields() -> None:
|
|
21
|
+
"""Regular users come without ``gender`` and ``web_app``."""
|
|
22
|
+
payload = {"id": 1, "names": [{"name": "Test User", "type": "NICK"}]}
|
|
23
|
+
|
|
24
|
+
user = User.model_validate(payload)
|
|
25
|
+
|
|
26
|
+
assert user.gender is None
|
|
27
|
+
assert user.web_app is None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_user_still_accepts_dict_web_app() -> None:
|
|
31
|
+
"""The dict type for ``web_app`` is kept from the original PyMax schema
|
|
32
|
+
(no real example of this format was seen, but we keep compatibility)."""
|
|
33
|
+
user = User.model_validate({"id": 2, "webApp": {}})
|
|
34
|
+
|
|
35
|
+
assert user.web_app == {}
|
|
@@ -2,10 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import msgpack
|
|
4
4
|
import pytest
|
|
5
|
+
import zstandard
|
|
5
6
|
|
|
6
7
|
from pymax.api.messages.enums import ItemType
|
|
7
8
|
from pymax.protocol import Command, InboundFrame, Opcode, OutboundFrame
|
|
8
|
-
from pymax.protocol.tcp.compression import Lz4BlockCompression
|
|
9
|
+
from pymax.protocol.tcp.compression import Lz4BlockCompression, ZstdCompression
|
|
9
10
|
from pymax.protocol.tcp.framing import TcpPacketFramer
|
|
10
11
|
from pymax.protocol.tcp.payload import MsgpackPayloadCodec, TcpPayloadDecoder
|
|
11
12
|
from pymax.protocol.tcp.protocol import TcpProtocol
|
|
@@ -128,6 +129,49 @@ def test_msgpack_codec_uses_first_dict_when_stream_has_extra_data() -> None:
|
|
|
128
129
|
assert codec.decode(encoded) == {"ok": True}
|
|
129
130
|
|
|
130
131
|
|
|
132
|
+
def test_tcp_payload_decoder_decompresses_lz4_for_compression_factor_four() -> None:
|
|
133
|
+
# This is a raw LZ4 block produced by the official-compatible compressor.
|
|
134
|
+
# Its first byte is 0xF4, which MsgPack reads as -12 when decompression is
|
|
135
|
+
# incorrectly skipped for cof=4.
|
|
136
|
+
compressed = bytes.fromhex(
|
|
137
|
+
"f40a84a6707265666978a27878a464617461b0664a73436c4b437508008f"
|
|
138
|
+
"a47461696cd92a79010016dfa6726570656174d9684142434404004c5044"
|
|
139
|
+
"41424344"
|
|
140
|
+
)
|
|
141
|
+
decoder = TcpPayloadDecoder(
|
|
142
|
+
serializer=MsgpackPayloadCodec(),
|
|
143
|
+
compression=Lz4BlockCompression(),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
decoded = decoder.decode(compressed, flags=4)
|
|
147
|
+
|
|
148
|
+
assert decoded == {
|
|
149
|
+
"prefix": "xx",
|
|
150
|
+
"data": "fJsClKCufJsClKCu",
|
|
151
|
+
"tail": "y" * 42,
|
|
152
|
+
"repeat": "ABCD" * 26,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_tcp_payload_decoder_decompresses_zstd() -> None:
|
|
157
|
+
expected = {"error": "FAIL_LOGIN_TOKEN", "message": "Token expired"}
|
|
158
|
+
compressed = zstandard.ZstdCompressor().compress(msgpack.packb(expected, use_bin_type=True))
|
|
159
|
+
decoder = TcpPayloadDecoder(
|
|
160
|
+
serializer=MsgpackPayloadCodec(),
|
|
161
|
+
compression=Lz4BlockCompression(),
|
|
162
|
+
zstd_compression=ZstdCompression(),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
assert decoder.decode(compressed, flags=0xFF) == expected
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_zstd_decompression_rejects_oversized_output() -> None:
|
|
169
|
+
compressed = zstandard.ZstdCompressor().compress(b"x" * 128)
|
|
170
|
+
|
|
171
|
+
with pytest.raises(ValueError, match="output too large"):
|
|
172
|
+
ZstdCompression().decompress(compressed, max_output=64)
|
|
173
|
+
|
|
174
|
+
|
|
131
175
|
def test_lz4_decompresses_literals_and_rejects_invalid_blocks() -> None:
|
|
132
176
|
compression = Lz4BlockCompression()
|
|
133
177
|
|