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.
- {maxapi_python-0.1.1.dist-info → maxapi_python-0.1.2.dist-info}/METADATA +20 -4
- maxapi_python-0.1.2.dist-info/RECORD +25 -0
- pymax/__init__.py +55 -0
- pymax/core.py +156 -0
- pymax/crud.py +99 -0
- pymax/exceptions.py +20 -0
- pymax/files.py +85 -0
- pymax/filters.py +38 -0
- pymax/interfaces.py +67 -0
- pymax/mixins/__init__.py +18 -0
- pymax/mixins/auth.py +81 -0
- pymax/mixins/channel.py +25 -0
- pymax/mixins/group.py +220 -0
- pymax/mixins/handler.py +60 -0
- pymax/mixins/message.py +293 -0
- pymax/mixins/self.py +38 -0
- pymax/mixins/user.py +82 -0
- pymax/mixins/websocket.py +242 -0
- pymax/models.py +8 -0
- pymax/payloads.py +175 -0
- pymax/static.py +210 -0
- pymax/types.py +432 -0
- pymax/utils.py +38 -0
- maxapi_python-0.1.1.dist-info/RECORD +0 -4
- {maxapi_python-0.1.1.dist-info → maxapi_python-0.1.2.dist-info}/WHEEL +0 -0
- {maxapi_python-0.1.1.dist-info → maxapi_python-0.1.2.dist-info}/licenses/LICENSE +0 -0
pymax/mixins/self.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
from pymax.interfaces import ClientProtocol
|
2
|
+
from pymax.payloads import ChangeProfilePayload
|
3
|
+
from pymax.static import Opcode
|
4
|
+
|
5
|
+
|
6
|
+
class SelfMixin(ClientProtocol):
|
7
|
+
async def change_profile(
|
8
|
+
self,
|
9
|
+
first_name: str,
|
10
|
+
last_name: str | None = None,
|
11
|
+
description: str | None = None,
|
12
|
+
) -> bool:
|
13
|
+
"""
|
14
|
+
Изменяет профиль
|
15
|
+
|
16
|
+
Args:
|
17
|
+
first_name (str): Имя.
|
18
|
+
last_name (str | None, optional): Фамилия. Defaults to None.
|
19
|
+
description (str | None, optional): Описание. Defaults to None.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
bool: True, если профиль изменен
|
23
|
+
"""
|
24
|
+
|
25
|
+
payload = ChangeProfilePayload(
|
26
|
+
first_name=first_name,
|
27
|
+
last_name=last_name,
|
28
|
+
description=description,
|
29
|
+
).model_dump(
|
30
|
+
by_alias=True,
|
31
|
+
exclude_none=True,
|
32
|
+
)
|
33
|
+
|
34
|
+
data = await self._send_and_wait(opcode=Opcode.PROFILE, payload=payload)
|
35
|
+
if error := data.get("payload", {}).get("error"):
|
36
|
+
self.logger.error("Change profile error: %s", error)
|
37
|
+
return False
|
38
|
+
return True
|
pymax/mixins/user.py
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
from pymax.interfaces import ClientProtocol
|
2
|
+
from pymax.payloads import FetchContactsPayload
|
3
|
+
from pymax.static import Opcode
|
4
|
+
from pymax.types import User
|
5
|
+
|
6
|
+
|
7
|
+
class UserMixin(ClientProtocol):
|
8
|
+
def get_cached_user(self, user_id: int) -> User | None:
|
9
|
+
"""
|
10
|
+
Получает юзера из кеша по его ID
|
11
|
+
|
12
|
+
Args:
|
13
|
+
user_id (int): ID пользователя.
|
14
|
+
|
15
|
+
Returns:
|
16
|
+
User | None: Объект User или None при ошибке.
|
17
|
+
"""
|
18
|
+
user = self._users.get(user_id)
|
19
|
+
self.logger.debug("get_cached_user id=%s hit=%s", user_id, bool(user))
|
20
|
+
return user
|
21
|
+
|
22
|
+
async def get_users(self, user_ids: list[int]) -> list[User]:
|
23
|
+
"""
|
24
|
+
Получает информацию о пользователях по их ID (с кешем).
|
25
|
+
"""
|
26
|
+
self.logger.debug("get_users ids=%s", user_ids)
|
27
|
+
cached = {uid: self._users[uid] for uid in user_ids if uid in self._users}
|
28
|
+
missing_ids = [uid for uid in user_ids if uid not in self._users]
|
29
|
+
|
30
|
+
if missing_ids:
|
31
|
+
self.logger.debug("Fetching missing users: %s", missing_ids)
|
32
|
+
fetched_users = await self.fetch_users(missing_ids)
|
33
|
+
if fetched_users:
|
34
|
+
for user in fetched_users:
|
35
|
+
self._users[user.id] = user
|
36
|
+
cached[user.id] = user
|
37
|
+
|
38
|
+
ordered = [cached[uid] for uid in user_ids if uid in cached]
|
39
|
+
self.logger.debug("get_users result_count=%d", len(ordered))
|
40
|
+
return ordered
|
41
|
+
|
42
|
+
async def get_user(self, user_id: int) -> User | None:
|
43
|
+
"""
|
44
|
+
Получает информацию о пользователе по его ID (с кешем).
|
45
|
+
"""
|
46
|
+
self.logger.debug("get_user id=%s", user_id)
|
47
|
+
if user_id in self._users:
|
48
|
+
return self._users[user_id]
|
49
|
+
|
50
|
+
users = await self.fetch_users([user_id])
|
51
|
+
if users:
|
52
|
+
self._users[user_id] = users[0]
|
53
|
+
return users[0]
|
54
|
+
return None
|
55
|
+
|
56
|
+
async def fetch_users(self, user_ids: list[int]) -> None | list[User]:
|
57
|
+
"""
|
58
|
+
Получает информацию о пользователях по их ID.
|
59
|
+
"""
|
60
|
+
try:
|
61
|
+
self.logger.info("Fetching users count=%d", len(user_ids))
|
62
|
+
|
63
|
+
payload = FetchContactsPayload(contact_ids=user_ids).model_dump(
|
64
|
+
by_alias=True
|
65
|
+
)
|
66
|
+
|
67
|
+
data = await self._send_and_wait(
|
68
|
+
opcode=Opcode.CONTACT_INFO, payload=payload
|
69
|
+
)
|
70
|
+
if error := data.get("payload", {}).get("error"):
|
71
|
+
self.logger.error("Fetch users error: %s", error)
|
72
|
+
return None
|
73
|
+
|
74
|
+
users = [User.from_dict(u) for u in data["payload"].get("contacts", [])]
|
75
|
+
for user in users:
|
76
|
+
self._users[user.id] = user
|
77
|
+
|
78
|
+
self.logger.debug("Fetched users: %d", len(users))
|
79
|
+
return users
|
80
|
+
except Exception:
|
81
|
+
self.logger.exception("Fetch users failed")
|
82
|
+
return []
|
@@ -0,0 +1,242 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
from typing import Any, override
|
4
|
+
|
5
|
+
import websockets
|
6
|
+
|
7
|
+
from pymax.exceptions import WebSocketNotConnectedError
|
8
|
+
from pymax.interfaces import ClientProtocol
|
9
|
+
from pymax.payloads import BaseWebSocketMessage, SyncPayload
|
10
|
+
from pymax.static import ChatType, Constants, Opcode
|
11
|
+
from pymax.types import Channel, Chat, Dialog, Me, Message
|
12
|
+
|
13
|
+
|
14
|
+
class WebSocketMixin(ClientProtocol):
|
15
|
+
@property
|
16
|
+
def ws(self) -> websockets.ClientConnection:
|
17
|
+
if self._ws is None or not self.is_connected:
|
18
|
+
self.logger.critical("WebSocket not connected when access attempted")
|
19
|
+
raise WebSocketNotConnectedError
|
20
|
+
return self._ws
|
21
|
+
|
22
|
+
def _make_message(
|
23
|
+
self, opcode: int, payload: dict[str, Any], cmd: int = 0
|
24
|
+
) -> dict[str, Any]:
|
25
|
+
self._seq += 1
|
26
|
+
|
27
|
+
msg = BaseWebSocketMessage(
|
28
|
+
ver=11,
|
29
|
+
cmd=cmd,
|
30
|
+
seq=self._seq,
|
31
|
+
opcode=opcode,
|
32
|
+
payload=payload,
|
33
|
+
).model_dump(by_alias=True)
|
34
|
+
|
35
|
+
self.logger.debug(
|
36
|
+
"make_message opcode=%s cmd=%s seq=%s", opcode, cmd, self._seq
|
37
|
+
)
|
38
|
+
return msg
|
39
|
+
|
40
|
+
async def _send_interactive_ping(self) -> None:
|
41
|
+
while self.is_connected:
|
42
|
+
try:
|
43
|
+
await self._send_and_wait(
|
44
|
+
opcode=1,
|
45
|
+
payload={"interactive": True},
|
46
|
+
cmd=0,
|
47
|
+
)
|
48
|
+
self.logger.debug("Interactive ping sent successfully")
|
49
|
+
except Exception:
|
50
|
+
self.logger.warning("Interactive ping failed", exc_info=True)
|
51
|
+
await asyncio.sleep(30)
|
52
|
+
|
53
|
+
async def _connect(self, user_agent: dict[str, Any]) -> dict[str, Any]:
|
54
|
+
try:
|
55
|
+
self.logger.info("Connecting to WebSocket %s", self.uri)
|
56
|
+
self._ws = await websockets.connect(self.uri, origin="https://web.max.ru")
|
57
|
+
self.is_connected = True
|
58
|
+
self._incoming = asyncio.Queue()
|
59
|
+
self._pending = {}
|
60
|
+
self._recv_task = asyncio.create_task(self._recv_loop())
|
61
|
+
self.logger.info("WebSocket connected, starting handshake")
|
62
|
+
return await self._handshake(user_agent)
|
63
|
+
except Exception as e:
|
64
|
+
self.logger.error("Failed to connect: %s", e, exc_info=True)
|
65
|
+
raise ConnectionError(f"Failed to connect: {e}")
|
66
|
+
|
67
|
+
async def _handshake(self, user_agent: dict[str, Any]) -> dict[str, Any]:
|
68
|
+
try:
|
69
|
+
self.logger.debug(
|
70
|
+
"Sending handshake with user_agent keys=%s", list(user_agent.keys())
|
71
|
+
)
|
72
|
+
resp = await self._send_and_wait(
|
73
|
+
opcode=Opcode.SESSION_INIT,
|
74
|
+
payload={"deviceId": str(self._device_id), "userAgent": user_agent},
|
75
|
+
)
|
76
|
+
self.logger.info("Handshake completed")
|
77
|
+
return resp
|
78
|
+
except Exception as e:
|
79
|
+
self.logger.error("Handshake failed: %s", e, exc_info=True)
|
80
|
+
raise ConnectionError(f"Handshake failed: {e}")
|
81
|
+
|
82
|
+
async def _recv_loop(self) -> None:
|
83
|
+
if self._ws is None:
|
84
|
+
self.logger.warning("Recv loop started without websocket instance")
|
85
|
+
return
|
86
|
+
|
87
|
+
self.logger.debug("Receive loop started")
|
88
|
+
while True:
|
89
|
+
try:
|
90
|
+
raw = await self._ws.recv()
|
91
|
+
try:
|
92
|
+
data = json.loads(raw)
|
93
|
+
except Exception:
|
94
|
+
self.logger.warning("JSON parse error", exc_info=True)
|
95
|
+
continue
|
96
|
+
|
97
|
+
seq = data.get("seq")
|
98
|
+
fut = self._pending.get(seq) if isinstance(seq, int) else None
|
99
|
+
|
100
|
+
if fut and not fut.done():
|
101
|
+
fut.set_result(data)
|
102
|
+
self.logger.debug("Matched response for pending seq=%s", seq)
|
103
|
+
else:
|
104
|
+
if self._incoming is not None:
|
105
|
+
try:
|
106
|
+
self._incoming.put_nowait(data)
|
107
|
+
except asyncio.QueueFull:
|
108
|
+
self.logger.warning(
|
109
|
+
"Incoming queue full; dropping message seq=%s",
|
110
|
+
data.get("seq"),
|
111
|
+
)
|
112
|
+
|
113
|
+
if (
|
114
|
+
data.get("opcode") == Opcode.NOTIF_MESSAGE
|
115
|
+
and self._on_message_handlers
|
116
|
+
):
|
117
|
+
try:
|
118
|
+
for handler, filter in self._on_message_handlers:
|
119
|
+
payload = data.get("payload", {})
|
120
|
+
msg = Message.from_dict(payload.get("message"))
|
121
|
+
if msg:
|
122
|
+
if msg.status:
|
123
|
+
continue # TODO: заглушка! сделать отдельный хендлер
|
124
|
+
if filter:
|
125
|
+
if filter.match(msg):
|
126
|
+
result = handler(msg)
|
127
|
+
else:
|
128
|
+
continue
|
129
|
+
else:
|
130
|
+
result = handler(msg)
|
131
|
+
if asyncio.iscoroutine(result):
|
132
|
+
task = asyncio.create_task(result)
|
133
|
+
self._background_tasks.add(task)
|
134
|
+
task.add_done_callback(
|
135
|
+
lambda t: self._background_tasks.discard(t)
|
136
|
+
or self._log_task_exception(t)
|
137
|
+
)
|
138
|
+
except Exception:
|
139
|
+
self.logger.exception("Error in on_message_handler")
|
140
|
+
|
141
|
+
except websockets.exceptions.ConnectionClosed:
|
142
|
+
self.logger.info("WebSocket connection closed; exiting recv loop")
|
143
|
+
break
|
144
|
+
except Exception:
|
145
|
+
self.logger.exception("Error in recv_loop; backing off briefly")
|
146
|
+
await asyncio.sleep(0.5)
|
147
|
+
|
148
|
+
def _log_task_exception(self, task: asyncio.Task[Any]) -> None:
|
149
|
+
try:
|
150
|
+
exc = task.exception()
|
151
|
+
if exc:
|
152
|
+
self.logger.exception("Background task exception: %s", exc)
|
153
|
+
except Exception:
|
154
|
+
pass
|
155
|
+
|
156
|
+
@override
|
157
|
+
async def _send_and_wait(
|
158
|
+
self,
|
159
|
+
opcode: int,
|
160
|
+
payload: dict[str, Any],
|
161
|
+
cmd: int = 0,
|
162
|
+
timeout: float = Constants.DEFAULT_TIMEOUT.value,
|
163
|
+
) -> dict[str, Any]:
|
164
|
+
ws = self.ws
|
165
|
+
|
166
|
+
msg = self._make_message(opcode, payload, cmd)
|
167
|
+
loop = asyncio.get_running_loop()
|
168
|
+
fut: asyncio.Future[dict[str, Any]] = loop.create_future()
|
169
|
+
self._pending[msg["seq"]] = fut
|
170
|
+
|
171
|
+
try:
|
172
|
+
self.logger.debug(
|
173
|
+
"Sending frame opcode=%s cmd=%s seq=%s", opcode, cmd, msg["seq"]
|
174
|
+
)
|
175
|
+
await ws.send(json.dumps(msg))
|
176
|
+
data = await asyncio.wait_for(fut, timeout=timeout)
|
177
|
+
self.logger.debug(
|
178
|
+
"Received frame for seq=%s opcode=%s",
|
179
|
+
data.get("seq"),
|
180
|
+
data.get("opcode"),
|
181
|
+
)
|
182
|
+
return data
|
183
|
+
except Exception:
|
184
|
+
self.logger.exception(
|
185
|
+
"Send and wait failed (opcode=%s, seq=%s)", opcode, msg["seq"]
|
186
|
+
)
|
187
|
+
raise RuntimeError("Send and wait failed")
|
188
|
+
finally:
|
189
|
+
self._pending.pop(msg["seq"], None)
|
190
|
+
|
191
|
+
async def _sync(self) -> None:
|
192
|
+
try:
|
193
|
+
self.logger.info("Starting initial sync")
|
194
|
+
|
195
|
+
payload = SyncPayload(
|
196
|
+
interactive=True,
|
197
|
+
token=self._token,
|
198
|
+
chats_sync=0,
|
199
|
+
contacts_sync=0,
|
200
|
+
presence_sync=0,
|
201
|
+
drafts_sync=0,
|
202
|
+
chats_count=40,
|
203
|
+
).model_dump(by_alias=True)
|
204
|
+
|
205
|
+
data = await self._send_and_wait(opcode=19, payload=payload)
|
206
|
+
raw_payload = data.get("payload", {})
|
207
|
+
|
208
|
+
if error := raw_payload.get("error"):
|
209
|
+
self.logger.error("Sync error: %s", error)
|
210
|
+
return
|
211
|
+
|
212
|
+
for raw_chat in raw_payload.get("chats", []):
|
213
|
+
try:
|
214
|
+
if raw_chat.get("type") == ChatType.DIALOG.value:
|
215
|
+
self.dialogs.append(Dialog.from_dict(raw_chat))
|
216
|
+
elif raw_chat.get("type") == ChatType.CHAT.value:
|
217
|
+
self.chats.append(Chat.from_dict(raw_chat))
|
218
|
+
elif raw_chat.get("type") == ChatType.CHANNEL.value:
|
219
|
+
self.channels.append(Channel.from_dict(raw_chat))
|
220
|
+
except Exception:
|
221
|
+
self.logger.exception("Error parsing chat entry")
|
222
|
+
|
223
|
+
if raw_payload.get("profile", {}).get("contact"):
|
224
|
+
self.me = Me.from_dict(
|
225
|
+
raw_payload.get("profile", {}).get("contact", {})
|
226
|
+
)
|
227
|
+
|
228
|
+
self.logger.info(
|
229
|
+
"Sync completed: dialogs=%d chats=%d channels=%d",
|
230
|
+
len(self.dialogs),
|
231
|
+
len(self.chats),
|
232
|
+
len(self.channels),
|
233
|
+
)
|
234
|
+
except Exception:
|
235
|
+
self.logger.exception("Sync failed")
|
236
|
+
|
237
|
+
@override
|
238
|
+
async def _get_chat(self, chat_id: int) -> Chat | None:
|
239
|
+
for chat in self.chats:
|
240
|
+
if chat.id == chat_id:
|
241
|
+
return chat
|
242
|
+
return None
|
pymax/models.py
ADDED
pymax/payloads.py
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
from typing import Any, Literal
|
2
|
+
|
3
|
+
from pydantic import BaseModel, Field
|
4
|
+
|
5
|
+
from pymax.static import AttachType, AuthType
|
6
|
+
|
7
|
+
|
8
|
+
def to_camel(string: str) -> str:
|
9
|
+
parts = string.split("_")
|
10
|
+
return parts[0] + "".join(word.capitalize() for word in parts[1:])
|
11
|
+
|
12
|
+
|
13
|
+
class CamelModel(BaseModel):
|
14
|
+
model_config = {
|
15
|
+
"alias_generator": to_camel,
|
16
|
+
"populate_by_name": True,
|
17
|
+
}
|
18
|
+
|
19
|
+
|
20
|
+
class BaseWebSocketMessage(BaseModel):
|
21
|
+
ver: int = 11
|
22
|
+
cmd: int
|
23
|
+
seq: int
|
24
|
+
opcode: int
|
25
|
+
payload: dict[str, Any]
|
26
|
+
|
27
|
+
|
28
|
+
class RequestCodePayload(CamelModel):
|
29
|
+
phone: str
|
30
|
+
type: AuthType = AuthType.START_AUTH
|
31
|
+
language: str = "ru"
|
32
|
+
|
33
|
+
|
34
|
+
class SendCodePayload(CamelModel):
|
35
|
+
token: str
|
36
|
+
verify_code: str
|
37
|
+
auth_token_type: AuthType = AuthType.CHECK_CODE
|
38
|
+
|
39
|
+
|
40
|
+
class SyncPayload(CamelModel):
|
41
|
+
interactive: bool = True
|
42
|
+
token: str
|
43
|
+
chats_sync: int = 0
|
44
|
+
contacts_sync: int = 0
|
45
|
+
presence_sync: int = 0
|
46
|
+
drafts_sync: int = 0
|
47
|
+
chats_count: int = 40
|
48
|
+
|
49
|
+
|
50
|
+
class ReplyLink(CamelModel):
|
51
|
+
type: str = "REPLY"
|
52
|
+
message_id: str
|
53
|
+
|
54
|
+
|
55
|
+
class UploadPhotoPayload(CamelModel):
|
56
|
+
count: int = 1
|
57
|
+
|
58
|
+
|
59
|
+
class AttachPhotoPayload(CamelModel):
|
60
|
+
type: AttachType = Field(AttachType.PHOTO, alias="_type")
|
61
|
+
photo_token: str
|
62
|
+
|
63
|
+
|
64
|
+
class SendMessagePayloadMessage(CamelModel):
|
65
|
+
text: str
|
66
|
+
cid: int
|
67
|
+
elements: list[Any]
|
68
|
+
attaches: list[dict[str, Any]]
|
69
|
+
link: ReplyLink | None = None
|
70
|
+
|
71
|
+
|
72
|
+
class SendMessagePayload(CamelModel):
|
73
|
+
chat_id: int
|
74
|
+
message: SendMessagePayloadMessage
|
75
|
+
notify: bool = False
|
76
|
+
|
77
|
+
|
78
|
+
class EditMessagePayload(CamelModel):
|
79
|
+
chat_id: int
|
80
|
+
message_id: int
|
81
|
+
text: str
|
82
|
+
elements: list[Any]
|
83
|
+
attaches: list[Any]
|
84
|
+
|
85
|
+
|
86
|
+
class DeleteMessagePayload(CamelModel):
|
87
|
+
chat_id: int
|
88
|
+
message_ids: list[int]
|
89
|
+
for_me: bool = False
|
90
|
+
|
91
|
+
|
92
|
+
class FetchContactsPayload(CamelModel):
|
93
|
+
contact_ids: list[int]
|
94
|
+
|
95
|
+
|
96
|
+
class FetchHistoryPayload(CamelModel):
|
97
|
+
chat_id: int
|
98
|
+
from_time: int = Field(alias="from")
|
99
|
+
forward: int
|
100
|
+
backward: int = 200
|
101
|
+
get_messages: bool = True
|
102
|
+
|
103
|
+
|
104
|
+
class ChangeProfilePayload(CamelModel):
|
105
|
+
first_name: str
|
106
|
+
last_name: str | None = None
|
107
|
+
description: str | None = None
|
108
|
+
|
109
|
+
|
110
|
+
class ResolveLinkPayload(CamelModel):
|
111
|
+
link: str
|
112
|
+
|
113
|
+
|
114
|
+
class PinMessagePayload(CamelModel):
|
115
|
+
chat_id: int
|
116
|
+
notify_pin: bool
|
117
|
+
pin_message_id: int
|
118
|
+
|
119
|
+
|
120
|
+
class CreateGroupAttach(CamelModel):
|
121
|
+
type: Literal["CONTROL"] = Field("CONTROL", alias="_type")
|
122
|
+
event: str = "new"
|
123
|
+
chat_type: str = "CHAT"
|
124
|
+
title: str
|
125
|
+
user_ids: list[int]
|
126
|
+
|
127
|
+
|
128
|
+
class CreateGroupMessage(CamelModel):
|
129
|
+
cid: int
|
130
|
+
attaches: list[CreateGroupAttach]
|
131
|
+
|
132
|
+
|
133
|
+
class CreateGroupPayload(CamelModel):
|
134
|
+
message: CreateGroupMessage
|
135
|
+
notify: bool = True
|
136
|
+
|
137
|
+
|
138
|
+
class InviteUsersPayload(CamelModel):
|
139
|
+
chat_id: int
|
140
|
+
user_ids: list[int]
|
141
|
+
show_history: bool
|
142
|
+
operation: str = "add"
|
143
|
+
|
144
|
+
|
145
|
+
class RemoveUsersPayload(CamelModel):
|
146
|
+
chat_id: int
|
147
|
+
user_ids: list[int]
|
148
|
+
operation: str = "remove"
|
149
|
+
clean_msg_period: int
|
150
|
+
|
151
|
+
|
152
|
+
class ChangeGroupSettingsOptions(BaseModel):
|
153
|
+
ONLY_OWNER_CAN_CHANGE_ICON_TITLE: bool | None
|
154
|
+
ALL_CAN_PIN_MESSAGE: bool | None
|
155
|
+
ONLY_ADMIN_CAN_ADD_MEMBER: bool | None
|
156
|
+
ONLY_ADMIN_CAN_CALL: bool | None
|
157
|
+
MEMBERS_CAN_SEE_PRIVATE_LINK: bool | None
|
158
|
+
|
159
|
+
|
160
|
+
class ChangeGroupSettingsPayload(CamelModel):
|
161
|
+
chat_id: int
|
162
|
+
options: ChangeGroupSettingsOptions
|
163
|
+
|
164
|
+
|
165
|
+
class ChangeGroupProfilePayload(CamelModel):
|
166
|
+
chat_id: int
|
167
|
+
theme: str | None
|
168
|
+
description: str | None
|
169
|
+
|
170
|
+
|
171
|
+
class GetGroupMembersPayload(CamelModel):
|
172
|
+
type: str = "MEMBER"
|
173
|
+
marker: int
|
174
|
+
chat_id: int
|
175
|
+
count: int
|