pyrogram-client 0.0.2.dev1__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.
@@ -0,0 +1,42 @@
1
+ from asyncio import sleep
2
+ from io import BytesIO
3
+ from typing import Literal
4
+
5
+ from pyrogram import Client
6
+ from pyrogram.filters import chat
7
+ from pyrogram.handlers import MessageHandler
8
+ from pyrogram.types import Message
9
+
10
+ from pyro_client.storage import PgStorage
11
+
12
+ AuthTopic = Literal["phone", "code", "pass"]
13
+
14
+
15
+ class BaseClient(Client):
16
+ storage: PgStorage
17
+
18
+ def __init__(self, name: str, *args, **kwargs):
19
+ super().__init__(name, *args, storage_engine=PgStorage(name), **kwargs)
20
+
21
+ async def send(self, txt: str, uid: int | str = "me", photo: bytes = None, video: bytes = None) -> Message:
22
+ if photo:
23
+ return await self.send_photo(uid, BytesIO(photo), txt)
24
+ elif video:
25
+ return await self.send_video(uid, BytesIO(video), txt)
26
+ else:
27
+ return await self.send_message(uid, txt)
28
+
29
+ async def wait_from(self, uid: int, topic: str, past: int = 0, timeout: int = 10) -> str:
30
+ handler = MessageHandler(self.got_msg, chat(uid))
31
+ self.add_handler(handler)
32
+ while past < timeout:
33
+ if txt := self.storage.session.state.get(uid, {}).pop(topic, None):
34
+ self.remove_handler(handler)
35
+ return txt
36
+ await sleep(1)
37
+ past += 1
38
+ return await self.wait_from(uid, topic, past, timeout)
39
+
40
+ async def got_msg(self, _, msg: Message):
41
+ if topic := self.storage.session.state.get(msg.from_user.id, {}).pop("waiting_for", None):
42
+ self.storage.session.state[msg.from_user.id][topic] = msg.text
@@ -0,0 +1,9 @@
1
+ from pyro_client.client.base import BaseClient, AuthTopic
2
+
3
+
4
+ class BotClient(BaseClient):
5
+ def __init__(self, api_id: str, api_hash: str, token: str):
6
+ super().__init__(token.split(":")[0], api_id, api_hash, bot_token=token)
7
+
8
+ async def wait_auth_from(self, uid: int, topic: AuthTopic, past: int = 0, timeout: int = 60) -> str:
9
+ return await super().wait_from(uid, topic, past, timeout)
@@ -0,0 +1,58 @@
1
+ from io import BytesIO
2
+
3
+ from pyrogram.raw.functions.messages import UploadMedia
4
+ from pyrogram.raw.functions.upload import GetFile
5
+ from pyrogram.raw.types import (
6
+ MessageMediaDocument,
7
+ InputMediaUploadedDocument,
8
+ InputPeerSelf,
9
+ MessageMediaPhoto,
10
+ InputMediaUploadedPhoto,
11
+ InputDocumentFileLocation,
12
+ InputPhotoFileLocation,
13
+ )
14
+ from pyrogram.raw.types.upload import File
15
+ from pyrogram.types import Message
16
+
17
+ from pyro_client.client.bot import BotClient
18
+
19
+
20
+ class FileClient(BotClient):
21
+ @staticmethod
22
+ def ref_enc(ph_id: int, access_hash: int, ref: bytes) -> bytes:
23
+ return ph_id.to_bytes(8, "big") + access_hash.to_bytes(8, "big", signed=True) + ref
24
+
25
+ @staticmethod
26
+ def ref_dec(full_ref: bytes) -> tuple[int, int, bytes]:
27
+ pid, ah = int.from_bytes(full_ref[:8], "big"), int.from_bytes(full_ref[8:16], "big", signed=True)
28
+ return pid, ah, full_ref[16:]
29
+
30
+ async def save_doc(self, byts: bytes, ctype: str) -> tuple[MessageMediaDocument, bytes]:
31
+ in_file = await self.save_file(BytesIO(byts))
32
+ imud = InputMediaUploadedDocument(file=in_file, mime_type=ctype, attributes=[])
33
+ upf: MessageMediaDocument = await self.invoke(UploadMedia(peer=InputPeerSelf(), media=imud))
34
+ return upf, (
35
+ upf.document.id.to_bytes(8, "big")
36
+ + upf.document.access_hash.to_bytes(8, "big", signed=True)
37
+ + upf.document.file_reference
38
+ )
39
+
40
+ async def save_photo(self, file: bytes) -> tuple[MessageMediaPhoto, bytes]:
41
+ in_file = await self.save_file(BytesIO(file))
42
+ upm = UploadMedia(peer=InputPeerSelf(), media=InputMediaUploadedPhoto(file=in_file))
43
+ upp: MessageMediaPhoto = await self.invoke(upm)
44
+ return upp, self.ref_enc(upp.photo.id, upp.photo.access_hash, upp.photo.file_reference)
45
+
46
+ async def get_doc(self, fid: bytes) -> File:
47
+ pid, ah, ref = self.ref_dec(fid)
48
+ loc = InputDocumentFileLocation(id=pid, access_hash=ah, file_reference=ref, thumb_size="x")
49
+ return await self.invoke(GetFile(location=loc, offset=0, limit=512 * 1024))
50
+
51
+ async def get_photo(self, fid: bytes, st: str) -> File:
52
+ pid, ah, ref = self.ref_dec(fid)
53
+ loc = InputPhotoFileLocation(id=pid, access_hash=ah, file_reference=ref, thumb_size=st)
54
+ return await self.invoke(GetFile(location=loc, offset=0, limit=512 * 1024))
55
+
56
+ async def bot_got_msg(self, _, msg: Message):
57
+ if state := self.storage.session.state.pop("bot", None):
58
+ self.storage.session.state[state] = msg.text
@@ -0,0 +1,106 @@
1
+ from pyrogram import enums
2
+ from pyrogram.errors import BadRequest, SessionPasswordNeeded, AuthKeyUnregistered
3
+ from pyrogram.types import Message, User, SentCode
4
+
5
+ from pyro_client.client.base import BaseClient, AuthTopic
6
+ from pyro_client.client.bot import BotClient
7
+
8
+
9
+ class UserClient(BaseClient):
10
+ bot: BotClient
11
+
12
+ def __init__(self, name: str, api_id: str, api_hash: str, bot_token: str = None, proxy: dict = None,
13
+ device: str = "iPhone 17 Air", app: str = "XyncNet 1.0", ver: str = "iOS 19.0.1"):
14
+ super().__init__(name, api_id, api_hash, device_model=device, app_version=app, system_version=ver)
15
+ self.bot = bot_token and BotClient(api_id, api_hash, bot_token)
16
+
17
+ async def start(self, use_qr: bool = False, except_ids: list[int] = None):
18
+ await self.bot.start()
19
+ await super().start(use_qr=use_qr, except_ids=except_ids or [])
20
+
21
+ async def ask_for(self, topic: AuthTopic, question: str) -> str:
22
+ await self.bot.send_message(self.storage.me_id, question)
23
+ self.bot.storage.session.state[self.storage.me_id] = {"waiting_for": topic}
24
+ return await self.bot.wait_auth_from(self.storage.me_id, topic)
25
+
26
+ async def receive(self, txt: str, photo: bytes = None, video: bytes = None) -> Message:
27
+ return await self.bot.send(txt, self.me.id, photo, video)
28
+
29
+ async def authorize(self, sent_code: SentCode = None) -> User:
30
+ sent_code_desc = {
31
+ enums.SentCodeType.APP: "Telegram app",
32
+ enums.SentCodeType.SMS: "SMS",
33
+ enums.SentCodeType.CALL: "phone call",
34
+ enums.SentCodeType.FLASH_CALL: "phone flash call",
35
+ enums.SentCodeType.FRAGMENT_SMS: "Fragment SMS",
36
+ enums.SentCodeType.EMAIL_CODE: "email code",
37
+ }
38
+ # Step 1: Phone
39
+ if not self.phone_number:
40
+ try:
41
+ self.phone_number = await self.ask_for("phone", "Your phone:")
42
+ if not self.phone_number:
43
+ await self.authorize()
44
+ sent_code = await self.send_code(self.phone_number)
45
+ except BadRequest as e:
46
+ await self.send(e.MESSAGE)
47
+ self.phone_number = None
48
+ return await self.authorize(sent_code)
49
+ # Step 2: Code
50
+ if not self.phone_code:
51
+ _ = await self.ask_for("code", f"The confirm code sent via {sent_code_desc[sent_code.type]}")
52
+ self.phone_code = _.replace("_", "")
53
+ try:
54
+ signed_in = await self.sign_in(self.phone_number, sent_code.phone_code_hash, self.phone_code)
55
+ except BadRequest as e:
56
+ await self.send(e.MESSAGE)
57
+ self.phone_code = None
58
+ return await self.authorize(sent_code)
59
+ except SessionPasswordNeeded as e:
60
+ # Step 2.1?: Cloud password
61
+ await self.send(e.MESSAGE)
62
+ while True:
63
+ self.password = await self.ask_for("pass", f"Enter pass: (hint: {await self.get_password_hint()})")
64
+ try:
65
+ return await self.check_password(self.password)
66
+ except BadRequest as e:
67
+ await self.send(e.MESSAGE)
68
+ self.password = None
69
+
70
+ if isinstance(signed_in, User):
71
+ await self.send("✅")
72
+ return signed_in
73
+
74
+ if not signed_in:
75
+ await self.send("No registered such phone number")
76
+
77
+ async def stop(self, block: bool = True):
78
+ await super().stop(block)
79
+ await self.bot.stop(block)
80
+
81
+
82
+ async def main():
83
+ from x_auth import models
84
+ from x_model import init_db
85
+
86
+ from pyro_client.loader import WSToken, PG_DSN, TOKEN, API_ID, API_HASH
87
+
88
+ _ = await init_db(PG_DSN, models, True)
89
+ await models.Proxy.load_list(WSToken)
90
+ session = await models.Session.filter(is_bot=False).order_by("-date").first()
91
+ uc = UserClient("", API_ID, API_HASH, TOKEN)
92
+ # try:
93
+ await uc.start()
94
+ await uc.receive("hi")
95
+ # except Exception as e:
96
+ # print(e.MESSAGE)
97
+ # await uc.send(e.MESSAGE)
98
+ # await uc.storage.session.delete()
99
+ # finally:
100
+ await uc.stop()
101
+
102
+
103
+ if __name__ == "__main__":
104
+ from asyncio import run
105
+
106
+ run(main())
pyro_client/loader.py ADDED
@@ -0,0 +1,11 @@
1
+ from dotenv import load_dotenv
2
+ from os import getenv as env
3
+
4
+ load_dotenv()
5
+
6
+ API_ID = env("API_ID")
7
+ API_HASH = env("API_HASH")
8
+ PG_DSN = f"postgres://{env('POSTGRES_USER')}:{env('POSTGRES_PASSWORD')}@{env('POSTGRES_HOST', 'xyncdbs')}:" \
9
+ f"{env('POSTGRES_PORT', 5432)}/{env('POSTGRES_DB', env('POSTGRES_USER'))}"
10
+ TOKEN = env("TOKEN")
11
+ WSToken = env("WST")
pyro_client/storage.py ADDED
@@ -0,0 +1,144 @@
1
+ import time
2
+ from typing import Any
3
+
4
+ from pyrogram import raw, utils
5
+ from pyrogram.storage import Storage
6
+ from tortoise.functions import Count
7
+ from x_auth.models import Proxy, Username, Version, Session, Peer, UpdateState
8
+
9
+ from pyro_client.loader import WSToken
10
+
11
+
12
+ def get_input_peer(peer_id: int, access_hash: int, peer_type: str):
13
+ if peer_type in ["user", "bot"]:
14
+ return raw.types.InputPeerUser(user_id=peer_id, access_hash=access_hash)
15
+
16
+ if peer_type == "group":
17
+ return raw.types.InputPeerChat(chat_id=-peer_id)
18
+
19
+ if peer_type in ["channel", "supergroup"]:
20
+ return raw.types.InputPeerChannel(channel_id=utils.get_channel_id(peer_id), access_hash=access_hash)
21
+
22
+ raise ValueError(f"Invalid peer type: {peer_type}")
23
+
24
+
25
+ class PgStorage(Storage):
26
+ VERSION = 1
27
+ USERNAME_TTL = 8 * 60 * 60
28
+ session: Session
29
+ me_id: int
30
+
31
+ async def open(self):
32
+ self.me_id = int((uid_dc := self.name.split("_")).pop(0))
33
+ if not (session := await Session.get_or_none(id=self.name)):
34
+ username, _ = await Username.get_or_create(id=self.me_id)
35
+ await Proxy.load_list(WSToken)
36
+ # await Proxy.get_replaced(WSToken)
37
+ proxy = (
38
+ await Proxy.annotate(sessions_count=Count("sessions"))
39
+ .filter(valid=True)
40
+ .order_by("sessions_count", "-updated_at")
41
+ .first()
42
+ )
43
+ session = await Session.create(
44
+ id=self.name,
45
+ user=username,
46
+ dc_id=int(uid_dc[0]) if uid_dc else None,
47
+ date=int(time.time()),
48
+ proxy=proxy,
49
+ )
50
+ self.session = session
51
+
52
+ async def save(self):
53
+ await self.date(int(time.time()))
54
+
55
+ async def close(self): ...
56
+
57
+ async def delete(self):
58
+ await Session.filter(id=self.name).delete()
59
+
60
+ async def update_peers(self, peers: list[tuple[int, int, str, str]]):
61
+ for peer in peers:
62
+ uid, ac_hsh, typ, phn = peer
63
+ un, _ = await Username.get_or_create(id=uid)
64
+ await Peer.update_or_create(
65
+ {"username": un, "type": typ, "phone_number": phn}, session_id=self.name, id=ac_hsh
66
+ )
67
+
68
+ async def update_usernames(self, usernames: list[tuple[int, list[str]]]):
69
+ for telegram_id, user_list in usernames:
70
+ for username in user_list:
71
+ await Username.update_or_create({"username": username}, id=telegram_id)
72
+
73
+ async def get_peer_by_id(self, peer_id_or_username: int | str):
74
+ attr = "id" if isinstance(peer_id_or_username, int) else "username"
75
+ if not (peer := await Peer.get_or_none(session_id=self.name, **{"username__" + attr: peer_id_or_username})):
76
+ raise KeyError(f"User not found: {peer_id_or_username}")
77
+ if peer.last_update_on:
78
+ if abs(time.time() - peer.last_update_on.timestamp()) > self.USERNAME_TTL:
79
+ raise KeyError(f"Username expired: {peer_id_or_username}")
80
+ return get_input_peer(peer.username_id, peer.id, peer.type)
81
+
82
+ async def get_peer_by_username(self, username: str):
83
+ return await self.get_peer_by_id(username)
84
+
85
+ async def update_state(self, value: tuple[int, int, int, int, int] = object):
86
+ if value is None:
87
+ return await UpdateState.filter(session_id=self.name)
88
+ elif isinstance(value, int):
89
+ await UpdateState.filter(session_id=self.name, id=value).delete()
90
+ else:
91
+ sid, pts, qts, date, seq = value
92
+ await UpdateState.get_or_create(
93
+ {"pts": pts, "qts": qts, "date": date, "seq": seq}, session_id=self.name, id=sid
94
+ )
95
+
96
+ async def get_peer_by_phone_number(self, phone_number: str):
97
+ if not (
98
+ peer := await Peer.filter(session_id=self.name, phone_number=phone_number).values_list(
99
+ "id", "access_hash", "type"
100
+ )
101
+ ):
102
+ raise KeyError(f"Phone number not found: {phone_number}")
103
+ return get_input_peer(*peer)
104
+
105
+ async def _get(self, attr: str):
106
+ return await Session.get(id=self.name).values_list(attr, flat=True)
107
+
108
+ async def _set(self, attr: str, value):
109
+ await Session.update_or_create({attr: value}, id=self.name)
110
+
111
+ async def _accessor(self, attr: str, value: Any = object):
112
+ if value == object:
113
+ return await self._get(attr)
114
+ else:
115
+ await self._set(attr, value)
116
+
117
+ async def dc_id(self, value: int = object):
118
+ return await self._accessor("dc_id", value)
119
+
120
+ async def api_id(self, value: int = object):
121
+ return await self._accessor("api_id", value)
122
+
123
+ async def test_mode(self, value: bool = object):
124
+ return await self._accessor("test_mode", value)
125
+
126
+ async def auth_key(self, value: bytes = object):
127
+ return await self._accessor("auth_key", value)
128
+
129
+ async def date(self, value: int = object):
130
+ return await self._accessor("date", value)
131
+
132
+ async def user_id(self, value: int = object):
133
+ return await self._accessor("user_id", value)
134
+
135
+ async def is_bot(self, value: bool = object):
136
+ return await self._accessor("is_bot", value)
137
+
138
+ @staticmethod
139
+ async def version(value: int = object):
140
+ if value == object:
141
+ ver = await Version.first()
142
+ return ver.number
143
+ else:
144
+ await Version.update_or_create(id=value)
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrogram-client
3
+ Version: 0.0.2.dev1
4
+ Author-email: Mike Artemiev <mixartemev@gmail.com>
5
+ Project-URL: Homepage, https://gitlab.com/XyncNet/pyro-client
6
+ Project-URL: Repository, https://gitlab.com/XyncNet/pyro-client
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: kurigram
9
+ Requires-Dist: tgcrypto
10
+ Requires-Dist: xn-auth
11
+ Provides-Extra: dev
12
+ Requires-Dist: build; extra == "dev"
13
+ Requires-Dist: pre-commit; extra == "dev"
14
+ Requires-Dist: python-dotenv; extra == "dev"
15
+ Requires-Dist: setuptools-scm; extra == "dev"
16
+ Requires-Dist: twine; extra == "dev"
@@ -0,0 +1,10 @@
1
+ pyro_client/loader.py,sha256=CsBBsb8PfEtU4z7eDMlkc0V3x4PfwTvc2R1HbdVhAMg,362
2
+ pyro_client/storage.py,sha256=c538LC1uof0glq2LIWlyd5yqJxqWz1FUgqF9l6znBCQ,5419
3
+ pyro_client/client/base.py,sha256=m3NiiqnWTMvYvZO9p2HXFboBIUDN_NcERrek0aw-QiM,1550
4
+ pyro_client/client/bot.py,sha256=GECmzAQh0MU3REJB8dsq5A0sIeDzzmnRYYfftRG5xF8,406
5
+ pyro_client/client/file.py,sha256=JzP-BqqOdfP8LfVXI-qnO4hIS68mZaMnk8SVqrSngA0,2548
6
+ pyro_client/client/user.py,sha256=2GndFXUCESC6H2H8M3JgZZK3x2GkZx1I2UXHjjAiTwY,4289
7
+ pyrogram_client-0.0.2.dev1.dist-info/METADATA,sha256=m46X7Bm523iBohF3ogVn7JoqjfUAAybkdPC3gx65zSA,563
8
+ pyrogram_client-0.0.2.dev1.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
9
+ pyrogram_client-0.0.2.dev1.dist-info/top_level.txt,sha256=pR3onX0Aots7ODOSrd8dssJg3J2kz36Crt_48QSwyi0,12
10
+ pyrogram_client-0.0.2.dev1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.7.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pyro_client