maxapi-python 1.2.1__tar.gz → 1.2.3__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 (68) hide show
  1. maxapi_python-1.2.3/.github/workflows/tests.yml +153 -0
  2. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/.gitignore +1 -1
  3. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/PKG-INFO +43 -6
  4. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/README.md +35 -5
  5. maxapi_python-1.2.3/examples/example.py +74 -0
  6. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/pyproject.toml +53 -1
  7. maxapi_python-1.2.3/pytest.ini +26 -0
  8. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/redocs/source/clients.rst +22 -0
  9. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/core.py +2 -1
  10. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/files.py +5 -0
  11. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/interfaces.py +1 -0
  12. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/mixins/group.py +31 -12
  13. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/mixins/self.py +74 -9
  14. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/mixins/socket.py +10 -0
  15. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/mixins/websocket.py +16 -16
  16. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/payloads.py +32 -14
  17. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/static/constant.py +1 -1
  18. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/static/enum.py +1 -0
  19. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/types.py +57 -28
  20. maxapi_python-1.2.1/examples/example.py +0 -268
  21. maxapi_python-1.2.1/examples/flt_test.py +0 -51
  22. maxapi_python-1.2.1/examples/large_file_upload.py +0 -51
  23. maxapi_python-1.2.1/examples/reg.py +0 -34
  24. maxapi_python-1.2.1/examples/test.py +0 -20
  25. maxapi_python-1.2.1/pytest.ini +0 -6
  26. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/.coderabbit.yaml +0 -0
  27. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/.github/FUNDING.yml +0 -0
  28. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  29. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  30. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/.github/ISSUE_TEMPLATE/refactor.md +0 -0
  31. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/.github/pull_request_template.md +0 -0
  32. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/.github/workflows/publish.yml +0 -0
  33. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/.pre-commit-config.yaml +0 -0
  34. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/LICENSE +0 -0
  35. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/assets/icon.svg +0 -0
  36. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/assets/logo.svg +0 -0
  37. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/examples/telegram_bridge.py +0 -0
  38. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/mkdocs.yml +0 -0
  39. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/redocs/Makefile +0 -0
  40. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/redocs/build.sh +0 -0
  41. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/redocs/make.bat +0 -0
  42. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/redocs/source/_static/logo.svg +0 -0
  43. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/redocs/source/conf.py +0 -0
  44. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/redocs/source/decorators.rst +0 -0
  45. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/redocs/source/examples.rst +0 -0
  46. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/redocs/source/guides.rst +0 -0
  47. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/redocs/source/index.rst +0 -0
  48. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/redocs/source/installation.rst +0 -0
  49. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/redocs/source/quickstart.rst +0 -0
  50. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/redocs/source/types.rst +0 -0
  51. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/ruff.toml +0 -0
  52. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/__init__.py +0 -0
  53. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/crud.py +0 -0
  54. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/exceptions.py +0 -0
  55. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/filters.py +0 -0
  56. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/formatter.py +0 -0
  57. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/formatting.py +0 -0
  58. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/mixins/__init__.py +0 -0
  59. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/mixins/auth.py +0 -0
  60. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/mixins/channel.py +0 -0
  61. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/mixins/handler.py +0 -0
  62. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/mixins/message.py +0 -0
  63. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/mixins/scheduler.py +0 -0
  64. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/mixins/telemetry.py +0 -0
  65. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/mixins/user.py +0 -0
  66. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/mixins/utils.py +0 -0
  67. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/models.py +0 -0
  68. {maxapi_python-1.2.1 → maxapi_python-1.2.3}/src/pymax/navigation.py +0 -0
