From 4dad9c1ed0ff2df74889c8c0e97c59f5f595cd71 Mon Sep 17 00:00:00 2001 From: ink-developer Date: Mon, 22 Dec 2025 21:19:07 +0300 Subject: [PATCH 1/8] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D1=82=D0=B8?= =?UTF-8?q?=D0=BF=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pymax/types.py | 44 ++++++++++++++++---------------------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/src/pymax/types.py b/src/pymax/types.py index 16d4da5..6fc59c8 100644 --- a/src/pymax/types.py +++ b/src/pymax/types.py @@ -97,9 +97,7 @@ def __init__( """ Синоним для класса Name. """ - super().__init__( - name=name, first_name=first_name, last_name=last_name, type=type - ) + super().__init__(name=name, first_name=first_name, last_name=last_name, type=type) class Contact: @@ -219,7 +217,7 @@ class StickerAttach: def __init__( self, author_type: str, - lottie_url: str, + lottie_url: str | None, url: str, sticker_id: int, tags: list[str] | None, @@ -248,7 +246,7 @@ def __init__( def from_dict(cls, data: dict[str, Any]) -> Self: return cls( author_type=data["authorType"], - lottie_url=data["lottieUrl"], + lottie_url=data.get("lottieUrl"), url=data["url"], sticker_id=data["stickerId"], tags=data.get("tags"), @@ -443,9 +441,7 @@ def __str__(self) -> str: class FileAttach: - def __init__( - self, file_id: int, name: str, size: int, token: str, type: AttachType - ) -> None: + def __init__(self, file_id: int, name: str, size: int, token: str, type: AttachType) -> None: self.file_id = file_id self.name = name self.size = size @@ -553,9 +549,7 @@ def __str__(self) -> str: class Element: - def __init__( - self, type: FormattingType | str, length: int, from_: int | None = None - ) -> None: + def __init__(self, type: FormattingType | str, length: int, from_: int | None = None) -> None: self.type = type self.length = length self.from_ = from_ @@ -566,9 +560,7 @@ def from_dict(cls, data: dict[Any, Any]) -> Self: @override def __repr__(self) -> str: - return ( - f"Element(type={self.type!r}, length={self.length!r}, from_={self.from_!r})" - ) + return f"Element(type={self.type!r}, length={self.length!r}, from_={self.from_!r})" @override def __str__(self) -> str: @@ -591,7 +583,9 @@ def from_dict(cls, data: dict[str, Any]) -> Self: @override def __repr__(self) -> str: - return f"MessageLink(chat_id={self.chat_id!r}, message={self.message!r}, type={self.type!r})" + return ( + f"MessageLink(chat_id={self.chat_id!r}, message={self.message!r}, type={self.type!r})" + ) @override def __str__(self) -> str: @@ -678,9 +672,7 @@ def __init__( @classmethod def from_dict(cls, data: dict[Any, Any]) -> Self: message = data["message"] if data.get("message") else data - attaches: list[ - PhotoAttach | VideoAttach | FileAttach | ControlAttach | StickerAttach - ] = [] + attaches: list[PhotoAttach | VideoAttach | FileAttach | ControlAttach | StickerAttach] = [] for a in message.get("attaches", []): if a["_type"] == AttachType.PHOTO: attaches.append(PhotoAttach.from_dict(a)) @@ -778,9 +770,7 @@ def from_dict(cls, data: dict[Any, Any]) -> Self: join_time=data["joinTime"], created=data["created"], last_message=( - Message.from_dict(data["lastMessage"]) - if data.get("lastMessage") - else None + Message.from_dict(data["lastMessage"]) if data.get("lastMessage") else None ), type=ChatType(data["type"]), last_fire_delayed_error_time=data["lastFireDelayedErrorTime"], @@ -865,14 +855,10 @@ def __init__( @classmethod def from_dict(cls, data: dict[Any, Any]) -> Self: raw_admins = data.get("adminParticipants", {}) or {} - admin_participants: dict[int, dict[Any, Any]] = { - int(k): v for k, v in raw_admins.items() - } + admin_participants: dict[int, dict[Any, Any]] = {int(k): v for k, v in raw_admins.items()} raw_participants = data.get("participants", {}) or {} participants: dict[int, int] = {int(k): v for k, v in raw_participants.items()} - last_msg = ( - Message.from_dict(data["lastMessage"]) if data.get("lastMessage") else None - ) + last_msg = Message.from_dict(data["lastMessage"]) if data.get("lastMessage") else None return cls( participants_count=data.get("participantsCount", 0), access=AccessType(data.get("access", AccessType.PUBLIC.value)), @@ -1051,7 +1037,9 @@ def __repr__(self) -> str: @override def __str__(self) -> str: - return f"Session: {self.client} from {self.location} at {self.time} (current={self.current})" + return ( + f"Session: {self.client} from {self.location} at {self.time} (current={self.current})" + ) class Folder: From 1834b41f38cf7ef62a5f334bb49eb9f3a686ada0 Mon Sep 17 00:00:00 2001 From: ink-developer Date: Mon, 22 Dec 2025 21:23:04 +0300 Subject: [PATCH 2/8] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D1=82=D0=B8?= =?UTF-8?q?=D0=BF=D0=BE=D0=B2=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pymax/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymax/types.py b/src/pymax/types.py index 6fc59c8..f6bdec7 100644 --- a/src/pymax/types.py +++ b/src/pymax/types.py @@ -90,7 +90,7 @@ class Names(Name): def __init__( self, name: str | None, - first_name: None, + first_name: None | str, last_name: str | None, type: str | None, ) -> None: From bae44763751a7c9be2ed5d69823d37a363ccb7b2 Mon Sep 17 00:00:00 2001 From: ink-developer Date: Mon, 22 Dec 2025 22:11:07 +0300 Subject: [PATCH 3/8] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D1=83=20=D1=84?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/example.py | 34 ++++------------ src/pymax/files.py | 5 +++ src/pymax/mixins/self.py | 83 +++++++++++++++++++++++++++++++++++----- src/pymax/payloads.py | 3 ++ src/pymax/types.py | 2 +- 5 files changed, 91 insertions(+), 36 deletions(-) diff --git a/examples/example.py b/examples/example.py index cce5611..2706eb9 100644 --- a/examples/example.py +++ b/examples/example.py @@ -6,12 +6,12 @@ import pymax from pymax import MaxClient, Message, ReactionInfo, SocketMaxClient -from pymax.files import File, Video +from pymax.files import File, Photo, Video from pymax.payloads import UserAgentPayload from pymax.static.enum import AttachType, Opcode from pymax.types import Chat -phone = "+7903223111" +phone = "+79291250363" headers = UserAgentPayload(device_type="WEB") client = MaxClient( @@ -24,34 +24,16 @@ client.logger.setLevel(logging.INFO) -@client.on_raw_receive -async def handle_raw_receive(data: dict[str, Any]) -> None: - print(f"Raw data received: {data}") - - -@client.task(seconds=10) -async def periodic_task() -> None: - # print(f"Periodic task executed at {datetime.datetime.now()}") - ... - - @client.on_start async def handle_start() -> None: print(f"Client started as {client.me.names[0].first_name}!") - chat_id = -1 - max_messages = 1000 - messages = [] - from_time = int(time() * 1000) - while len(messages) < max_messages: - r = await client.fetch_history(chat_id=chat_id, from_time=from_time, backward=30) - if not r: - break - from_time = r[0].time - messages.extend(r) - print(f"First message time: {from_time}, id: {r[0].id}, text: {r[0].text}") - print(f"Last message time: {from_time}, id: {r[-1].id}, text: {r[-1].text}") - print(f"Loaded {len(messages)}/{max_messages} messages...") + photo = Photo(path="/tests2/test.png") + + await client.change_profile( + first_name="Dima", + photo=photo, + ) # channel = await client.resolve_channel_by_name("fm92") # if channel: # print(f"Resolved channel by name: {channel.title}, ID: {channel.id}") diff --git a/src/pymax/files.py b/src/pymax/files.py index c0b57a3..f8745d8 100644 --- a/src/pymax/files.py +++ b/src/pymax/files.py @@ -46,6 +46,11 @@ class Photo(BaseFile): } # FIXME: костыль ✅ def __init__(self, url: str | None = None, path: str | None = None) -> None: + if path: + self.file_name = Path(path).name + elif url: + self.file_name = Path(url).name + super().__init__(url, path) def validate_photo(self) -> tuple[str, str] | None: diff --git a/src/pymax/mixins/self.py b/src/pymax/mixins/self.py index 4904178..98c6010 100644 --- a/src/pymax/mixins/self.py +++ b/src/pymax/mixins/self.py @@ -1,7 +1,13 @@ +import urllib.parse +from http import HTTPStatus from typing import Any +from urllib.parse import parse_qs, urlparse from uuid import uuid4 +import aiohttp + from pymax.exceptions import Error +from pymax.files import Photo from pymax.interfaces import ClientProtocol from pymax.mixins.utils import MixinsUtils from pymax.payloads import ( @@ -10,17 +16,60 @@ DeleteFolderPayload, GetFolderPayload, UpdateFolderPayload, + UploadPayload, ) from pymax.static.enum import Opcode -from pymax.types import Folder, FolderList, FolderUpdate +from pymax.types import Folder, FolderList, FolderUpdate, Me class SelfMixin(ClientProtocol): + async def _request_photo_upload_url(self) -> str: + self.logger.info("Requesting profile photo upload URL") + + data = await self._send_and_wait( + opcode=Opcode.PHOTO_UPLOAD, + payload=UploadPayload(profile=True).model_dump(by_alias=True), + ) + + if data.get("payload", {}).get("error"): + MixinsUtils.handle_error(data) + + return data["payload"]["url"] + + async def _upload_profile_photo(self, upload_url: str, photo: Photo) -> str: + self.logger.info("Uploading profile photo") + + parsed_url = urlparse(upload_url) + photo_id = parse_qs(parsed_url.query)["photoIds"][0] + + form = aiohttp.FormData() + form.add_field( + "file", + await photo.read(), + filename=photo.file_name, + ) + + async with ( + aiohttp.ClientSession() as session, + session.post(upload_url, data=form) as response, + ): + if response.status != HTTPStatus.OK: + raise Error( + "Failed to upload profile photo.", message="UploadError", title="Upload Error" + ) + + self.logger.info("Upload successful") + data = await response.json() + return data["photos"][photo_id][ + "token" + ] # TODO: сделать нормальную типизацию и чекнинг ответа + async def change_profile( self, first_name: str, last_name: str | None = None, description: str | None = None, + photo: Photo | None = None, ) -> bool: """ Изменяет информацию профиля текущего пользователя. @@ -35,20 +84,36 @@ async def change_profile( :rtype: bool """ - payload = ChangeProfilePayload( - first_name=first_name, - last_name=last_name, - description=description, - ).model_dump( - by_alias=True, - exclude_none=True, - ) + if photo: + upload_url = await self._request_photo_upload_url() + photo_token = await self._upload_profile_photo(upload_url, photo) + + payload = ChangeProfilePayload( + first_name=first_name, + last_name=last_name, + description=description, + photo_token=photo_token, + ).model_dump( + by_alias=True, + exclude_none=True, + ) + else: + payload = ChangeProfilePayload( + first_name=first_name, + last_name=last_name, + description=description, + ).model_dump( + by_alias=True, + exclude_none=True, + ) data = await self._send_and_wait(opcode=Opcode.PROFILE, payload=payload) if data.get("payload", {}).get("error"): MixinsUtils.handle_error(data) + self.me = Me.from_dict(data["payload"]["profile"]["contact"]) + return True async def create_folder( diff --git a/src/pymax/payloads.py b/src/pymax/payloads.py index 4f31f09..9a27eeb 100644 --- a/src/pymax/payloads.py +++ b/src/pymax/payloads.py @@ -97,6 +97,7 @@ class ReplyLink(CamelModel): class UploadPayload(CamelModel): count: int = 1 + profile: bool = False class AttachPhotoPayload(CamelModel): @@ -168,6 +169,8 @@ class ChangeProfilePayload(CamelModel): first_name: str last_name: str | None = None description: str | None = None + photo_token: str | None = None + avatar_type: str = "USER_AVATAR" # TODO: вынести гада в энам class ResolveLinkPayload(CamelModel): diff --git a/src/pymax/types.py b/src/pymax/types.py index f6bdec7..2907028 100644 --- a/src/pymax/types.py +++ b/src/pymax/types.py @@ -47,7 +47,7 @@ class Name: def __init__( self, name: str | None, - first_name: None, + first_name: None | str, last_name: str | None, type: str | None, ) -> None: From c6ab809c991ffb9ea42136ca418964f1819ac267 Mon Sep 17 00:00:00 2001 From: ink-developer Date: Tue, 23 Dec 2025 16:30:11 +0300 Subject: [PATCH 4/8] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20=D0=B4=D0=BB=D1=8F=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D0=B8=20=D0=B8=D0=BD?= =?UTF-8?q?=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BE=20?= =?UTF-8?q?=D1=87=D0=B0=D1=82=D0=B5=20=D0=BF=D0=BE=20=D1=81=D1=81=D1=8B?= =?UTF-8?q?=D0=BB=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/example.py | 20 +++++++++++++----- src/pymax/mixins/group.py | 43 ++++++++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/examples/example.py b/examples/example.py index 2706eb9..2de5dde 100644 --- a/examples/example.py +++ b/examples/example.py @@ -28,12 +28,22 @@ async def handle_start() -> None: print(f"Client started as {client.me.names[0].first_name}!") - photo = Photo(path="/tests2/test.png") - - await client.change_profile( - first_name="Dima", - photo=photo, + """ + case e.chat instanceof at: + return na({ link: D, linkType: "CHANNEL" }); + case e.chat instanceof Ct: + return na({ link: D, linkType: "CHAT" }); + """ + data = await client._send_and_wait( + opcode=Opcode.CHAT_CHECK_LINK, + payload={ + "link": "inkomusic123", + "linkType": "CHANNEL", + }, ) + + print(data) + # channel = await client.resolve_channel_by_name("fm92") # if channel: # print(f"Resolved channel by name: {channel.title}, ID: {channel.id}") diff --git a/src/pymax/mixins/group.py b/src/pymax/mixins/group.py index bd8a6b8..d700c00 100644 --- a/src/pymax/mixins/group.py +++ b/src/pymax/mixins/group.py @@ -95,9 +95,7 @@ async def invite_users_to_group( operation="add", ).model_dump(by_alias=True) - data = await self._send_and_wait( - opcode=Opcode.CHAT_MEMBERS_UPDATE, payload=payload - ) + data = await self._send_and_wait(opcode=Opcode.CHAT_MEMBERS_UPDATE, payload=payload) if data.get("payload", {}).get("error"): MixinsUtils.handle_error(data) @@ -155,9 +153,7 @@ async def remove_users_from_group( clean_msg_period=clean_msg_period, ).model_dump(by_alias=True) - data = await self._send_and_wait( - opcode=Opcode.CHAT_MEMBERS_UPDATE, payload=payload - ) + data = await self._send_and_wait(opcode=Opcode.CHAT_MEMBERS_UPDATE, payload=payload) if data.get("payload", {}).get("error"): MixinsUtils.handle_error(data) @@ -293,6 +289,33 @@ async def join_group(self, link: str) -> Chat: return chat + async def resolve_group_by_link(self, link: str) -> Chat | None: + """ + Разрешает группу по ссылке + + Args: + link (str): Ссылка на группу. + + Returns: + Chat | None: Объект чата группы или None, если не найдено. + """ + proceed_link = self._process_chat_join_link(link) + if proceed_link is None: + raise ValueError("Invalid group link") + + data = await self._send_and_wait( + opcode=Opcode.LINK_INFO, + payload={ + "link": proceed_link, + }, + ) + + if data.get("payload", {}).get("error"): + MixinsUtils.handle_error(data) + + chat = Chat.from_dict(data["payload"].get("chat", {})) + return chat + async def rework_invite_link(self, chat_id: int) -> Chat: """ Пересоздает ссылку для приглашения в группу @@ -329,14 +352,10 @@ async def get_chats(self, chat_ids: list[int]) -> list[Chat]: chat_id for chat_id in chat_ids if await self._get_chat(chat_id) is None ] if missed_chat_ids: - payload = GetChatInfoPayload(chat_ids=missed_chat_ids).model_dump( - by_alias=True - ) + payload = GetChatInfoPayload(chat_ids=missed_chat_ids).model_dump(by_alias=True) else: chats: list[Chat] = [ - chat - for chat_id in chat_ids - if (chat := await self._get_chat(chat_id)) is not None + chat for chat_id in chat_ids if (chat := await self._get_chat(chat_id)) is not None ] return chats From de05a5eff85a578d497d3fbec60a72c690b488d3 Mon Sep 17 00:00:00 2001 From: ink-developer Date: Tue, 23 Dec 2025 20:45:25 +0300 Subject: [PATCH 5/8] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=20ContactAttach=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B0=D0=BA=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B8=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D0=BB=20Messag?= =?UTF-8?q?e=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=BA=D0=B8=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85=20=D1=82?= =?UTF-8?q?=D0=B8=D0=BF=D0=BE=D0=B2=20=D0=B2=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pymax/static/enum.py | 1 + src/pymax/types.py | 43 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/pymax/static/enum.py b/src/pymax/static/enum.py index 8541388..37bc741 100644 --- a/src/pymax/static/enum.py +++ b/src/pymax/static/enum.py @@ -196,6 +196,7 @@ class AttachType(str, Enum): STICKER = "STICKER" AUDIO = "AUDIO" CONTROL = "CONTROL" + CONTACT = "CONTACT" class FormattingType(str, Enum): diff --git a/src/pymax/types.py b/src/pymax/types.py index 2907028..6b37fa1 100644 --- a/src/pymax/types.py +++ b/src/pymax/types.py @@ -630,6 +630,36 @@ def from_dict(cls, data: dict[str, Any]) -> Self: ) +class ContactAttach: + def __init__( + self, contact_id: int, first_name: str, last_name: str, name: str, photo_url: str + ) -> None: + self.contact_id = contact_id + self.first_name = first_name + self.last_name = last_name + self.name = name + self.photo_url = photo_url + self.type = AttachType.CONTACT + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Self: + return cls( + contact_id=data["contactId"], + first_name=data["firstName"], + last_name=data["lastName"], + name=data["name"], + photo_url=data["photoUrl"], + ) + + @override + def __repr__(self) -> str: + return f"ContactAttach(contact_id={self.contact_id!r}, first_name={self.first_name!r}, last_name={self.last_name!r}, name={self.name!r}, photo_url={self.photo_url!r})" + + @override + def __str__(self) -> str: + return f"ContactAttach: {self.name}" + + class Message: def __init__( self, @@ -652,6 +682,7 @@ def __init__( | ControlAttach | StickerAttach | AudioAttach + | ContactAttach ] | None ), @@ -672,7 +703,15 @@ def __init__( @classmethod def from_dict(cls, data: dict[Any, Any]) -> Self: message = data["message"] if data.get("message") else data - attaches: list[PhotoAttach | VideoAttach | FileAttach | ControlAttach | StickerAttach] = [] + attaches: list[ + PhotoAttach + | VideoAttach + | FileAttach + | ControlAttach + | StickerAttach + | AudioAttach + | ContactAttach + ] = [] for a in message.get("attaches", []): if a["_type"] == AttachType.PHOTO: attaches.append(PhotoAttach.from_dict(a)) @@ -686,6 +725,8 @@ def from_dict(cls, data: dict[Any, Any]) -> Self: attaches.append(StickerAttach.from_dict(a)) elif a["_type"] == AttachType.AUDIO: attaches.append(AudioAttach.from_dict(a)) + elif a["_type"] == AttachType.CONTACT: + attaches.append(ContactAttach.from_dict(a)) link_value = message.get("link") if isinstance(link_value, dict): link = MessageLink.from_dict(link_value) From 699564860f45e46293e59fa8aae7dd4665a2d716 Mon Sep 17 00:00:00 2001 From: ink-developer Date: Wed, 24 Dec 2025 13:51:28 +0300 Subject: [PATCH 6/8] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=BD=D0=BE=D1=80=D0=BC=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/example.py | 226 +++------------------------------- examples/flt_test.py | 51 -------- examples/large_file_upload.py | 51 -------- examples/reg.py | 34 ----- examples/test.py | 20 --- 5 files changed, 20 insertions(+), 362 deletions(-) delete mode 100644 examples/flt_test.py delete mode 100644 examples/large_file_upload.py delete mode 100644 examples/reg.py delete mode 100644 examples/test.py diff --git a/examples/example.py b/examples/example.py index 2de5dde..94beb22 100644 --- a/examples/example.py +++ b/examples/example.py @@ -5,13 +5,14 @@ from typing import Any import pymax -from pymax import MaxClient, Message, ReactionInfo, SocketMaxClient +from pymax import MaxClient, Message, ReactionInfo, SocketMaxClient, filters from pymax.files import File, Photo, Video +from pymax.filters import Filters from pymax.payloads import UserAgentPayload from pymax.static.enum import AttachType, Opcode from pymax.types import Chat -phone = "+79291250363" +phone = "+79991234567" headers = UserAgentPayload(device_type="WEB") client = MaxClient( @@ -28,103 +29,32 @@ async def handle_start() -> None: print(f"Client started as {client.me.names[0].first_name}!") - """ - case e.chat instanceof at: - return na({ link: D, linkType: "CHANNEL" }); - case e.chat instanceof Ct: - return na({ link: D, linkType: "CHAT" }); - """ - data = await client._send_and_wait( - opcode=Opcode.CHAT_CHECK_LINK, - payload={ - "link": "inkomusic123", - "linkType": "CHANNEL", - }, - ) - - print(data) - - # channel = await client.resolve_channel_by_name("fm92") - # if channel: - # print(f"Resolved channel by name: {channel.title}, ID: {channel.id}") - # else: - # print("Channel not found by name.") - # channel = await client.join_channel(link) - # if channel: - # print(f"Joined channel: {channel.title}, ID: {channel.id}") - # else: - # print("Failed to join channel.") - # await client.send_message( - # "Hello! The client has started successfully.", - # chat_id=2265456546456, - # notify=True, - # ) - # folder_update = await client.create_folder( - # title="My Folder", - # chat_include=[0], - # ) - # print(f"Folder created: {folder_update.folder.title}") - # video_path = "tests2/test.mp4" - # video_file = Video(path=video_path) +@client.on_raw_receive +async def handle_raw_receive(data: dict[str, Any]) -> None: + print(f"Raw data received: {data}") - # await client.send_message( - # text="Here is the video you requested.", - # chat_id=0, - # attachment=video_file, - # notify=True, - # ) - # chat_id = -6970655 - # for chat in client.chats: - # if chat.id == chat_id: - # print(f"Found chat: {chat.title}, ID: {chat.id}") - # members_count = chat.participants_count - # marker = 0 - # member_list = [] - # while len(member_list) < members_count: - # await asyncio.sleep(10) - # r = await client.load_members( - # chat_id=chat_id, - # marker=marker, - # count=200, - # ) - # members, marker = r - # member_list.extend(members) - # print(f"Loaded {len(member_list)}/{members_count} members...") - # for member in member_list: - # print( - # f"Member {member.contact.names[0].first_name}, ID: {member.contact.id}" - # ) - # r = await client.load_members(chat_id=chat_id, count=50) - # print(f"Loaded {len(r)} members from chat {chat_id}") - # member_list, marker = r - # for member in member_list: - # print(f"Member {member.contact.names[0].first_name}, ID: {member.contact.id}") - -# @client.on_reaction_change -# async def handle_reaction_change( -# message_id: str, chat_id: int, reaction_info: ReactionInfo -# ) -> None: -# print( -# f"Reaction changed on message {message_id} in chat {chat_id}: " -# f"Total count: {reaction_info.total_count}, " -# f"Your reaction: {reaction_info.your_reaction}, " -# f"Counters: {reaction_info.counters[0].reaction}={reaction_info.counters[0].count}" -# ) +@client.on_reaction_change +async def handle_reaction_change( + message_id: str, chat_id: int, reaction_info: ReactionInfo +) -> None: + print( + f"Reaction changed on message {message_id} in chat {chat_id}: " + f"Total count: {reaction_info.total_count}, " + f"Your reaction: {reaction_info.your_reaction}, " + f"Counters: {reaction_info.counters[0].reaction}={reaction_info.counters[0].count}" + ) -# @client.on_chat_update -# async def handle_chat_update(chat: Chat) -> None: -# print(f"Chat updated: {chat.id}, new title: {chat.title}") +@client.on_chat_update +async def handle_chat_update(chat: Chat) -> None: + print(f"Chat updated: {chat.id}, new title: {chat.title}") -@client.on_message() +@client.on_message(Filters.chat(0) & Filters.text("hello")) async def handle_message(message: Message) -> None: print(f"New message in chat {message.chat_id} from {message.sender}: {message.text}") - # if message.link and message.link.message.attaches: - # for attach in message.link.message.attaches: - # print(f"Link attach type: {attach.type}") @client.on_message_edit() @@ -137,122 +67,6 @@ async def handle_deleted_message(message: Message) -> None: print(f"Deleted message in chat {message.chat_id}: {message.id}") -# async def login_flow_test(): -# await client.connect() -# temp_token = await client.request_code(phone) -# code = input("Введите код: ").strip() -# await client.login_with_code(temp_token, code) - - -# asyncio.run(login_flow_test()) - -# @client.on_message(filter=Filter(chat_id=0)) -# async def handle_message(message: Message) -> None: -# print(str(message.sender) + ": " + message.text) - - -# @client.on_message_edit() -# async def handle_edited_message(message: Message) -> None: -# print(f"Edited message in chat {message.chat_id}: {message.text}") - - -# @client.on_message_delete() -# async def handle_deleted_message(message: Message) -> None: -# print(f"Deleted message in chat {message.chat_id}: {message.id}") - - -# @client.on_start -# async def handle_start() -> None: -# print(f"Client started successfully at {datetime.datetime.now()}!") -# print(client.me.id) - -# await client.send_message( -# "Hello, this is a test message sent upon client start!", -# chat_id=23424, -# notify=True, -# ) -# file_path = "ruff.toml" -# file = File(path=file_path) -# msg = await client.send_message( -# text="Here is the file you requested.", -# chat_id=0, -# attachment=file, -# notify=True, -# ) -# if msg: -# print(f"File sent successfully in message ID: {msg.id}") -# history = await client.fetch_history(chat_id=0) -# if history: -# for message in history: -# if message.attaches: -# for attach in message.attaches: -# if attach.type == AttachType.AUDIO: -# print(attach.url) -# chat = await client.rework_invite_link(chat_id=0) -# print(chat.link) -# text = """ -# **123** -# *123* -# __123__ -# ~~123~~ -# """ -# message = await client.send_message(text, chat_id=0, notify=True) -# react_info = await client.add_reaction( -# chat_id=0, message_id="115368067020359151", reaction="👍" -# ) -# if react_info: -# print("Reaction added!") -# print(react_info.total_count) -# react_info = await client.get_reactions( -# chat_id=0, message_ids=["115368067020359151"] -# ) -# if react_info: -# print("Reactions fetched!") -# for msg_id, info in react_info.items(): -# print(f"Message ID: {msg_id}, Total Reactions: {info.total_count}") -# react_info = await client.remove_reaction( -# chat_id=0, message_id="115368067020359151" -# ) -# if react_info: -# print("Reaction removed!") -# print(react_info.total_count) -# print(client.dialogs) - -# if history: -# for message in history: -# if message.link: -# print(message.link.chat_id) -# print(message.link.message.text) -# for attach in message.attaches: -# if attach.type == AttachType.CONTROL: -# print(attach.event) -# print(attach.extra) -# if attach.type == AttachType.VIDEO: -# print(message) -# vid = await client.get_video_by_id( -# chat_id=0, -# video_id=attach.video_id, -# message_id=message.id, -# ) -# print(vid.url) -# elif attach.type == AttachType.FILE: -# file = await client.get_file_by_id( -# chat_id=0, -# file_id=attach.file_id, -# message_id=message.id, -# ) -# print(file.url) -# print(client.me.names[0].first_name) -# user = await client.get_user(client.me.id) - -# photo1 = Photo(path="tests/test.jpeg") -# photo2 = Photo(path="tests/test.jpg") - -# await client.send_message( -# "Hello with photo!", chat_id=0, photos=[photo1, photo2], notify=True -# ) - - if __name__ == "__main__": try: asyncio.run(client.start()) diff --git a/examples/flt_test.py b/examples/flt_test.py deleted file mode 100644 index 06a098e..0000000 --- a/examples/flt_test.py +++ /dev/null @@ -1,51 +0,0 @@ -import asyncio -import logging - -import pymax -import pymax.static -from pymax import MaxClient -from pymax.filters import Filters -from pymax.payloads import UserAgentPayload -from pymax.static.enum import Opcode - -phone = "+7903223423" -headers = UserAgentPayload(device_type="WEB") - -client = MaxClient( - phone=phone, - work_dir="cache", - reconnect=False, - logger=None, - headers=headers, -) -client.logger.setLevel(logging.DEBUG) - - -@client.task(seconds=10) -async def periodic_task() -> None: - client.logger.info("Periodic task executed") - - -@client.on_message(Filters.text("test") & ~Filters.chat(0)) -async def handle_message(message: pymax.Message) -> None: - print(f"New message from {message.sender}: {message.text}") - - -@client.on_start -async def on_start(): - print("Client started") - data = await client._send_and_wait( - opcode=Opcode.FILE_UPLOAD, - payload={"count": 1}, - ) - print("File upload response:", data) - # opcode=pymax.static.enum.Opcode.CHATS_LIST, - # payload={ - # "marker": 1765721869777, - # }, - # ) - - # print("Chats list:", data) - - -asyncio.run(client.start()) diff --git a/examples/large_file_upload.py b/examples/large_file_upload.py deleted file mode 100644 index 7f22b92..0000000 --- a/examples/large_file_upload.py +++ /dev/null @@ -1,51 +0,0 @@ -import asyncio -import logging -from pathlib import Path - -from pymax import MaxClient -from pymax.files import File, Video - -client = MaxClient(phone="+1234567890", work_dir="cache", reconnect=False) -client.logger.setLevel(logging.INFO) - - -def create_big_file(file_path: Path, size_in_mb: int) -> None: - with open(file_path, "wb") as f: - f.seek(size_in_mb * 1024 * 1024 - 1) - f.write(b"\0") - - -@client.on_start -async def upload_large_file_example(): - await asyncio.sleep(2) - - file_path = Path("tests2/large_file.dat") - - if not file_path.exists(): - create_big_file(file_path, size_in_mb=300) - file_size = file_path.stat().st_size - client.logger.info(f"File size: {file_size / (1024 * 1024):.2f} MB") - - file = File(path=str(file_path)) - chat_id = 0 - - client.logger.info("Starting file upload...") - - try: - await client.send_message( - chat_id=chat_id, - text="📎 Вот большой файл", - attachment=file, - ) - client.logger.info("File uploaded successfully!") - - except OSError as e: - if "malloc failure" in str(e): - client.logger.error("Memory error - file too large for current memory") - client.logger.info("Recommendation: Upload smaller files or free up memory") - else: - raise - - -if __name__ == "__main__": - asyncio.run(client.start()) diff --git a/examples/reg.py b/examples/reg.py deleted file mode 100644 index 3811920..0000000 --- a/examples/reg.py +++ /dev/null @@ -1,34 +0,0 @@ -import asyncio - -from pymax import MaxClient, Message -from pymax.filters import Filters - -client = MaxClient( - phone="+1234567890", - work_dir="cache", -) - - -@client.on_message(Filters.chat(0)) -async def on_message(msg: Message): - print(f"[{msg.sender}] {msg.text}") - await client.send_message(chat_id=msg.chat_id, text="Привет!") - await client.add_reaction( - chat_id=msg.chat_id, message_id=str(msg.id), reaction="👍" - ) - - -@client.on_start -async def on_start(): - print(f"Клиент запущен. Ваш ID: {client.me.id}") - history = await client.fetch_history(chat_id=0) - for m in history: - print(f"- {m.text}") - - -async def main(): - await client.start() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/test.py b/examples/test.py deleted file mode 100644 index 5929cb3..0000000 --- a/examples/test.py +++ /dev/null @@ -1,20 +0,0 @@ -import asyncio - -from pymax import MaxClient -from pymax.payloads import UserAgentPayload - -ua = UserAgentPayload(device_type="DESKTOP", app_version="25.12.13") - -client = MaxClient( - phone="+79116290861", - work_dir="cache", - headers=ua, -) - - -@client.on_start -async def on_start() -> None: - print(f"MaxClient started as {client.me.names[0].first_name}!") - - -asyncio.run(client.start()) From 46d269b7ae49281801194fe6102b37d530dd6910 Mon Sep 17 00:00:00 2001 From: ink-developer Date: Wed, 24 Dec 2025 14:14:04 +0300 Subject: [PATCH 7/8] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D0=BA=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pymax/core.py | 1 + src/pymax/interfaces.py | 1 + src/pymax/mixins/socket.py | 10 ++++++++++ src/pymax/mixins/websocket.py | 9 +++++++++ 4 files changed, 21 insertions(+) diff --git a/src/pymax/core.py b/src/pymax/core.py index 77791a1..0766144 100644 --- a/src/pymax/core.py +++ b/src/pymax/core.py @@ -116,6 +116,7 @@ def __init__( self.dialogs: list[Dialog] = [] self.channels: list[Channel] = [] self.me: Me | None = None + self.contacts: list[User] = [] self._users: dict[int, User] = {} self._work_dir: str = work_dir diff --git a/src/pymax/interfaces.py b/src/pymax/interfaces.py index d28023d..4927fea 100644 --- a/src/pymax/interfaces.py +++ b/src/pymax/interfaces.py @@ -45,6 +45,7 @@ def __init__(self, logger: Logger) -> None: self.phone: str self.dialogs: list[Dialog] = [] self.channels: list[Channel] = [] + self.contacts: list[User] = [] self.me: Me | None = None self.host: str self.port: int diff --git a/src/pymax/mixins/socket.py b/src/pymax/mixins/socket.py index d83bf3e..f2b840b 100644 --- a/src/pymax/mixins/socket.py +++ b/src/pymax/mixins/socket.py @@ -28,6 +28,7 @@ Message, ReactionCounter, ReactionInfo, + User, ) @@ -603,6 +604,15 @@ async def _sync(self) -> None: self.channels.append(Channel.from_dict(raw_chat)) except Exception: self.logger.exception("Error parsing chat entry (socket)") + + for raw_user in raw_payload.get("contacts", []): + try: + user = User.from_dict(raw_user) + if user: + self.contacts.append(user) + except Exception: + self.logger.exception("Error parsing contact entry (socket)") + if raw_payload.get("profile", {}).get("contact"): self.me = Me.from_dict(raw_payload.get("profile", {}).get("contact", {})) self.logger.info( diff --git a/src/pymax/mixins/websocket.py b/src/pymax/mixins/websocket.py index 8fbecb4..c74f579 100644 --- a/src/pymax/mixins/websocket.py +++ b/src/pymax/mixins/websocket.py @@ -27,6 +27,7 @@ Message, ReactionCounter, ReactionInfo, + User, ) @@ -465,6 +466,14 @@ async def _sync(self, user_agent: UserAgentPayload) -> None: except Exception: self.logger.exception("Error parsing chat entry") + for raw_user in raw_payload.get("contacts", []): + try: + user = User.from_dict(raw_user) + if user: + self.contacts.append(user) + except Exception: + self.logger.exception("Error parsing contact entry") + if raw_payload.get("profile", {}).get("contact"): self.me = Me.from_dict(raw_payload.get("profile", {}).get("contact", {})) From b51926418a11522edf5050d4d3c1d616bbd9c6b2 Mon Sep 17 00:00:00 2001 From: ink-developer Date: Wed, 24 Dec 2025 14:17:11 +0300 Subject: [PATCH 8/8] =?UTF-8?q?=D0=91=D0=B0=D0=BC=D0=BF=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=81=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c6a4fb3..08399c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "maxapi-python" -version = "1.2.2" +version = "1.2.3" description = "Python wrapper для API мессенджера Max" readme = "README.md" requires-python = ">=3.10"