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.
Files changed (244) hide show
  1. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/PKG-INFO +2 -1
  2. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/client.rst +3 -3
  3. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/index.rst +1 -0
  4. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/messages.rst +12 -0
  5. maxapi_python-2.3.1/docs/release-2-3-1.rst +23 -0
  6. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/pyproject.toml +2 -1
  7. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/__init__.py +1 -1
  8. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/messages/payloads.py +21 -1
  9. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/messages/service.py +39 -0
  10. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/message.py +27 -0
  11. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/tcp/compression.py +18 -0
  12. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/tcp/payload.py +20 -4
  13. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/tcp/protocol.py +5 -1
  14. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/message.py +29 -2
  15. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/user.py +6 -4
  16. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/api/test_message_service.py +42 -0
  17. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/domain/test_bound_models.py +15 -4
  18. maxapi_python-2.3.1/tests/domain/test_user_models.py +35 -0
  19. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/protocol/test_protocols.py +45 -1
  20. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/uv.lock +93 -1
  21. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  22. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  23. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.github/ISSUE_TEMPLATE/refactor.md +0 -0
  24. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.github/pull_request_template.md +0 -0
  25. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.github/workflows/publish.yml +0 -0
  26. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.github/workflows/tests.yml +0 -0
  27. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.gitignore +0 -0
  28. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/.pre-commit-config.yaml +0 -0
  29. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/LICENSE +0 -0
  30. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/README.md +0 -0
  31. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/_static/.gitkeep +0 -0
  32. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/account.rst +0 -0
  33. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/api/auth.rst +0 -0
  34. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/api/client-client.rst +0 -0
  35. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/api/client-config.rst +0 -0
  36. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/api/client-web.rst +0 -0
  37. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/api/client.rst +0 -0
  38. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/api/files.rst +0 -0
  39. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/api/router.rst +0 -0
  40. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/auth.rst +0 -0
  41. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/chats.rst +0 -0
  42. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/conf.py +0 -0
  43. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/examples.rst +0 -0
  44. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/faq.rst +0 -0
  45. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/files.rst +0 -0
  46. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/formatting.rst +0 -0
  47. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/getting-started.rst +0 -0
  48. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/release-2-1-0.rst +0 -0
  49. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/release-2-1-1.rst +0 -0
  50. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/release-2-1-2.rst +0 -0
  51. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/release-2-1-3.rst +0 -0
  52. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/release-2-2-0.rst +0 -0
  53. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/release-2-3-0.rst +0 -0
  54. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/router.rst +0 -0
  55. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/troubleshooting.rst +0 -0
  56. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/audio_attachment.rst +0 -0
  57. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/call_attachment.rst +0 -0
  58. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/chat.rst +0 -0
  59. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/contact_attachment.rst +0 -0
  60. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/contact_info.rst +0 -0
  61. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/control_attachment.rst +0 -0
  62. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/element.rst +0 -0
  63. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/enums.rst +0 -0
  64. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/file_attachment.rst +0 -0
  65. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/folder.rst +0 -0
  66. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/folder_list.rst +0 -0
  67. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/folder_update.rst +0 -0
  68. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/index.rst +0 -0
  69. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/inline_keyboard_attachment.rst +0 -0
  70. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/message.rst +0 -0
  71. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/message_delete_event.rst +0 -0
  72. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/message_read_event.rst +0 -0
  73. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/name.rst +0 -0
  74. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/photo_attachment.rst +0 -0
  75. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/presence_event.rst +0 -0
  76. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/profile.rst +0 -0
  77. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/reaction_counter.rst +0 -0
  78. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/reaction_info.rst +0 -0
  79. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/reaction_update_event.rst +0 -0
  80. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/read_state.rst +0 -0
  81. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/session.rst +0 -0
  82. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/share_attachment.rst +0 -0
  83. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/sticker_attachment.rst +0 -0
  84. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/sync_overrides.rst +0 -0
  85. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/sync_state.rst +0 -0
  86. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/typing_event.rst +0 -0
  87. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/user.rst +0 -0
  88. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/types/video_attachment.rst +0 -0
  89. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/docs/users.rst +0 -0
  90. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/__init__.py +0 -0
  91. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/auth/__init__.py +0 -0
  92. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/auth/enums.py +0 -0
  93. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/auth/payloads.py +0 -0
  94. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/auth/service.py +0 -0
  95. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/auth/types.py +0 -0
  96. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/binding.py +0 -0
  97. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/bots/__init__.py +0 -0
  98. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/bots/payloads.py +0 -0
  99. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/bots/service.py +0 -0
  100. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/chats/__init__.py +0 -0
  101. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/chats/enums.py +0 -0
  102. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/chats/payloads.py +0 -0
  103. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/chats/service.py +0 -0
  104. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/facade.py +0 -0
  105. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/messages/__init__.py +0 -0
  106. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/messages/enums.py +0 -0
  107. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/models.py +0 -0
  108. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/response.py +0 -0
  109. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/self/__init__.py +0 -0
  110. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/self/enums.py +0 -0
  111. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/self/payloads.py +0 -0
  112. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/self/service.py +0 -0
  113. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/session/__init__.py +0 -0
  114. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/session/enums.py +0 -0
  115. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/session/payloads.py +0 -0
  116. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/session/service.py +0 -0
  117. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/uploads/__init__.py +0 -0
  118. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/uploads/models.py +0 -0
  119. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/uploads/payloads.py +0 -0
  120. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/uploads/service.py +0 -0
  121. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/users/__init__.py +0 -0
  122. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/users/enums.py +0 -0
  123. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/users/payloads.py +0 -0
  124. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/api/users/service.py +0 -0
  125. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/app.py +0 -0
  126. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/__init__.py +0 -0
  127. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/base.py +0 -0
  128. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/email.py +0 -0
  129. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/models.py +0 -0
  130. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/providers.py +0 -0
  131. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/qr.py +0 -0
  132. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/service.py +0 -0
  133. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/auth/sms.py +0 -0
  134. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/base.py +0 -0
  135. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/client.py +0 -0
  136. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/client_web.py +0 -0
  137. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/config.py +0 -0
  138. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/connection/__init__.py +0 -0
  139. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/connection/connection.py +0 -0
  140. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/connection/pending.py +0 -0
  141. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/connection/readers/__init__.py +0 -0
  142. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/connection/readers/base.py +0 -0
  143. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/connection/readers/tcp.py +0 -0
  144. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/connection/readers/ws.py +0 -0
  145. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/dispatch/__init__.py +0 -0
  146. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/dispatch/dispatcher.py +0 -0
  147. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/dispatch/enums.py +0 -0
  148. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/dispatch/mapping.py +0 -0
  149. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/dispatch/resolvers.py +0 -0
  150. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/dispatch/router.py +0 -0
  151. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/exceptions.py +0 -0
  152. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/files/__init__.py +0 -0
  153. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/files/base.py +0 -0
  154. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/files/file.py +0 -0
  155. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/files/photo.py +0 -0
  156. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/files/static.py +0 -0
  157. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/files/video.py +0 -0
  158. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/formatting/__init__.py +0 -0
  159. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/formatting/markdown.py +0 -0
  160. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/__init__.py +0 -0
  161. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/auth.py +0 -0
  162. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/base.py +0 -0
  163. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/bots.py +0 -0
  164. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/chat.py +0 -0
  165. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/protocol.py +0 -0
  166. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/self.py +0 -0
  167. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/infra/user.py +0 -0
  168. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/logging.py +0 -0
  169. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/__init__.py +0 -0
  170. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/base.py +0 -0
  171. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/enums.py +0 -0
  172. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/models.py +0 -0
  173. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/tcp/__init__.py +0 -0
  174. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/tcp/framing.py +0 -0
  175. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/ws/__init__.py +0 -0
  176. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/protocol/ws/protocol.py +0 -0
  177. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/py.typed +0 -0
  178. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/routers.py +0 -0
  179. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/session/__init__.py +0 -0
  180. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/session/models.py +0 -0
  181. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/session/protocol.py +0 -0
  182. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/session/store.py +0 -0
  183. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/telemetry/__init__.py +0 -0
  184. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/telemetry/navigation.py +0 -0
  185. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/telemetry/payloads.py +0 -0
  186. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/telemetry/service.py +0 -0
  187. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/transport/__init__.py +0 -0
  188. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/transport/base.py +0 -0
  189. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/transport/tcp.py +0 -0
  190. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/transport/websocket.py +0 -0
  191. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/__init__.py +0 -0
  192. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/__init__.py +0 -0
  193. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/__init__.py +0 -0
  194. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/audio.py +0 -0
  195. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/call.py +0 -0
  196. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/contact.py +0 -0
  197. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/control.py +0 -0
  198. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/enums.py +0 -0
  199. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/file.py +0 -0
  200. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/keyboards/__init__.py +0 -0
  201. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/keyboards/inline.py +0 -0
  202. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/photo.py +0 -0
  203. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/share.py +0 -0
  204. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/sticker.py +0 -0
  205. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/unknown.py +0 -0
  206. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/attachments/video.py +0 -0
  207. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/auth.py +0 -0
  208. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/base.py +0 -0
  209. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/bots.py +0 -0
  210. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/chat.py +0 -0
  211. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/element.py +0 -0
  212. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/enums.py +0 -0
  213. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/error.py +0 -0
  214. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/folder.py +0 -0
  215. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/login.py +0 -0
  216. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/member.py +0 -0
  217. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/name.py +0 -0
  218. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/presence.py +0 -0
  219. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/profile.py +0 -0
  220. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/session.py +0 -0
  221. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/domain/sync.py +0 -0
  222. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/__init__.py +0 -0
  223. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/file.py +0 -0
  224. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/mark.py +0 -0
  225. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/message.py +0 -0
  226. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/presence.py +0 -0
  227. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/reaction.py +0 -0
  228. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/typing.py +0 -0
  229. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/src/pymax/types/events/video.py +0 -0
  230. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/__init__.py +0 -0
  231. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/api/test_auth_service.py +0 -0
  232. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/api/test_chat_user_self_session_services.py +0 -0
  233. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/api/test_upload_service.py +0 -0
  234. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/app/test_app_runtime.py +0 -0
  235. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/auth/test_auth_flows.py +0 -0
  236. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/conftest.py +0 -0
  237. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/connection/test_connection.py +0 -0
  238. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/connection/test_readers_and_transports.py +0 -0
  239. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/dispatch/test_dispatcher.py +0 -0
  240. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/domain/test_message_models.py +0 -0
  241. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/files/test_files_and_formatting.py +0 -0
  242. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/session/test_store.py +0 -0
  243. {maxapi_python-2.3.0 → maxapi_python-2.3.1}/tests/telemetry/test_telemetry.py +0 -0
  244. {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.0
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()``, ``fetch_history()``, ``delete_message()``,
304
- ``pin_message()``, ``read_message()``, реакции и получение URL для входящих
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-ссылки,
@@ -22,6 +22,7 @@ PyMax - асинхронная Python-библиотека для Max API. Он
22
22
  :maxdepth: 1
23
23
  :caption: Новости
24
24
 
25
+ release-2-3-1
25
26
  release-2-3-0
26
27
  release-2-2-0
27
28
  release-2-1-3
@@ -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.0"
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
- __version__ = "2.3.0"
1
+ __version__ = "2.3.1"
2
2
 
3
3
 
4
4
  from .auth import (
@@ -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 & 0x03 and self.compression:
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 flags=%s", flags)
118
+ logger.debug("tcp payload decompressed cof=%s", flags)
104
119
  except ValueError:
105
- logger.debug("tcp payload decompress skipped flags=%s", flags)
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, compression=self.compression
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:`edit`, :meth:`pin`, :meth:`delete`,
97
- :meth:`read`, :meth:`react`, :meth:`unreact` и :meth:`get_reactions`.
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: str | None = None
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]["message_id"] == 10
126
- assert actions.calls[2][2]["attachments"] == ["file"]
127
- assert actions.calls[4][2]["message_ids"] == [10]
128
- assert actions.calls[6][2]["message_id"] == "10"
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