@@ -0,0 +1,153 @@
1
+ name: Tests
2
+
3
+ #on:
4
+ # push:
5
+ # branches: [ main, develop ]
6
+ # pull_request:
7
+ # branches: [ main, develop ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ['3.10', '3.11', '3.12', '3.13']
16
+
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Set up Python ${{ matrix.python-version }}
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+
26
+ - name: Cache pip packages
27
+ uses: actions/cache@v4
28
+ with:
29
+ path: ~/.cache/pip
30
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
31
+ restore-keys: |
32
+ ${{ runner.os }}-pip-
33
+
34
+ - name: Install runtime + test tools
35
+ run: |
36
+ python -m pip install --upgrade pip
37
+ # Устанавливаем package (runtime deps)
38
+ pip install -e "."
39
+ # Явно ставим инструменты для тестов/линта, чтобы они были доступны в runner
40
+ pip install pytest pytest-asyncio pytest-cov pytest-timeout flake8 mypy
41
+
42
+ - name: Lint with flake8
43
+ run: |
44
+ flake8 src/pymax tests \
45
+ --count \
46
+ --select=E9,F63,F7,F82 \
47
+ --show-source \
48
+ --statistics
49
+ flake8 src/pymax tests \
50
+ --count \
51
+ --exit-zero \
52
+ --max-complexity=10 \
53
+ --max-line-length=79 \
54
+ --statistics
55
+ continue-on-error: true
56
+
57
+ - name: Type check with mypy
58
+ run: |
59
+ mypy src/pymax \
60
+ --ignore-missing-imports \
61
+ --no-error-summary
62
+ continue-on-error: true
63
+
64
+ - name: Run unit tests
65
+ run: |
66
+ pytest -m "not mockserver" \
67
+ --cov=src/pymax \
68
+ --cov-report=xml \
69
+ --cov-report=term-missing
70
+
71
+ - name: Upload coverage to Codecov
72
+ uses: codecov/codecov-action@v4
73
+ with:
74
+ files: coverage.xml
75
+ flags: unittests
76
+ name: codecov-umbrella
77
+ fail_ci_if_error: false
78
+
79
+ - name: Archive pytest cache
80
+ if: always()
81
+ uses: actions/upload-artifact@v4
82
+ with:
83
+ name: pytest-cache-${{ matrix.python-version }}
84
+ path: .pytest_cache/
85
+ retention-days: 5
86
+
87
+ integration-tests:
88
+ runs-on: ubuntu-latest
89
+
90
+ steps:
91
+ - name: Checkout repository (with submodules)
92
+ uses: actions/checkout@v4
93
+ with:
94
+ submodules: recursive
95
+
96
+ - name: Set up Python
97
+ uses: actions/setup-python@v5
98
+ with:
99
+ python-version: '3.13'
100
+
101
+ - name: Install runtime + test tools (integration)
102
+ run: |
103
+ python -m pip install --upgrade pip
104
+ pip install -e "."
105
+ pip install pytest pytest-asyncio pytest-cov pytest-timeout flake8 mypy
106
+
107
+ - name: Set up Go
108
+ uses: actions/setup-go@v5
109
+ with:
110
+ go-version: '1.22'
111
+
112
+ - name: Start MockServer
113
+ run: |
114
+ git clone https://github.com/fresh-milkshake/gomax-prerelease.git
115
+ cd gomax-prerelease/mockserver
116
+ go mod download
117
+ go run cmd/server/main.go &
118
+ sleep 3
119
+
120
+ - name: Run integration tests
121
+ run: |
122
+ pytest -m mockserver -v --tb=short
123
+ continue-on-error: true
124
+ env:
125
+ MOCKSERVER_WS_URL: ws://localhost:8080/
126
+ MOCKSERVER_HTTP_URL: http://localhost:8080
127
+
128
+ code-quality:
129
+ runs-on: ubuntu-latest
130
+
131
+ steps:
132
+ - name: Checkout repository
133
+ uses: actions/checkout@v4
134
+
135
+ - name: Set up Python
136
+ uses: actions/setup-python@v5
137
+ with:
138
+ python-version: '3.13'
139
+
140
+ - name: Install dependencies + quality tools
141
+ run: |
142
+ python -m pip install --upgrade pip
143
+ pip install -e "."
144
+ # black/isort/pylint используются только в этом job
145
+ pip install black isort pylint
146
+
147
+ - name: Check code formatting with black
148
+ run: black --check src/pymax tests
149
+ continue-on-error: true
150
+
151
+ - name: Check import sorting with isort
152
+ run: isort --check-only src/pymax tests
153
+ continue-on-error: true
@@ -116,7 +116,7 @@ cache/
116
116
  # Keep lockfiles and important configs tracked? If you want to track specific lockfiles,
117
117
  # remove them from this .gitignore (for example: remove poetry.lock or uv.lock).
118
118
  tests2/
119
- tests/
119
+ tests
120
120
 
121
121
 
