Skip to content
This repository was archived by the owner on Feb 11, 2026. It is now read-only.

Commit bae4476

Browse files
committed
Добавил загрузку фото профиля
1 parent 1834b41 commit bae4476

5 files changed

Lines changed: 91 additions & 36 deletions

File tree

examples/example.py

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66

77
import pymax
88
from pymax import MaxClient, Message, ReactionInfo, SocketMaxClient
9-
from pymax.files import File, Video
9+
from pymax.files import File, Photo, Video
1010
from pymax.payloads import UserAgentPayload
1111
from pymax.static.enum import AttachType, Opcode
1212
from pymax.types import Chat
1313

14-
phone = "+7903223111"
14+
phone = "+79291250363"
1515
headers = UserAgentPayload(device_type="WEB")
1616

1717
client = MaxClient(
@@ -24,34 +24,16 @@
2424
client.logger.setLevel(logging.INFO)
2525

2626

27-
@client.on_raw_receive
28-
async def handle_raw_receive(data: dict[str, Any]) -> None:
29-
print(f"Raw data received: {data}")
30-
31-
32-
@client.task(seconds=10)
33-
async def periodic_task() -> None:
34-
# print(f"Periodic task executed at {datetime.datetime.now()}")
35-
...
36-
37-
3827
@client.on_start
3928
async def handle_start() -> None:
4029
print(f"Client started as {client.me.names[0].first_name}!")
4130

42-
chat_id = -1
43-
max_messages = 1000
44-
messages = []
45-
from_time = int(time() * 1000)
46-
while len(messages) < max_messages:
47-
r = await client.fetch_history(chat_id=chat_id, from_time=from_time, backward=30)
48-
if not r:
49-
break
50-
from_time = r[0].time
51-
messages.extend(r)
52-
print(f"First message time: {from_time}, id: {r[0].id}, text: {r[0].text}")
53-
print(f"Last message time: {from_time}, id: {r[-1].id}, text: {r[-1].text}")
54-
print(f"Loaded {len(messages)}/{max_messages} messages...")
31+
photo = Photo(path="/tests2/test.png")
32+
33+
await client.change_profile(
34+
first_name="Dima",
35+
photo=photo,
36+
)
5537
# channel = await client.resolve_channel_by_name("fm92")
5638
# if channel:
5739
# print(f"Resolved channel by name: {channel.title}, ID: {channel.id}")

src/pymax/files.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ class Photo(BaseFile):
4646
} # FIXME: костыль ✅
4747

4848
def __init__(self, url: str | None = None, path: str | None = None) -> None:
49+
if path:
50+
self.file_name = Path(path).name
51+
elif url:
52+
self.file_name = Path(url).name
53+
4954
super().__init__(url, path)
5055

5156
def validate_photo(self) -> tuple[str, str] | None:

src/pymax/mixins/self.py

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
import urllib.parse
2+
from http import HTTPStatus
13
from typing import Any
4+
from urllib.parse import parse_qs, urlparse
25
from uuid import uuid4
36

7+
import aiohttp
8+
49
from pymax.exceptions import Error
10+
from pymax.files import Photo
511
from pymax.interfaces import ClientProtocol
612
from pymax.mixins.utils import MixinsUtils
713
from pymax.payloads import (
@@ -10,17 +16,60 @@
1016
DeleteFolderPayload,
1117
GetFolderPayload,
1218
UpdateFolderPayload,
19+
UploadPayload,
1320
)
1421
from pymax.static.enum import Opcode
15-
from pymax.types import Folder, FolderList, FolderUpdate
22+
from pymax.types import Folder, FolderList, FolderUpdate, Me
1623

1724

1825
class SelfMixin(ClientProtocol):
26+
async def _request_photo_upload_url(self) -> str:
27+
self.logger.info("Requesting profile photo upload URL")
28+
29+
data = await self._send_and_wait(
30+
opcode=Opcode.PHOTO_UPLOAD,
31+
payload=UploadPayload(profile=True).model_dump(by_alias=True),
32+
)
33+
34+
if data.get("payload", {}).get("error"):
35+
MixinsUtils.handle_error(data)
36+
37+
return data["payload"]["url"]
38+
39+
async def _upload_profile_photo(self, upload_url: str, photo: Photo) -> str:
40+
self.logger.info("Uploading profile photo")
41+
42+
parsed_url = urlparse(upload_url)
43+
photo_id = parse_qs(parsed_url.query)["photoIds"][0]
44+
45+
form = aiohttp.FormData()
46+
form.add_field(
47+
"file",
48+
await photo.read(),
49+
filename=photo.file_name,
50+
)
51+
52+
async with (
53+
aiohttp.ClientSession() as session,
54+
session.post(upload_url, data=form) as response,
55+
):
56+
if response.status != HTTPStatus.OK:
57+
raise Error(
58+
"Failed to upload profile photo.", message="UploadError", title="Upload Error"
59+
)
60+
61+
self.logger.info("Upload successful")
62+
data = await response.json()
63+
return data["photos"][photo_id][
64+
"token"
65+
] # TODO: сделать нормальную типизацию и чекнинг ответа
66+
1967
async def change_profile(
2068
self,
2169
first_name: str,
2270
last_name: str | None = None,
2371
description: str | None = None,
72+
photo: Photo | None = None,
2473
) -> bool:
2574
"""
2675
Изменяет информацию профиля текущего пользователя.
@@ -35,20 +84,36 @@ async def change_profile(
3584
:rtype: bool
3685
"""
3786

38-
payload = ChangeProfilePayload(
39-
first_name=first_name,
40-
last_name=last_name,
41-
description=description,
42-
).model_dump(
43-
by_alias=True,
44-
exclude_none=True,
45-
)
87+
if photo:
88+
upload_url = await self._request_photo_upload_url()
89+
photo_token = await self._upload_profile_photo(upload_url, photo)
90+
91+
payload = ChangeProfilePayload(
92+
first_name=first_name,
93+
last_name=last_name,
94+
description=description,
95+
photo_token=photo_token,
96+
).model_dump(
97+
by_alias=True,
98+
exclude_none=True,
99+
)
100+
else:
101+
payload = ChangeProfilePayload(
102+
first_name=first_name,
103+
last_name=last_name,
104+
description=description,
105+
).model_dump(
106+
by_alias=True,
107+
exclude_none=True,
108+
)
46109

47110
data = await self._send_and_wait(opcode=Opcode.PROFILE, payload=payload)
48111

49112
if data.get("payload", {}).get("error"):
50113
MixinsUtils.handle_error(data)
51114

115+
self.me = Me.from_dict(data["payload"]["profile"]["contact"])
116+
52117
return True
53118

54119
async def create_folder(

src/pymax/payloads.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class ReplyLink(CamelModel):
9797

9898
class UploadPayload(CamelModel):
9999
count: int = 1
100+
profile: bool = False
100101

101102

102103
class AttachPhotoPayload(CamelModel):
@@ -168,6 +169,8 @@ class ChangeProfilePayload(CamelModel):
168169
first_name: str
169170
last_name: str | None = None
170171
description: str | None = None
172+
photo_token: str | None = None
173+
avatar_type: str = "USER_AVATAR" # TODO: вынести гада в энам
171174

172175

173176
class ResolveLinkPayload(CamelModel):

src/pymax/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class Name:
4747
def __init__(
4848
self,
4949
name: str | None,
50-
first_name: None,
50+
first_name: None | str,
5151
last_name: str | None,
5252
type: str | None,
5353
) -> None:

0 commit comments

Comments
 (0)