maxapi-python 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxapi-python
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Python wrapper для API мессенджера Max
5
5
  Project-URL: Homepage, https://github.com/noxzion/PyMax
6
6
  Project-URL: Repository, https://github.com/noxzion/PyMax
7
7
  Project-URL: Issues, https://github.com/noxzion/PyMax/issues
8
- Author-email: noxzion <negroid2281488ilikrilex@gmail.com>
8
+ Author-email: noxzion <mail@gmail.com>
9
9
  License-Expression: MIT
10
10
  License-File: LICENSE
11
11
  Keywords: api,max,messenger,websocket,wrapper
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Requires-Python: >=3.10
15
+ Requires-Dist: aiofiles>=24.1.0
16
+ Requires-Dist: aiohttp>=3.12.15
17
+ Requires-Dist: lz4>=4.4.4
18
+ Requires-Dist: msgpack>=1.1.1
15
19
  Requires-Dist: sqlmodel>=0.0.24
16
20
  Requires-Dist: websockets>=11.0
17
21
  Description-Content-Type: text/markdown
@@ -31,6 +35,14 @@ Description-Content-Type: text/markdown
31
35
  <img src="https://img.shields.io/badge/packaging-uv-D7FF64.svg" alt="Packaging">
32
36
  </p>
33
37
 
38
+ ---
39
+ > ⚠️ **Дисклеймер**
40
+ >
41
+ > * Это **неофициальная** библиотека для работы с внутренним API Max.
42
+ > * Использование может **нарушать условия предоставления услуг** сервиса.
43
+ > * **Вы используете её исключительно на свой страх и риск.**
44
+ > * **Разработчики и контрибьюторы не несут никакой ответственности** за любые последствия использования этого пакета, включая, но не ограничиваясь: блокировку аккаунтов, утерю данных, юридические риски и любые другие проблемы.
45
+ > * API может быть изменен в любой момент без предупреждения.
34
46
  ---
35
47
 
36
48
  ## Описание
@@ -74,7 +86,7 @@ phone = "+1234567890"
74
86
  client = MaxClient(phone=phone, work_dir="cache")
75
87
 
76
88
  # Обработчик входящих сообщений
77
- @client.on_message
89
+ @client.on_message()
78
90
  async def handle_message(message: Message) -> None:
79
91
  print(f"{message.sender}: {message.text}")
80
92
 
@@ -133,12 +145,16 @@ if __name__ == "__main__":
133
145
 
134
146
  ## Документация
135
147
 