122
122
  # Bad dev's requirements
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxapi-python
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: Python wrapper для API мессенджера Max
5
5
  Project-URL: Homepage, https://github.com/ink-developer/PyMax
6
6
  Project-URL: Repository, https://github.com/ink-developer/PyMax
@@ -19,6 +19,13 @@ Requires-Dist: msgpack>=1.1.1
19
19
  Requires-Dist: qrcode>=8.2
20
20
  Requires-Dist: sqlmodel>=0.0.24
21
21
  Requires-Dist: websockets>=15.0
22
+ Provides-Extra: test
23
+ Requires-Dist: flake8; extra == 'test'
24
+ Requires-Dist: mypy; extra == 'test'
25
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == 'test'
26
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'test'
27
+ Requires-Dist: pytest-timeout>=2.1.0; extra == 'test'
28
+ Requires-Dist: pytest>=8.0.0; extra == 'test'
22
29
  Description-Content-Type: text/markdown
23
30
 
24
31
  <p align="center">
@@ -36,11 +43,6 @@ Description-Content-Type: text/markdown
36
43
  <img src="https://img.shields.io/badge/packaging-uv-D7FF64.svg" alt="Packaging">
37
44
  </p>
38
45
 
39
- > [!IMPORTANT]
40
- > (20.12.25) Из за резкого изменения апи большая часть библиотеки не работает.
41
- Смотрите [новость](https://t.me/pymax_news/111)
42
- >
43
- > P.s добавил логин по qr в dev/1.2.1
44
46
 
45
47
  ---
46
48
  > ⚠️ **Дисклеймер**
@@ -82,6 +84,41 @@ uv add -U maxapi-python
82
84
 
83
85
  ## Быстрый старт
84
86
 
87
+ ### Аутентификация (`device_type`)
88
+
89
+ > [!IMPORTANT]
90
+ > Параметр `device_type` в `UserAgentPayload` **критически важен** для выбора способа авторизации:
91
+
92
+ **Вход по номеру телефона (DESKTOP):**
93
+
94
+ ```python
95
+ from pymax import MaxClient
96
+ from pymax.payloads import UserAgentPayload
97
+
98
+ ua = UserAgentPayload(device_type="DESKTOP", app_version="25.12.13")
99
+
100
+ client = MaxClient(
101
+ phone="+79111111111",
102
+ work_dir="cache",
103
+ headers=ua,
104
+ )
105
+ ```
106
+
107
+ **Вход через QR-код (WEB)** — токен совместим с веб-версией Max:
108
+
109
+ ```python
110
+ from pymax import MaxClient
111
+ from pymax.payloads import UserAgentPayload
112
+
113
+ ua = UserAgentPayload(device_type="WEB", app_version="25.12.13")
114
+
115
+ client = MaxClient(
116
+ phone="+7911111111",
117
+ work_dir="cache",
118
+ headers=ua,
119
+ )
120
+ ```
121
+
85
122
  ### Базовый пример использования
86
123
 
87
124
  ```python
@@ -13,11 +13,6 @@
13
13
  <img src="https://img.shields.io/badge/packaging-uv-D7FF64.svg" alt="Packaging">
14
14
  </p>
15
15
 
16
- > [!IMPORTANT]
17
- > (20.12.25) Из за резкого изменения апи большая часть библиотеки не работает.
18
- Смотрите [новость](https://t.me/pymax_news/111)
19
- >
20
- > P.s добавил логин по qr в dev/1.2.1
21
16
 
22
17
  ---
23
18
  > ⚠️ **Дисклеймер**
@@ -59,6 +54,41 @@ uv add -U maxapi-python
59
54
 
60
55
  ## Быстрый старт
61
56
 
57
+ ### Аутентификация (`device_type`)
58
+
59
+ > [!IMPORTANT]
60
+ > Параметр `device_type` в `UserAgentPayload` **критически важен** для выбора способа авторизации:
61
+
62
+ **Вход по номеру телефона (DESKTOP):**
63
+
64
+ ```python
65
+ from pymax import MaxClient
66
+ from pymax.payloads import UserAgentPayload
67
+
68
+ ua = UserAgentPayload(device_type="DESKTOP", app_version="25.12.13")
69
+
70
+ client = MaxClient(
71
+ phone="+79111111111",
72
+ work_dir="cache",
73
+ headers=ua,
74
+ )
75
+ ```
76
+
77
+ **Вход через QR-код (WEB)** — токен совместим с веб-версией Max:
78
+
79
+ ```python
80
+ from pymax import MaxClient
81
+ from pymax.payloads import UserAgentPayload
82
+
83
+ ua = UserAgentPayload(device_type="WEB", app_version="25.12.13")
84
+
85
+ client = MaxClient(
86
+ phone="+7911111111",
87
+ work_dir="cache",
88
+ headers=ua,
89
+ )
90
+ ```
91
+
62
92
  ### Базовый пример использования
63
93
 
64
94
  ```python
@@ -0,0 +1,74 @@
1
+ import asyncio
2
+ import datetime
3
+ import logging
4
+ from time import time
5
+ from typing import Any
6
+
7
+ import pymax
8
+ from pymax import MaxClient, Message, ReactionInfo, SocketMaxClient, filters
9
+ from pymax.files import File, Photo, Video
10
+ from pymax.filters import Filters
11
+ from pymax.payloads import UserAgentPayload
12
+ from pymax.static.enum import AttachType, Opcode
13
+ from pymax.types import Chat
14
+
15
+ phone = "+79991234567"
16
+ headers = UserAgentPayload(device_type="WEB")
17
+
18
+ client = MaxClient(
19
+ phone=phone,
20
+ work_dir="cache",
21
+ reconnect=False,
22
+ logger=None,
23
+ headers=headers,
24
+ )
25
+ client.logger.setLevel(logging.INFO)
26
+
27
+
28
+ @client.on_start
29
+ async def handle_start() -> None:
30
+ print(f"Client started as {client.me.names[0].first_name}!")
31
+
32
+
33
+ @client.on_raw_receive
34
+ async def handle_raw_receive(data: dict[str, Any]) -> None:
35
+ print(f"Raw data received: {data}")
36
+
37
+
38
+ @client.on_reaction_change
39
+ async def handle_reaction_change(
40
+ message_id: str, chat_id: int, reaction_info: ReactionInfo
41
+ ) -> None:
42
+ print(
43
+ f"Reaction changed on message {message_id} in chat {chat_id}: "
44
+ f"Total count: {reaction_info.total_count}, "
45
+ f"Your reaction: {reaction_info.your_reaction}, "
46
+ f"Counters: {reaction_info.counters[0].reaction}={reaction_info.counters[0].count}"
47
+ )
48
+
49
+
50
+ @client.on_chat_update
51
+ async def handle_chat_update(chat: Chat) -> None:
52
+ print(f"Chat updated: {chat.id}, new title: {chat.title}")
53
+
54
+
55
+ @client.on_message(Filters.chat(0) & Filters.text("hello"))
56
+ async def handle_message(message: Message) -> None:
57
+ print(f"New message in chat {message.chat_id} from {message.sender}: {message.text}")
58
+
59
+
60
+ @client.on_message_edit()
61
+ async def handle_edited_message(message: Message) -> None:
62
+ print(f"Edited message in chat {message.chat_id}: {message.text}")
63
+
64
+
65
+ @client.on_message_delete()
66
+ async def handle_deleted_message(message: Message) -> None:
67
+ print(f"Deleted message in chat {message.chat_id}: {message.id}")
68
+
69
+
70
+ if __name__ == "__main__":
71
+ try:
72
+ asyncio.run(client.start())
73
+ except KeyboardInterrupt:
74
+ print("Client stopped by user")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "maxapi-python"
3
- version = "1.2.1"
3
+ version = "1.2.3"
4
4
  description = "Python wrapper для API мессенджера Max"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -36,7 +36,25 @@ where = ["src"]
36
36
  [tool.setuptools.package-dir]
37
37
  "" = "src"
38
38
 
39
+ [project.optional-dependencies]
40
+ test = [
41
+ "pytest>=8.0.0",
42
+ "pytest-asyncio>=0.24.0",
43
+ "pytest-cov>=5.0.0",
44
+ "pytest-timeout>=2.1.0",
45
+ "flake8",
46
+ "mypy",
47
+ ]
48
+
39
49
  [dependency-groups]
50
+ test = [
51
+ "pytest>=8.0.0",
52
+ "pytest-asyncio>=0.24.0",
53
+ "pytest-cov>=5.0.0",
54
+ "pytest-timeout>=2.1.0",
55
+ "flake8",
56
+ "mypy",
57
+ ]
40
58
  dev = [
41
59
  "furo>=2025.9.25",
42
60
  "ghp-import>=2.1.0",
@@ -46,6 +64,10 @@ dev = [
46
64
  "pre-commit>=4.3.0",
47
65
  "pydocstring>=0.2.1",
48
66
  "sphinx>=8.1.3",
67
+ "pytest>=8.0.0",
68
+ "pytest-asyncio>=0.24.0",
69
+ "pytest-cov>=5.0.0",
70
+ "pytest-timeout>=2.1.0",
49
71
  ]
50
72
 
51
73
  [tool.hatch.build.targets.wheel]
@@ -74,3 +96,33 @@ profile = "black"
74
96
  line_length = 79
75
97
  multi_line_output = 3
76
98
  include_trailing_comma = true
99
+
100
+ [tool.pytest.ini_options]
101
+ asyncio_mode = "auto"
102
+ testpaths = ["tests"]
103
+ python_files = ["test_*.py"]
104
+ python_classes = ["Test*"]
105
+ python_functions = ["test_*"]
106
+ addopts = "-v --tb=short --strict-markers"
107
+ markers = [
108
+ "asyncio: marker for asyncio tests",
109
+ "mockserver: marker for MockServer integration tests",
110
+ "integration: marker for integration tests",
111
+ "slow: marker for slow tests",
112
+ ]
113
+
114
+ [tool.coverage.run]
115
+ source = ["src/pymax"]
116
+ branch = true
117
+
118
+ [tool.coverage.report]
119
+ exclude_lines = [
120
+ "pragma: no cover",
121
+ "def __repr__",
122
+ "raise AssertionError",
123
+ "raise NotImplementedError",
124
+ "if __name__ == .__main__.:",
125
+ "if TYPE_CHECKING:",
126
+ "class .*\\bProtocol\\):",
127
+ "@(abc\\.)?abstractmethod",
128
+ ]
@@ -0,0 +1,26 @@
1
+ [pytest]
2
+
3
+ asyncio_mode = auto
4
+
5
+ testpaths = tests
6
+
7
+ python_files = test_*.py
8
+ python_classes = Test*
9
+ python_functions = test_*
10
+
11
+ addopts =
12
+ -v
13
+ --tb=short
14
+ --strict-markers
15
+ -ra
16
+ --color=yes
17
+
18
+ markers =
19
+ asyncio: асинхронные тесты
20
+ mockserver: интеграционные тесты с MockServer
21
+ integration: интеграционные тесты
22
+ slow: медленные тесты
23
+ unit: модульные тесты
24
+ skip_ci: пропустить в CI
25
+
26
+ timeout = 30
@@ -20,6 +20,28 @@ MaxClient
20
20
  logger=None, # Пользовательский логгер
21
21
  )
22
22
 
23
+ .. warning::
24
+
25
+ Параметр ``device_type`` в ``UserAgentPayload`` **критически важен** для выбора способа авторизации:
26
+
27
+ **DESKTOP** — вход по номеру телефона:
28
+
29
+ .. code-block:: python
30
+
31
+ from pymax.payloads import UserAgentPayload
32
+
33
+ ua = UserAgentPayload(device_type="DESKTOP", app_version="25.12.13")
34
+ client = MaxClient(phone="+79111111111", headers=ua)
35
+
36
+ **WEB** — вход через QR-код; токен совместим с веб-версией Max:
37
+
38
+ .. code-block:: python
39
+
40
+ from pymax.payloads import UserAgentPayload
41
+
42
+ ua = UserAgentPayload(device_type="WEB", app_version="25.12.13")
43
+ client = MaxClient(phone="+79111111111", headers=ua)
44
+
23
45
  Основные методы:
24
46
 
25
47
  .. code-block:: python
@@ -116,6 +116,7 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
116
116
  self.dialogs: list[Dialog] = []
117
117
  self.channels: list[Channel] = []
118
118
  self.me: Me | None = None
119
+ self.contacts: list[User] = []
119
120
  self._users: dict[int, User] = {}
120
121
 
121
122
  self._work_dir: str = work_dir
@@ -295,7 +296,7 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
295
296
  if self._token is None:
296
297
  await self._login()
297
298
 
298
- await self._sync()
299
+ await self._sync(self.user_agent)
299
300
 
300
301
  await self._post_login_tasks(sync=False)
301
302
 
@@ -46,6 +46,11 @@ class Photo(BaseFile):
46
46
  } # FIXME: костыль ✅
