maxapi-python 0.1.3__py3-none-any.whl → 1.1.1__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.
- {maxapi_python-0.1.3.dist-info → maxapi_python-1.1.1.dist-info}/METADATA +1 -1
- maxapi_python-1.1.1.dist-info/RECORD +28 -0
- {maxapi_python-0.1.3.dist-info → maxapi_python-1.1.1.dist-info}/licenses/LICENSE +21 -21
- pymax/__init__.py +57 -55
- pymax/core.py +193 -156
- pymax/crud.py +99 -99
- pymax/exceptions.py +29 -20
- pymax/files.py +86 -86
- pymax/filters.py +38 -38
- pymax/interfaces.py +79 -67
- pymax/mixins/__init__.py +35 -18
- pymax/mixins/auth.py +81 -81
- pymax/mixins/channel.py +25 -25
- pymax/mixins/group.py +220 -220
- pymax/mixins/handler.py +60 -60
- pymax/mixins/message.py +293 -293
- pymax/mixins/self.py +38 -38
- pymax/mixins/socket.py +380 -0
- pymax/mixins/telemetry.py +114 -0
- pymax/mixins/user.py +82 -82
- pymax/mixins/websocket.py +262 -243
- pymax/models.py +8 -8
- pymax/navigation.py +185 -0
- pymax/payloads.py +195 -175
- pymax/static.py +212 -210
- pymax/types.py +570 -434
- pymax/utils.py +38 -38
- maxapi_python-0.1.3.dist-info/RECORD +0 -25
- {maxapi_python-0.1.3.dist-info → maxapi_python-1.1.1.dist-info}/WHEEL +0 -0
pymax/crud.py
CHANGED
@@ -1,99 +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()
|
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
CHANGED
@@ -1,20 +1,29 @@
|
|
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")
|
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")
|
21
|
+
|
22
|
+
|
23
|
+
class LoginError(Exception):
|
24
|
+
"""
|
25
|
+
Исключение, вызываемое при ошибке авторизации.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(self, message: str) -> None:
|
29
|
+
super().__init__(f"Login error: {message}")
|
pymax/files.py
CHANGED
@@ -1,86 +1,86 @@
|
|
1
|
-
import mimetypes
|
2
|
-
from abc import ABC, abstractmethod
|
3
|
-
from pathlib import Path
|
4
|
-
from typing import ClassVar
|
5
|
-
|
6
|
-
from aiofiles import open as aio_open
|
7
|
-
from aiohttp import ClientSession
|
8
|
-
from typing_extensions import override
|
9
|
-
|
10
|
-
|
11
|
-
class BaseFile(ABC):
|
12
|
-
def __init__(self, url: str | None = None, path: str | None = None) -> None:
|
13
|
-
self.url = url
|
14
|
-
self.path = path
|
15
|
-
|
16
|
-
if self.url is None and self.path is None:
|
17
|
-
raise ValueError("Either url or path must be provided.")
|
18
|
-
|
19
|
-
if self.url and self.path:
|
20
|
-
raise ValueError("Only one of url or path must be provided.")
|
21
|
-
|
22
|
-
@abstractmethod
|
23
|
-
async def read(self) -> bytes:
|
24
|
-
if self.url:
|
25
|
-
async with ClientSession() as session, session.get(self.url) as response:
|
26
|
-
response.raise_for_status()
|
27
|
-
return await response.read()
|
28
|
-
elif self.path:
|
29
|
-
async with aio_open(self.path, "rb") as f:
|
30
|
-
return await f.read()
|
31
|
-
else:
|
32
|
-
raise ValueError("Either url or path must be provided.")
|
33
|
-
|
34
|
-
|
35
|
-
class Photo(BaseFile):
|
36
|
-
ALLOWED_EXTENSIONS: ClassVar[set[str]] = {
|
37
|
-
".jpg",
|
38
|
-
".jpeg",
|
39
|
-
".png",
|
40
|
-
".gif",
|
41
|
-
".webp",
|
42
|
-
".bmp",
|
43
|
-
} # FIXME: костыль ✅
|
44
|
-
|
45
|
-
def __init__(self, url: str | None = None, path: str | None = None) -> None:
|
46
|
-
super().__init__(url, path)
|
47
|
-
|
48
|
-
def validate_photo(self) -> tuple[str, str] | None:
|
49
|
-
if self.path:
|
50
|
-
extension = Path(self.path).suffix.lower()
|
51
|
-
if extension not in self.ALLOWED_EXTENSIONS:
|
52
|
-
raise ValueError(
|
53
|
-
f"Invalid photo extension: {extension}. Allowed: {self.ALLOWED_EXTENSIONS}"
|
54
|
-
)
|
55
|
-
|
56
|
-
return (extension[1:], ("image/" + extension[1:]).lower())
|
57
|
-
elif self.url:
|
58
|
-
extension = Path(self.url).suffix.lower()
|
59
|
-
if extension in self.ALLOWED_EXTENSIONS:
|
60
|
-
raise ValueError(
|
61
|
-
f"Invalid photo extension in URL: {extension}. Allowed: {self.ALLOWED_EXTENSIONS}"
|
62
|
-
)
|
63
|
-
|
64
|
-
mime_type = mimetypes.guess_type(self.url)[0]
|
65
|
-
|
66
|
-
if not mime_type or not mime_type.startswith("image/"):
|
67
|
-
raise ValueError(f"URL does not appear to be an image: {self.url}")
|
68
|
-
|
69
|
-
return (extension[1:], mime_type)
|
70
|
-
return None
|
71
|
-
|
72
|
-
@override
|
73
|
-
async def read(self) -> bytes:
|
74
|
-
return await super().read()
|
75
|
-
|
76
|
-
|
77
|
-
class Video(BaseFile):
|
78
|
-
@override
|
79
|
-
async def read(self) -> bytes:
|
80
|
-
return await super().read()
|
81
|
-
|
82
|
-
|
83
|
-
class File(BaseFile):
|
84
|
-
@override
|
85
|
-
async def read(self) -> bytes:
|
86
|
-
return await super().read()
|
1
|
+
import mimetypes
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import ClassVar
|
5
|
+
|
6
|
+
from aiofiles import open as aio_open
|
7
|
+
from aiohttp import ClientSession
|
8
|
+
from typing_extensions import override
|
9
|
+
|
10
|
+
|
11
|
+
class BaseFile(ABC):
|
12
|
+
def __init__(self, url: str | None = None, path: str | None = None) -> None:
|
13
|
+
self.url = url
|
14
|
+
self.path = path
|
15
|
+
|
16
|
+
if self.url is None and self.path is None:
|
17
|
+
raise ValueError("Either url or path must be provided.")
|
18
|
+
|
19
|
+
if self.url and self.path:
|
20
|
+
raise ValueError("Only one of url or path must be provided.")
|
21
|
+
|
22
|
+
@abstractmethod
|
23
|
+
async def read(self) -> bytes:
|
24
|
+
if self.url:
|
25
|
+
async with ClientSession() as session, session.get(self.url) as response:
|
26
|
+
response.raise_for_status()
|
27
|
+
return await response.read()
|
28
|
+
elif self.path:
|
29
|
+
async with aio_open(self.path, "rb") as f:
|
30
|
+
return await f.read()
|
31
|
+
else:
|
32
|
+
raise ValueError("Either url or path must be provided.")
|
33
|
+
|
34
|
+
|
35
|
+
class Photo(BaseFile):
|
36
|
+
ALLOWED_EXTENSIONS: ClassVar[set[str]] = {
|
37
|
+
".jpg",
|
38
|
+
".jpeg",
|
39
|
+
".png",
|
40
|
+
".gif",
|
41
|
+
".webp",
|
42
|
+
".bmp",
|
43
|
+
} # FIXME: костыль ✅
|
44
|
+
|
45
|
+
def __init__(self, url: str | None = None, path: str | None = None) -> None:
|
46
|
+
super().__init__(url, path)
|
47
|
+
|
48
|
+
def validate_photo(self) -> tuple[str, str] | None:
|
49
|
+
if self.path:
|
50
|
+
extension = Path(self.path).suffix.lower()
|
51
|
+
if extension not in self.ALLOWED_EXTENSIONS:
|
52
|
+
raise ValueError(
|
53
|
+
f"Invalid photo extension: {extension}. Allowed: {self.ALLOWED_EXTENSIONS}"
|
54
|
+
)
|
55
|
+
|
56
|
+
return (extension[1:], ("image/" + extension[1:]).lower())
|
57
|
+
elif self.url:
|
58
|
+
extension = Path(self.url).suffix.lower()
|
59
|
+
if extension in self.ALLOWED_EXTENSIONS:
|
60
|
+
raise ValueError(
|
61
|
+
f"Invalid photo extension in URL: {extension}. Allowed: {self.ALLOWED_EXTENSIONS}"
|
62
|
+
)
|
63
|
+
|
64
|
+
mime_type = mimetypes.guess_type(self.url)[0]
|
65
|
+
|
66
|
+
if not mime_type or not mime_type.startswith("image/"):
|
67
|
+
raise ValueError(f"URL does not appear to be an image: {self.url}")
|
68
|
+
|
69
|
+
return (extension[1:], mime_type)
|
70
|
+
return None
|
71
|
+
|
72
|
+
@override
|
73
|
+
async def read(self) -> bytes:
|
74
|
+
return await super().read()
|
75
|
+
|
76
|
+
|
77
|
+
class Video(BaseFile):
|
78
|
+
@override
|
79
|
+
async def read(self) -> bytes:
|
80
|
+
return await super().read()
|
81
|
+
|
82
|
+
|
83
|
+
class File(BaseFile):
|
84
|
+
@override
|
85
|
+
async def read(self) -> bytes:
|
86
|
+
return await super().read()
|
pymax/filters.py
CHANGED
@@ -1,38 +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
|
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
CHANGED
@@ -1,67 +1,79 @@
|
|
1
|
-
import asyncio
|
2
|
-
import logging
|
3
|
-
|
4
|
-
|
5
|
-
from
|
6
|
-
from
|
7
|
-
from
|
8
|
-
|
9
|
-
import
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
from .
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
from
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
self.
|
27
|
-
self.
|
28
|
-
self.
|
29
|
-
self.
|
30
|
-
self.
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
self.
|
36
|
-
|
37
|
-
self.
|
38
|
-
self.
|
39
|
-
self.
|
40
|
-
self.
|
41
|
-
self.
|
42
|
-
self.
|
43
|
-
self.
|
44
|
-
self.
|
45
|
-
self.
|
46
|
-
self.
|
47
|
-
self.
|
48
|
-
self.
|
49
|
-
self.
|
50
|
-
|
51
|
-
] =
|
52
|
-
self.
|
53
|
-
self.
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
self
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
import socket
|
4
|
+
import ssl
|
5
|
+
from abc import ABC, abstractmethod
|
6
|
+
from collections.abc import Awaitable, Callable
|
7
|
+
from logging import Logger
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import TYPE_CHECKING, Any
|
10
|
+
|
11
|
+
import websockets
|
12
|
+
|
13
|
+
from .filters import Filter
|
14
|
+
from .static import Constants
|
15
|
+
from .types import Channel, Chat, Dialog, Me, Message, User
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from uuid import UUID
|
19
|
+
|
20
|
+
from .crud import Database
|
21
|
+
|
22
|
+
|
23
|
+
class ClientProtocol(ABC):
|
24
|
+
def __init__(self, logger: Logger) -> None:
|
25
|
+
super().__init__()
|
26
|
+
self.logger = logger
|
27
|
+
self._users: dict[int, User] = {}
|
28
|
+
self.chats: list[Chat] = []
|
29
|
+
self.phone: str = ""
|
30
|
+
self._database: Database
|
31
|
+
self._device_id: UUID
|
32
|
+
self._on_message_handlers: list[
|
33
|
+
tuple[Callable[[Message], Any], Filter | None]
|
34
|
+
] = []
|
35
|
+
self.uri: str
|
36
|
+
|
37
|
+
self.is_connected: bool = False
|
38
|
+
self.phone: str
|
39
|
+
self.chats: list[Chat] = []
|
40
|
+
self.dialogs: list[Dialog] = []
|
41
|
+
self.channels: list[Channel] = []
|
42
|
+
self.me: Me | None = None
|
43
|
+
self.host: str
|
44
|
+
self.port: int
|
45
|
+
self._users: dict[int, User] = {}
|
46
|
+
self._work_dir: str
|
47
|
+
self._database_path: Path
|
48
|
+
self._ws: websockets.ClientConnection | None = None
|
49
|
+
self._seq: int = 0
|
50
|
+
self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
51
|
+
self._recv_task: asyncio.Task[Any] | None = None
|
52
|
+
self._incoming: asyncio.Queue[dict[str, Any]] | None = None
|
53
|
+
self.user_agent = Constants.DEFAULT_USER_AGENT.value
|
54
|
+
|
55
|
+
self._session_id: int
|
56
|
+
self._action_id: int = 0
|
57
|
+
self._current_screen: str = "chats_list_tab"
|
58
|
+
|
59
|
+
self._on_message_handlers: list[
|
60
|
+
tuple[Callable[[Message], Any], Filter | None]
|
61
|
+
] = []
|
62
|
+
self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
|
63
|
+
self._background_tasks: set[asyncio.Task[Any]] = set()
|
64
|
+
self._ssl_context: ssl.SSLContext
|
65
|
+
self._socket: socket.socket | None = None
|
66
|
+
|
67
|
+
@abstractmethod
|
68
|
+
async def _send_and_wait(
|
69
|
+
self,
|
70
|
+
opcode: int,
|
71
|
+
payload: dict[str, Any],
|
72
|
+
cmd: int = 0,
|
73
|
+
timeout: float = Constants.DEFAULT_TIMEOUT.value,
|
74
|
+
) -> dict[str, Any]:
|
75
|
+
pass
|
76
|
+
|
77
|
+
@abstractmethod
|
78
|
+
async def _get_chat(self, chat_id: int) -> Chat | None:
|
79
|
+
pass
|