136
- WIP
148
+ [WIP](https://noxzion.github.io/)
137
149
 
138
150
  ## Лицензия
139
151
 
140
152
  Этот проект распространяется под лицензией MIT. См. файл [LICENSE](LICENSE) для получения информации.
141
153
 
154
+ ## Новости
155
+
156
+ [Telegram](https://t.me/pymax_news)
157
+
142
158
  ## Авторы
143
159
 
144
160
  - **[noxzion](https://github.com/noxzion)** — оригинальный автор проекта
@@ -0,0 +1,25 @@
1
+ pymax/__init__.py,sha256=I-ZUVKBfHN-MPkLUbLPxflsvnHSFsyXdW3TmbN2_zz0,950
2
+ pymax/core.py,sha256=DjwhOXmMoPl2pT9k18lrZLjlyvzPqkby6hIbPEYlctM,6001
3
+ pymax/crud.py,sha256=_V88YErPKE4k0jPjGn8Op9FDGGVuNwRjuKN-4iIVdgM,3306
4
+ pymax/exceptions.py,sha256=tiD_JD-MYSb4qFyKov-KWOm0zlD1p_gG6nf1fV-0-SY,702
5
+ pymax/files.py,sha256=Bdvzb8xzMCstRHClldj6bh6TE-AplHG3hInjOnGU6f8,2712
6
+ pymax/filters.py,sha256=EKNDzB3jjxlfjMGs3HivFvuQ1HSuYn-xKCxvmRFmq6c,1344
7
+ pymax/interfaces.py,sha256=xAlzVgS4RLChlChZ6WbKLWGezO_b0fHC_2NBePMtyVI,2179
8
+ pymax/models.py,sha256=7sWAmVuJjM7SPnDkpYEi8CARbTpUKbXqtWKMQdwd0w0,209
9
+ pymax/payloads.py,sha256=Pp4vc3BwKzqqWpIce_NhlNw9FA7i6Jhd-hRPg1Ld8Xg,3702
10
+ pymax/static.py,sha256=LQBXSWpiA3a4IWGqC84jTt7LBHSKEqnLDlvgSiMrX_M,4891
11
+ pymax/types.py,sha256=Fsf7_8rkUnCZ7zG_A9P4WJG8wfY8YxZcc-mRLEL3kU0,14140
12
+ pymax/utils.py,sha256=2QnhUb1o-vAIthtodtfrxty9v-qd96OtMd2GMG0n5h4,1402
13
+ pymax/mixins/__init__.py,sha256=yFhB18-PMhn5yIbvIh_XBCTIdkf87M1hmsmMwL9ved0,371
14
+ pymax/mixins/auth.py,sha256=eIlJ5YY7ez99nQuICTu97XWoeKgcjk3ZVhb73qbAsMw,3160
15
+ pymax/mixins/channel.py,sha256=juLIojxsmTXstYfE9pVc98gZ9VSVZJy-chLSDOxay0A,848
16
+ pymax/mixins/group.py,sha256=5MH2S78Q0JxcECEopGPi80QjcTO5bQNOwYLgRx3Gkt4,8040
17
+ pymax/mixins/handler.py,sha256=S6zGnm6U9yNY7LU1MV0TsYQj3MXkeOhiuUGlXyM2kTw,2164
18
+ pymax/mixins/message.py,sha256=pm4YFRmmYdldnk3G4rfjwAbodY-TfeIIvFHez6Xse_o,10542
19
+ pymax/mixins/self.py,sha256=Z1FiWFXJf3Qu9mKTQGrobe01bx4opa-uH5p6wedRWbI,1207
20
+ pymax/mixins/user.py,sha256=V8HLhLMopxItWfc4BF9P3eFjF8ksGAepkqaXrJ1e0ZE,3127
21
+ pymax/mixins/websocket.py,sha256=o7ITyScJNcoBP93dg9itLfGWl1oHmJy31K42j5sUTJQ,9736
22
+ maxapi_python-0.1.2.dist-info/METADATA,sha256=jWYrhQ_-auSt6H_q5vS5OrH6QeQvQ_heeIu-sGSBsas,5930
23
+ maxapi_python-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
24
+ maxapi_python-0.1.2.dist-info/licenses/LICENSE,sha256=Ud-0SKeXO_yA02Bb1nMDnEaSGwz2OqNlfGQbk0IzqPI,1085
25
+ maxapi_python-0.1.2.dist-info/RECORD,,
pymax/__init__.py ADDED
@@ -0,0 +1,55 @@
1
+ """
2
+ Python wrapper для API мессенджера Max
3
+ """
4
+
5
+ from .core import (
6
+ InvalidPhoneError,
7
+ MaxClient,
8
+ WebSocketNotConnectedError,
9
+ )
10
+ from .static import (
11
+ AccessType,
12
+ AuthType,
13
+ ChatType,
14
+ Constants,
15
+ DeviceType,
16
+ ElementType,
17
+ MessageStatus,
18
+ MessageType,
19
+ Opcode,
20
+ )
21
+ from .types import (
22
+ Channel,
23
+ Chat,
24
+ Dialog,
25
+ Element,
26
+ Message,
27
+ User,
28
+ )
29
+
30
+ __author__ = "noxzion"
31
+
32
+ __all__ = [
33
+ # Перечисления и константы
34
+ "AccessType",
35
+ "AuthType",
36
+ # Типы данных
37
+ "Channel",
38
+ "Chat",
39
+ "ChatType",
40
+ "Constants",
41
+ "DeviceType",
42
+ "Dialog",
43
+ "Element",
44
+ "ElementType",
45
+ # Исключения
46
+ "InvalidPhoneError",
47
+ # Клиент
48
+ "MaxClient",
49
+ "Message",
50
+ "MessageStatus",
51
+ "MessageType",
52
+ "Opcode",
53
+ "User",
54
+ "WebSocketNotConnectedError",
55
+ ]
pymax/core.py ADDED
@@ -0,0 +1,156 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import re
5
+ import time
6
+ from collections.abc import Awaitable, Callable
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ import websockets
11
+
12
+ from .crud import Database
13
+ from .exceptions import InvalidPhoneError, WebSocketNotConnectedError
14
+ from .mixins import ApiMixin, WebSocketMixin
15
+ from .payloads import (
16
+ BaseWebSocketMessage,
17
+ SyncPayload,
18
+ )
19
+ from .static import ChatType, Constants, Opcode
20
+ from .types import Channel, Chat, Dialog, Me, Message, User, override
21
+
22
+ if TYPE_CHECKING:
23
+ from .filters import Filter
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class MaxClient(ApiMixin, WebSocketMixin):
29
+ """
30
+ Основной клиент для работы с WebSocket API сервиса Max.
31
+
32
+
33
+ Args:
34
+ phone (str): Номер телефона для авторизации.
35
+ uri (str, optional): URI WebSocket сервера. По умолчанию Constants.WEBSOCKET_URI.value.
36
+ work_dir (str, optional): Рабочая директория для хранения базы данных. По умолчанию ".".
37
+ logger (logging.Logger | None): Пользовательский логгер. Если не передан — используется
38
+ логгер модуля с именем f"{__name__}.MaxClient".
39
+
40
+ Raises:
41
+ InvalidPhoneError: Если формат номера телефона неверный.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ phone: str,
47
+ uri: str = Constants.WEBSOCKET_URI.value,
48
+ headers: dict[str, Any] | None = Constants.DEFAULT_USER_AGENT.value,
49
+ token: str | None = None,
50
+ work_dir: str = ".",
51
+ logger: logging.Logger | None = None,
52
+ ) -> None:
53
+ self.uri: str = uri
54
+ self.is_connected: bool = False
55
+ self.phone: str = phone
56
+ self.chats: list[Chat] = []
57
+ self.dialogs: list[Dialog] = []
58
+ self.channels: list[Channel] = []
59
+ self.me: Me | None = None
60
+ self._users: dict[int, User] = {}
61
+ if not self._check_phone():
62
+ raise InvalidPhoneError(self.phone)
63
+ self._work_dir: str = work_dir
64
+ self._database_path: Path = Path(work_dir) / "session.db"
65
+ self._database_path.parent.mkdir(parents=True, exist_ok=True)
66
+ self._database_path.touch(exist_ok=True)
67
+ self._database = Database(self._work_dir)
68
+ self._ws: websockets.ClientConnection | None = None
69
+ self._seq: int = 0
70
+ self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
71
+ self._recv_task: asyncio.Task[Any] | None = None
72
+ self._incoming: asyncio.Queue[dict[str, Any]] | None = None
73
+ self._device_id = self._database.get_device_id()
74
+ self._token = self._database.get_auth_token() or token
75
+ self.user_agent = headers
76
+ self._on_message_handlers: list[
77
+ tuple[Callable[[Message], Any], Filter | None]
78
+ ] = []
79
+ self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
80
+ self._background_tasks: set[asyncio.Task[Any]] = set()
81
+ self.logger = logger or logging.getLogger(f"{__name__}.MaxClient")
82
+ self._setup_logger()
83
+
84
+ self.logger.debug(
85
+ "Initialized MaxClient uri=%s work_dir=%s", self.uri, self._work_dir
86
+ )
87
+
88
+ def _setup_logger(self) -> None:
89
+ self.logger.setLevel(logging.INFO)
90
+
91
+ if not logger.handlers:
92
+ handler = logging.StreamHandler()
93
+ formatter = logging.Formatter(
94
+ "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
95
+ )
96
+ handler.setFormatter(formatter)
97
+ logger.addHandler(handler)
98
+
99
+ async def close(self) -> None:
100
+ try:
101
+ self.logger.info("Closing client")
102
+ if self._recv_task:
103
+ self._recv_task.cancel()
104
+ try:
105
+ await self._recv_task
106
+ except asyncio.CancelledError:
107
+ self.logger.debug("recv_task cancelled")
108
+ if self._ws:
109
+ await self._ws.close()
110
+ self.is_connected = False
111
+ self.logger.info("Client closed")
112
+ except Exception:
113
+ self.logger.exception("Error closing client")
114
+
115
+ async def start(self) -> None:
116
+ """
117
+ Запускает клиент, подключается к WebSocket, авторизует
118
+ пользователя (если нужно) и запускает фоновый цикл.
119
+ """
120
+ try:
121
+ self.logger.info("Client starting")
122
+ await self._connect(self.user_agent)
123
+
124
+ if self._token and self._database.get_auth_token() is None:
125
+ self._database.update_auth_token(self._device_id, self._token)
126
+
127
+ if self._token is None:
128
+ await self._login()
129
+ else:
130
+ await self._sync()
131
+
132
+ if self._on_start_handler:
133
+ self.logger.debug("Calling on_start handler")
134
+ result = self._on_start_handler()
135
+ if asyncio.iscoroutine(result):
136
+ await result
137
+
138
+ if self._ws:
139
+ ping_task = asyncio.create_task(self._send_interactive_ping())
140
+ self._background_tasks.add(ping_task)
141
+ ping_task.add_done_callback(
142
+ lambda t: self._background_tasks.discard(t)
143
+ or self._log_task_exception(t)
144
+ )
145
+
146
+ try:
147
+ await self._ws.wait_closed()
148
+ except asyncio.CancelledError:
149
+ self.logger.debug("wait_closed cancelled")
150
+ except Exception:
151
+ self.logger.exception("Client start failed")
152
+
153
+
154
+ class SocketMaxClient:
155
+ pass # нокс займись
156
+ # нет не займусь
pymax/crud.py ADDED
@@ -0,0 +1,99 @@
1
+ from uuid import UUID
2
+
3
+ from sqlalchemy.engine.base import Engine
4
+ from sqlmodel import Session, SQLModel, create_engine, select
5
+
6
+ from .models import Auth
7
+ from .static import DeviceType
8
+
9
+
10
+ class Database:
11
+ def __init__(self, workdir: str) -> None:
12
+ self.workdir = workdir
13
+ self.engine = self.get_engine(workdir)
14
+ self.create_all()
15
+ self._ensure_single_auth()
16
+
17
+ self.workdir = workdir
18
+ self.engine = self.get_engine(workdir)
19
+ self.create_all()
20
+ self._ensure_single_auth()
21
+
22
+ def create_all(self) -> None:
23
+ SQLModel.metadata.create_all(self.engine)
24
+
25
+ def get_engine(self, workdir: str) -> Engine:
26
+ return create_engine(f"sqlite:///{workdir}/session.db")
27
+
28
+ def get_session(self) -> Session:
29
+ return Session(bind=self.engine)
30
+
31
+ def get_auth_token(self) -> str | None:
32
+ with self.get_session() as session:
33
+ return session.exec(select(Auth.token)).first()
34
+
35
+ def get_device_id(self) -> UUID:
36
+ with self.get_session() as session:
37
+ device_id = session.exec(select(Auth.device_id)).first()
38
+ if device_id is None:
39
+ auth = Auth()
40
+ session.add(auth)
41
+ session.commit()
42
+ session.refresh(auth)
43
+ return auth.device_id
44
+ return device_id
45
+
46
+ def insert_auth(self, auth: Auth) -> Auth:
47
+ with self.get_session() as session:
48
+ session.add(auth)
49
+ session.commit()
50
+ session.refresh(auth)
51
+ return auth
52
+
53
+ def update_auth_token(self, device_id: UUID, token: str) -> None:
54
+ with self.get_session() as session:
55
+ auth = session.exec(select(Auth).where(Auth.device_id == device_id)).first()
56
+ if auth:
57
+ auth.token = token
58
+ session.add(auth)
59
+ session.commit()
60
+ session.refresh(auth)
61
+ return
62
+
63
+ existing = session.exec(select(Auth)).first()
64
+ if existing:
65
+ existing.device_id = device_id
66
+ existing.token = token
67
+ session.add(existing)
68
+ session.commit()
69
+ session.refresh(existing)
70
+ return
71
+
72
+ new_auth = Auth(device_id=device_id, token=token)
73
+ session.add(new_auth)
74
+ session.commit()
75
+ session.refresh(new_auth)
76
+
77
+ def update(self, auth: Auth) -> Auth:
78
+ with self.get_session() as session:
79
+ session.add(auth)
80
+ session.commit()
81
+ session.refresh(auth)
82
+ return auth
83
+
84
+ def _ensure_single_auth(self) -> None:
85
+ with self.get_session() as session:
86
+ rows = session.exec(select(Auth)).all()
87
+ if not rows:
88
+ # Create default Auth with device type from enum
89
+ auth = Auth(device_type=DeviceType.WEB.value)
90
+ session.add(auth)
91
+ session.commit()
92
+ session.refresh(auth)
93
+ return
94
+
95
+ if len(rows) > 1:
96
+ _ = rows[0]
97
+ for extra in rows[1:]:
98
+ session.delete(extra)
99
+ session.commit()
pymax/exceptions.py ADDED
@@ -0,0 +1,20 @@
1
+ class InvalidPhoneError(Exception):
2
+ """
3
+ Исключение, вызываемое при неверном формате номера телефона.
4
+
5
+ Args:
6
+ phone (str): Некорректный номер телефона.
7
+ """
8
+
9
+ def __init__(self, phone: str) -> None:
10
+ super().__init__(f"Invalid phone number format: {phone}")
11
+
12
+
13
+ class WebSocketNotConnectedError(Exception):
14
+ """
15
+ Исключение, вызываемое при попытке обращения к WebSocket,
16
+ если соединение не установлено.
17
+ """
18
+
19
+ def __init__(self) -> None:
20
+ super().__init__("WebSocket is not connected")
pymax/files.py ADDED
@@ -0,0 +1,85 @@
1
+ import mimetypes
2
+ from abc import ABC, abstractmethod
3
+ from pathlib import Path
4
+ from typing import ClassVar, override
5
+
6
+ from aiofiles import open as aio_open
7
+ from aiohttp import ClientSession
8
+
9
+
10
+ class BaseFile(ABC):
11
+ def __init__(self, url: str | None = None, path: str | None = None) -> None:
12
+ self.url = url
13
+ self.path = path
14
+
15
+ if self.url is None and self.path is None:
16
+ raise ValueError("Either url or path must be provided.")
17
+
18
+ if self.url and self.path:
19
+ raise ValueError("Only one of url or path must be provided.")
20
+
21
+ @abstractmethod
22
+ async def read(self) -> bytes:
23
+ if self.url:
24
+ async with ClientSession() as session, session.get(self.url) as response:
25
+ response.raise_for_status()
26
+ return await response.read()
27
+ elif self.path:
28
+ async with aio_open(self.path, "rb") as f:
29
+ return await f.read()
30
+ else:
31
+ raise ValueError("Either url or path must be provided.")
32
+
33
+
34
+ class Photo(BaseFile):
35
+ ALLOWED_EXTENSIONS: ClassVar[set[str]] = {
36
+ ".jpg",
37
+ ".jpeg",
38
+ ".png",
39
+ ".gif",
40
+ ".webp",
41
+ ".bmp",
42
+ } # FIXME: костыль ✅
43
+
44
+ def __init__(self, url: str | None = None, path: str | None = None) -> None:
45
+ super().__init__(url, path)
46
+
47
+ def validate_photo(self) -> tuple[str, str] | None:
48
+ if self.path:
49
+ extension = Path(self.path).suffix.lower()
50
+ if extension not in self.ALLOWED_EXTENSIONS:
51
+ raise ValueError(
52
+ f"Invalid photo extension: {extension}. Allowed: {self.ALLOWED_EXTENSIONS}"
53
+ )
54
+
55
+ return (extension[1:], ("image/" + extension[1:]).lower())
56
+ elif self.url:
57
+ extension = Path(self.url).suffix.lower()
58
+ if extension in self.ALLOWED_EXTENSIONS:
59
+ raise ValueError(
60
+ f"Invalid photo extension in URL: {extension}. Allowed: {self.ALLOWED_EXTENSIONS}"
61
+ )
62
+
63
+ mime_type = mimetypes.guess_type(self.url)[0]
64
+
65
+ if not mime_type or not mime_type.startswith("image/"):
66
+ raise ValueError(f"URL does not appear to be an image: {self.url}")
67
+
68
+ return (extension[1:], mime_type)
69
+ return None
70
+
71
+ @override
72
+ async def read(self) -> bytes:
73
+ return await super().read()
74
+
75
+
76
+ class Video(BaseFile):
77
+ @override
78
+ async def read(self) -> bytes:
79
+ return await super().read()
80
+
81
+
82
+ class File(BaseFile):
83
+ @override
84
+ async def read(self) -> bytes:
85
+ return await super().read()
pymax/filters.py ADDED
@@ -0,0 +1,38 @@
1
+ from .static import MessageStatus, MessageType
2
+ from .types import Message
3
+
4
+
5
+ class Filter:
6
+ def __init__(
7
+ self,
8
+ user_id: int | None = None,
9
+ text: list[str] | None = None,
10
+ status: MessageStatus | str | None = None,
11
+ type: MessageType | str | None = None,
12
+ text_contains: str | None = None,
13
+ reaction_info: bool | None = None,
14
+ ) -> None:
15
+ self.user_id = user_id
16
+ self.text = text
17
+ self.status = status
18
+ self.type = type
19
+ self.reaction_info = reaction_info
20
+ self.text_contains = text_contains
21
+
22
+ def match(self, message: Message) -> bool:
23
+ if self.user_id is not None and message.sender != self.user_id:
24
+ return False
25
+ if self.text is not None and any(
26
+ text not in message.text for text in self.text
27
+ ):
28
+ return False
29
+ if self.text_contains is not None and self.text_contains not in message.text:
30
+ return False
31
+ if self.status is not None and message.status != self.status:
32
+ return False
33
+ if self.type is not None and message.type != self.type:
34
+ return False
35
+ if self.reaction_info is not None and message.reactionInfo is None: # noqa: SIM103
36
+ return False
37
+
38
+ return True
pymax/interfaces.py ADDED
@@ -0,0 +1,67 @@
1
+ import asyncio
2
+ import logging
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Awaitable, Callable
5
+ from logging import Logger
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ import websockets
10
+
11
+ from .filters import Filter
12
+ from .static import Constants
13
+ from .types import Channel, Chat, Dialog, Me, Message, User
14
+
15
+ if TYPE_CHECKING:
16
+ from uuid import UUID
17
+
18
+ from .crud import Database
19
+
20
+
21
+ class ClientProtocol(ABC):
22
+ def __init__(self, logger: Logger) -> None:
23
+ super().__init__()
24
+ self.logger = logger
25
+ self._users: dict[int, User] = {}
26
+ self.chats: list[Chat] = []
27
+ self.phone: str = ""
28
+ self._database: Database
29
+ self._device_id: UUID
30
+ self._on_message_handlers: list[
31
+ tuple[Callable[[Message], Any], Filter | None]
32
+ ] = []
33
+ self.uri: str
34
+ self.is_connected: bool = False
35
+ self.phone: str
36
+ self.chats: list[Chat] = []
37
+ self.dialogs: list[Dialog] = []
38
+ self.channels: list[Channel] = []
39
+ self.me: Me | None = None
40
+ self._users: dict[int, User] = {}
41
+ self._work_dir: str
42
+ self._database_path: Path
43
+ self._ws: websockets.ClientConnection | None = None
44
+ self._seq: int = 0
45
+ self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
46
+ self._recv_task: asyncio.Task[Any] | None = None
47
+ self._incoming: asyncio.Queue[dict[str, Any]] | None = None
48
+ self.user_agent = Constants.DEFAULT_USER_AGENT.value
49
+ self._on_message_handlers: list[
50
+ tuple[Callable[[Message], Any], Filter | None]
51
+ ] = []
52
+ self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
53
+ self._background_tasks: set[asyncio.Task[Any]] = set()
54
+
55
+ @abstractmethod
56
+ async def _send_and_wait(
57
+ self,
58
+ opcode: int,
59
+ payload: dict[str, Any],
60
+ cmd: int = 0,
61
+ timeout: float = Constants.DEFAULT_TIMEOUT.value,
62
+ ) -> dict[str, Any]:
63
+ pass
64
+
65
+ @abstractmethod
66
+ async def _get_chat(self, chat_id: int) -> Chat | None:
67
+ pass
@@ -0,0 +1,18 @@
1
+ from .auth import AuthMixin
2
+ from .channel import ChannelMixin
3
+ from .handler import HandlerMixin
4
+ from .message import MessageMixin
5
+ from .self import SelfMixin
6
+ from .user import UserMixin
7
+ from .websocket import WebSocketMixin
8
+
9
+
10
+ class ApiMixin(
11
+ AuthMixin,
12
+ HandlerMixin,
13
+ UserMixin,
14
+ ChannelMixin,
15
+ SelfMixin,
16
+ MessageMixin,
17
+ ):
18
+ pass