47
47
 
48
48
  def __init__(self, url: str | None = None, path: str | None = None) -> None:
49
+ if path:
50
+ self.file_name = Path(path).name
51
+ elif url:
52
+ self.file_name = Path(url).name
53
+
49
54
  super().__init__(url, path)
50
55
 
51
56
  def validate_photo(self) -> tuple[str, str] | None:
@@ -45,6 +45,7 @@ class ClientProtocol(ABC):
45
45
  self.phone: str
46
46
  self.dialogs: list[Dialog] = []
47
47
  self.channels: list[Channel] = []
48
+ self.contacts: list[User] = []
48
49
  self.me: Me | None = None
49
50
  self.host: str
50
51
  self.port: int
@@ -95,9 +95,7 @@ class GroupMixin(ClientProtocol):
95
95
  operation="add",
96
96
  ).model_dump(by_alias=True)
97
97
 
98
- data = await self._send_and_wait(
99
- opcode=Opcode.CHAT_MEMBERS_UPDATE, payload=payload
100
- )
98
+ data = await self._send_and_wait(opcode=Opcode.CHAT_MEMBERS_UPDATE, payload=payload)
101
99
 
102
100
  if data.get("payload", {}).get("error"):
103
101
  MixinsUtils.handle_error(data)
@@ -155,9 +153,7 @@ class GroupMixin(ClientProtocol):
155
153
  clean_msg_period=clean_msg_period,
156
154
  ).model_dump(by_alias=True)
157
155
 
158
- data = await self._send_and_wait(
159
- opcode=Opcode.CHAT_MEMBERS_UPDATE, payload=payload
160
- )
156
+ data = await self._send_and_wait(opcode=Opcode.CHAT_MEMBERS_UPDATE, payload=payload)
161
157
 
162
158
  if data.get("payload", {}).get("error"):
163
159
  MixinsUtils.handle_error(data)
@@ -293,6 +289,33 @@ class GroupMixin(ClientProtocol):
293
289
 
294
290
  return chat
295
291
 
292
+ async def resolve_group_by_link(self, link: str) -> Chat | None:
293
+ """
294
+ Разрешает группу по ссылке
295
+
296
+ Args:
297
+ link (str): Ссылка на группу.
298
+
299
+ Returns:
300
+ Chat | None: Объект чата группы или None, если не найдено.
301
+ """
302
+ proceed_link = self._process_chat_join_link(link)
303
+ if proceed_link is None:
304
+ raise ValueError("Invalid group link")
305
+
306
+ data = await self._send_and_wait(
307
+ opcode=Opcode.LINK_INFO,
308
+ payload={
309
+ "link": proceed_link,
310
+ },
311
+ )
312
+
313
+ if data.get("payload", {}).get("error"):
314
+ MixinsUtils.handle_error(data)
315
+
316
+ chat = Chat.from_dict(data["payload"].get("chat", {}))
317
+ return chat
318
+
296
319
  async def rework_invite_link(self, chat_id: int) -> Chat:
297
320
  """
298
321
  Пересоздает ссылку для приглашения в группу
@@ -329,14 +352,10 @@ class GroupMixin(ClientProtocol):
329
352
  chat_id for chat_id in chat_ids if await self._get_chat(chat_id) is None
330
353
  ]
331
354
  if missed_chat_ids:
332
- payload = GetChatInfoPayload(chat_ids=missed_chat_ids).model_dump(
333
- by_alias=True
334
- )
355
+ payload = GetChatInfoPayload(chat_ids=missed_chat_ids).model_dump(by_alias=True)
335
356
  else:
336
357
  chats: list[Chat] = [
337
- chat
338
- for chat_id in chat_ids
339
- if (chat := await self._get_chat(chat_id)) is not None
358
+ chat for chat_id in chat_ids if (chat := await self._get_chat(chat_id)) is not None
340
359
  ]
341
360
  return chats
342
361