pyrogram-client 0.0.2__tar.gz

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,10 @@
1
+ TOKEN=your:bottoken
2
+ API_ID=tg_app_api_id
3
+ API_HASH=tg_app_api_hash
4
+ WST=webshare_token
5
+
6
+ POSTGRES_HOST=127.0.0.1
7
+ POSTGRES_PORT=5432
8
+ POSTGRES_USER=xync
9
+ POSTGRES_PASSWORD=passwd
10
+ POSTGRES_DB=xync
@@ -0,0 +1,12 @@
1
+ .idea
2
+ /venv
3
+ /.env
4
+ .pytest_cache
5
+ __pycache__
6
+ /dist
7
+ /*.egg-info
8
+ /build
9
+ .vscode
10
+ /xync_client/Gate/res.js
11
+ /tests/res.js
12
+ *.session*
@@ -0,0 +1,36 @@
1
+ repos:
2
+ - repo: local
3
+ hooks:
4
+ - id: tag
5
+ name: tag
6
+ ### make tag with next ver only if "fix" in commit_msg or starts with "feat"
7
+ entry: bash -c 'grep -e "^feat:" -e "^fix:" .git/COMMIT_EDITMSG && make patch || exit 0'
8
+ language: system
9
+ verbose: true
10
+ pass_filenames: false
11
+ always_run: true
12
+ stages: [post-commit]
13
+
14
+ - id: build
15
+ name: build
16
+ ### build & upload package only for "main" branch push
17
+ entry: bash -c 'echo $PRE_COMMIT_LOCAL_BRANCH | grep /master && make twine || echo 0'
18
+ language: system
19
+ pass_filenames: false
20
+ verbose: true
21
+ require_serial: true
22
+ stages: [pre-push]
23
+
24
+ - repo: https://github.com/astral-sh/ruff-pre-commit
25
+ ### Ruff version.
26
+ rev: v0.11.5
27
+ hooks:
28
+ ### Run the linter.
29
+ - id: ruff
30
+ args: [--fix, --unsafe-fixes]
31
+ stages: [pre-commit]
32
+ ### Run the formatter.
33
+ - id: ruff-format
34
+ types_or: [python, pyi]
35
+ verbose: true
36
+ stages: [pre-commit]
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrogram-client
3
+ Version: 0.0.2
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,24 @@
1
+ include .env
2
+ PACKAGE := pyro
3
+ VPYTHON := venv/bin/python
4
+
5
+ .PHONY: all install pre-commit clean build twine patch
6
+
7
+ all:
8
+ make install test clean build
9
+
10
+ install: venv
11
+ $(VPYTHON) -m pip install -e .[dev]; make pre-commit
12
+ pre-commit: .pre-commit-config.yaml
13
+ pre-commit install -t pre-commit -t post-commit -t pre-push
14
+
15
+ clean: .pytest_cache dist $(PACKAGE).egg-info
16
+ rm -rf .pytest_cache dist/* $(PACKAGE).egg-info $(PACKAGE)/__pycache__ dist/__pycache__
17
+
18
+ build:
19
+ $(VPYTHON) -m build
20
+ twine: build dist
21
+ $(VPYTHON) -m twine upload dist/* --skip-existing
22
+
23
+ patch:
24
+ git tag `$(VPYTHON) -m setuptools_scm --strip-dev`; git push --tags --prune -f
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "pyrogram-client"
3
+ requires-python = ">=3.11"
4
+ authors = [
5
+ {name = "Mike Artemiev", email = "mixartemev@gmail.com"},
6
+ ]
7
+ dynamic = ["version"]
8
+
9
+ dependencies = [
10
+ "kurigram",
11
+ "tgcrypto",
12
+ "xn-auth",
13
+ ]
14
+
15
+ [project.optional-dependencies]
16
+ dev = [
17
+ "build",
18
+ "pre-commit",
19
+ "python-dotenv",
20
+ "setuptools-scm",
21
+ "twine",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://gitlab.com/XyncNet/pyro-client"
26
+ Repository = "https://gitlab.com/XyncNet/pyro-client"
27
+
28
+ [tool.setuptools]
29
+ packages = ["pyro_client"]
30
+
31
+ [build-system]
32
+ requires = ["setuptools>=72", "setuptools-scm[toml]>=8"]
33
+ build-backend = "setuptools.build_meta"
34
+ [tool.setuptools_scm]
35
+ version_scheme = "python-simplified-semver" # if "feature" in `branch_name` SEMVER_MINOR++ else SEMVER_PATCH++
36
+ local_scheme = "no-local-version"
37
+
38
+ [tool.ruff]
39
+ line-length = 120
@@ -0,0 +1,82 @@
1
+ import asyncio
2
+ from collections import OrderedDict
3
+ from io import BytesIO
4
+ from typing import Literal
5
+
6
+ from pyrogram import Client
7
+ from pyrogram.filters import chat, contact, AndFilter
8
+ from pyrogram.handlers import MessageHandler
9
+ from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, ReplyKeyboardMarkup, KeyboardButton
10
+
11
+ from pyro_client.client.single import SingleMeta
12
+ from pyro_client.storage import PgStorage
13
+
14
+ AuthTopic = Literal["phone", "code", "pass"]
15
+
16
+
17
+ def sync_call_async(async_func, *args):
18
+ loop = asyncio.get_event_loop() # Получаем текущий запущенный цикл
19
+ if loop.is_running():
20
+ future = asyncio.run_coroutine_threadsafe(async_func(*args), loop)
21
+ return future.result(5) # Блокируемся, пока корутина не выполнится
22
+
23
+
24
+ class BaseClient(Client, metaclass=SingleMeta):
25
+ storage: PgStorage
26
+
27
+ def __init__(self, name: str, api_id, api_hash, *args, **kwargs):
28
+ super().__init__(name, api_id=api_id, api_hash=api_hash, *args, storage_engine=PgStorage(name), **kwargs)
29
+
30
+ async def send(
31
+ self,
32
+ txt: str,
33
+ uid: int | str = "me",
34
+ btns: list[InlineKeyboardButton | KeyboardButton] = None,
35
+ photo: bytes = None,
36
+ video: bytes = None,
37
+ ) -> Message:
38
+ ikm = (
39
+ (
40
+ InlineKeyboardMarkup([btns])
41
+ if isinstance(btns[0], InlineKeyboardButton)
42
+ else ReplyKeyboardMarkup([btns], one_time_keyboard=True)
43
+ )
44
+ if btns
45
+ else None
46
+ )
47
+ if photo:
48
+ return await self.send_photo(uid, BytesIO(photo), txt, reply_markup=ikm)
49
+ elif video:
50
+ return await self.send_video(uid, BytesIO(video), txt, reply_markup=ikm)
51
+ else:
52
+ return await self.send_message(uid, txt, reply_markup=ikm)
53
+
54
+ async def wait_from(self, uid: int, topic: str, past: int = 0, timeout: int = 10) -> str | None:
55
+ fltr = chat(uid)
56
+ if topic == "phone":
57
+ fltr &= contact
58
+ handler = MessageHandler(self.got_msg, fltr)
59
+ # handler, g = self.add_handler(handler, 1)
60
+ g = 0
61
+ if g not in self.dispatcher.groups:
62
+ self.dispatcher.groups[g] = []
63
+ self.dispatcher.groups = OrderedDict(sorted(self.dispatcher.groups.items()))
64
+ self.dispatcher.groups[g].append(handler)
65
+ #
66
+ while past < timeout:
67
+ if txt := self.storage.session.state.get(uid, {}).pop(topic, None):
68
+ # self.remove_handler(handler)
69
+ self.dispatcher.groups[g].remove(handler)
70
+ #
71
+ return txt
72
+ await asyncio.sleep(1)
73
+ past += 1
74
+ return self.remove_handler(handler, g)
75
+
76
+ async def got_msg(self, _, msg: Message):
77
+ if tpc := self.storage.session.state.get(msg.from_user.id, {}).pop("waiting_for", None):
78
+ self.storage.session.state[msg.from_user.id][tpc] = msg.contact.phone_number if tpc == "phone" else msg.text
79
+
80
+ def rm_handler(self, uid: int):
81
+ for gi, grp in self.dispatcher.groups.items():
82
+ [self.remove_handler(h, gi) for h in grp if isinstance(h.filters, AndFilter) and uid in h.filters.base]
@@ -0,0 +1,11 @@
1
+ from x_auth.models import Session
2
+
3
+ from pyro_client.client.base import BaseClient, AuthTopic
4
+
5
+
6
+ class BotClient(BaseClient):
7
+ def __init__(self, sess: Session, _=None):
8
+ super().__init__(sess.id, sess.api.id, sess.api.hsh, bot_token=f"{sess.id}:{sess.is_bot}")
9
+
10
+ async def wait_auth_from(self, uid: int, topic: AuthTopic, past: int = 0, timeout: int = 60) -> str:
11
+ return await super().wait_from(uid, topic, past, timeout)
@@ -0,0 +1,180 @@
1
+ {
2
+ "7": 2,
3
+ "1": 1,
4
+ "20": 4,
5
+ "211": 2,
6
+ "212": 4,
7
+ "213": 4,
8
+ "216": 4,
9
+ "218": 4,
10
+ "220": 4,
11
+ "221": 4,
12
+ "222": 4,
13
+ "223": 4,
14
+ "224": 4,
15
+ "225": 4,
16
+ "226": 4,
17
+ "227": 4,
18
+ "228": 4,
19
+ "229": 4,
20
+ "230": 4,
21
+ "231": 4,
22
+ "232": 4,
23
+ "233": 4,
24
+ "234": 4,
25
+ "235": 4,
26
+ "236": 4,
27
+ "237": 4,
28
+ "238": 4,
29
+ "239": 4,
30
+ "240": 4,
31
+ "241": 4,
32
+ "242": 4,
33
+ "243": 4,
34
+ "244": 4,
35
+ "245": 4,
36
+ "246": 2,
37
+ "247": 4,
38
+ "248": 4,
39
+ "249": 4,
40
+ "250": 4,
41
+ "251": 4,
42
+ "252": 4,
43
+ "253": 4,
44
+ "254": 4,
45
+ "255": 4,
46
+ "256": 4,
47
+ "257": 4,
48
+ "258": 4,
49
+ "260": 4,
50
+ "261": 4,
51
+ "262": 4,
52
+ "263": 4,
53
+ "264": 4,
54
+ "265": 4,
55
+ "266": 4,
56
+ "267": 4,
57
+ "268": 4,
58
+ "269": 4,
59
+ "27": 4,
60
+ "290": 4,
61
+ "291": 4,
62
+ "297": 1,
63
+ "298": 4,
64
+ "299": 1,
65
+ "30": 4,
66
+ "31": 4,
67
+ "32": 4,
68
+ "33": 4,
69
+ "34": 4,
70
+ "36": 4,
71
+ "373": 2,
72
+ "374": 2,
73
+ "375": 2,
74
+ "380": 2,
75
+ "39": 4,
76
+ "40": 4,
77
+ "41": 4,
78
+ "43": 4,
79
+ "44": 4,
80
+ "45": 4,
81
+ "46": 4,
82
+ "47": 4,
83
+ "48": 4,
84
+ "49": 2,
85
+ "500": 1,
86
+ "501": 1,
87
+ "502": 1,
88
+ "503": 1,
89
+ "504": 1,
90
+ "505": 1,
91
+ "506": 1,
92
+ "507": 1,
93
+ "508": 1,
94
+ "509": 1,
95
+ "51": 1,
96
+ "52": 1,
97
+ "53": 1,
98
+ "54": 1,
99
+ "55": 1,
100
+ "56": 1,
101
+ "57": 1,
102
+ "58": 1,
103
+ "590": 1,
104
+ "591": 1,
105
+ "592": 1,
106
+ "593": 1,
107
+ "594": 1,
108
+ "595": 1,
109
+ "596": 1,
110
+ "597": 1,
111
+ "598": 1,
112
+ "599": 2,
113
+ "60": 5,
114
+ "61": 5,
115
+ "62": 5,
116
+ "63": 5,
117
+ "64": 5,
118
+ "65": 5,
119
+ "66": 5,
120
+ "672": 1,
121
+ "673": 5,
122
+ "674": 1,
123
+ "675": 5,
124
+ "676": 5,
125
+ "677": 5,
126
+ "678": 1,
127
+ "679": 1,
128
+ "680": 1,
129
+ "681": 1,
130
+ "682": 1,
131
+ "683": 1,
132
+ "684": 1,
133
+ "685": 1,
134
+ "686": 1,
135
+ "687": 1,
136
+ "688": 1,
137
+ "689": 1,
138
+ "690": 1,
139
+ "691": 1,
140
+ "692": 1,
141
+ "81": 5,
142
+ "82": 5,
143
+ "84": 5,
144
+ "86": 5,
145
+ "852": 5,
146
+ "853": 5,
147
+ "855": 5,
148
+ "856": 5,
149
+ "880": 5,
150
+ "886": 5,
151
+ "90": 4,
152
+ "91": 4,
153
+ "92": 4,
154
+ "93": 5,
155
+ "94": 5,
156
+ "95": 5,
157
+ "960": 5,
158
+ "961": 4,
159
+ "962": 4,
160
+ "963": 4,
161
+ "964": 2,
162
+ "965": 4,
163
+ "966": 4,
164
+ "967": 4,
165
+ "968": 4,
166
+ "970": 2,
167
+ "971": 4,
168
+ "972": 4,
169
+ "973": 4,
170
+ "974": 4,
171
+ "975": 5,
172
+ "976": 5,
173
+ "977": 5,
174
+ "992": 2,
175
+ "993": 2,
176
+ "995": 2,
177
+ "994": 2,
178
+ "996": 2,
179
+ "998": 2
180
+ }
@@ -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,38 @@
1
+ from tortoise.functions import Count
2
+ from x_auth.models import Proxy, Session, Username, App
3
+
4
+ from pyro_client.loader import WSToken
5
+
6
+
7
+ class SingleMeta(type):
8
+ _instances = {}
9
+
10
+ async def __call__(cls, uid: int | str, bot=None):
11
+ if cls not in cls._instances:
12
+ bt = None
13
+ if isinstance(uid, str):
14
+ if len(ub := uid.split(":")) == 2:
15
+ uid, bt = ub
16
+ if uid.isnumeric():
17
+ uid = int(uid)
18
+ sess = await cls._sess(uid, bt, None if bot else ...)
19
+ cls._instances[cls] = super().__call__(sess, bot)
20
+ return cls._instances[cls]
21
+
22
+ async def _sess(self, uid: int | str, bt: str = None, px: Proxy | None = ..., dc: int = 2) -> Session:
23
+ username, _ = await Username.get_or_create(**{"id" if isinstance(uid, int) else "username": uid})
24
+ if not (
25
+ session := await Session.get_or_none(user=username, api__dc=dc) or await Session.get_or_none(id=username.id)
26
+ ):
27
+ if px is Ellipsis:
28
+ await Proxy.load_list(WSToken)
29
+ # await Proxy.get_replaced(WSToken)
30
+ px = await Proxy.annotate(sc=Count("sessions")).filter(valid=True).order_by("sc", "-updated_at").first()
31
+ if username.phone:
32
+ # noinspection PyUnresolvedReferences
33
+ dc = dc or self.get_dc()
34
+ session = await Session.create(
35
+ id=username.id, api=await App[20373304], user=username, dc_id=dc, proxy=px, is_bot=bt
36
+ )
37
+ await session.fetch_related("proxy", "api")
38
+ return session
@@ -0,0 +1,199 @@
1
+ import logging
2
+ from json import load
3
+ from os.path import dirname
4
+
5
+ from pydantic import BaseModel, Field
6
+ from pyrogram import enums
7
+ from pyrogram.enums import ClientPlatform
8
+ from pyrogram.errors import BadRequest, SessionPasswordNeeded, AuthKeyUnregistered, Unauthorized
9
+ from pyrogram.session import Auth, Session as PyroSession
10
+ from pyrogram.types import Message, User, SentCode, InlineKeyboardButton, KeyboardButton
11
+ from x_auth.models import Session, Username, Proxy
12
+
13
+ from pyro_client.client.base import BaseClient, AuthTopic
14
+ from pyro_client.client.bot import BotClient
15
+ from pyro_client.loader import WSToken
16
+
17
+ vers: dict[ClientPlatform, str] = {
18
+ ClientPlatform.IOS: "18.5",
19
+ ClientPlatform.ANDROID: "16",
20
+ }
21
+
22
+
23
+ class Prx(BaseModel):
24
+ scheme: str = "socks5"
25
+ hostname: str = Field(validation_alias="host")
26
+ port: int
27
+ username: str
28
+ password: str
29
+
30
+
31
+ class UserClient(BaseClient):
32
+ bot: BotClient
33
+
34
+ def __init__(self, sess: Session, bot):
35
+ self.bot = bot
36
+ super().__init__(
37
+ sess.id,
38
+ sess.api.id,
39
+ sess.api.hsh,
40
+ device_model="iPhone 16e",
41
+ app_version=sess.api.ver,
42
+ system_version=vers.get(sess.api.platform),
43
+ client_platform=sess.api.platform,
44
+ proxy=sess.proxy and sess.proxy.dict(),
45
+ )
46
+
47
+ async def start(self, use_qr: bool = False, except_ids: list[int] = None):
48
+ if not self.bot.is_connected:
49
+ await self.bot.start()
50
+ # dcs = await self.bot.invoke(GetConfig())
51
+ try:
52
+ await super().start(use_qr=use_qr, except_ids=except_ids or [])
53
+ await self.send("im ok")
54
+ except AuthKeyUnregistered as e:
55
+ await self.storage.session.delete()
56
+ raise e
57
+ except (AuthKeyUnregistered, Unauthorized) as e:
58
+ raise e
59
+
60
+ async def ask_for(
61
+ self, topic: AuthTopic, question: str, btns: list[InlineKeyboardButton, KeyboardButton] = None
62
+ ) -> str:
63
+ if topic == "phone":
64
+ btns = btns or [] + [KeyboardButton("Phone", True)]
65
+ await self.receive(question, btns)
66
+ uid = int(self.storage.name)
67
+ self.bot.storage.session.state[uid] = {"waiting_for": topic}
68
+ return await self.bot.wait_auth_from(uid, topic)
69
+
70
+ async def receive(
71
+ self,
72
+ txt: str,
73
+ btns: list[InlineKeyboardButton | KeyboardButton] = None,
74
+ photo: bytes = None,
75
+ video: bytes = None,
76
+ ) -> Message:
77
+ return await self.bot.send(txt, int(self.storage.name), btns, photo, video)
78
+
79
+ def get_dc(self):
80
+ if not self.phone_number:
81
+ return 2
82
+ with open(f"{dirname(__file__)}/dc.json", "r") as file:
83
+ jsn = load(file)
84
+ for k, v in jsn.items():
85
+ if self.phone_number.startswith(k):
86
+ return v
87
+ return 2
88
+
89
+ async def authorize(self, sent_code: SentCode = None) -> User:
90
+ sent_code_desc = {
91
+ enums.SentCodeType.APP: "Telegram app",
92
+ enums.SentCodeType.SMS: "SMS",
93
+ enums.SentCodeType.CALL: "phone call",
94
+ enums.SentCodeType.FLASH_CALL: "phone flash call",
95
+ enums.SentCodeType.FRAGMENT_SMS: "Fragment SMS",
96
+ enums.SentCodeType.EMAIL_CODE: "email code",
97
+ }
98
+ # Step 1: Phone
99
+ if not self.phone_number:
100
+ user = await Username[self.storage.session.id]
101
+ if not (user.phone and (phone := str(user.phone))):
102
+ phone = await self.ask_for("phone", "Phone plz")
103
+ user.phone = phone
104
+ await user.save()
105
+ self.phone_number = phone
106
+ if (dc := self.get_dc()) != 2:
107
+ await self.session_update(dc)
108
+ if not self.phone_number:
109
+ await self.authorize()
110
+ try:
111
+ # user.phone = int(self.phone_number.strip("+ ").replace(" ", ""))
112
+ sent_code = await self.send_code(self.phone_number, True, False, True, False, True)
113
+ except BadRequest as e:
114
+ await self.send(e.MESSAGE)
115
+ self.phone_number = None
116
+ return await self.authorize(sent_code)
117
+ # Step 2: Code
118
+ if not self.phone_code:
119
+ if code := await self.ask_for("code", f"Code from {sent_code_desc[sent_code.type]}"):
120
+ self.phone_code = code.replace("_", "")
121
+ try:
122
+ signed_in = await self.sign_in(self.phone_number, sent_code.phone_code_hash, self.phone_code)
123
+ except BadRequest as e:
124
+ await self.receive(e.MESSAGE)
125
+ self.phone_code = None
126
+ return await self.authorize(sent_code)
127
+ except SessionPasswordNeeded:
128
+ # Step 2.1?: Cloud password
129
+ while True:
130
+ self.password = await self.ask_for(
131
+ "pass", f"Enter pass: (hint: {await self.get_password_hint()})"
132
+ )
133
+ try:
134
+ signed_in = await self.check_password(self.password)
135
+ break
136
+ except BadRequest as e:
137
+ await self.send(e.MESSAGE)
138
+ self.password = None
139
+ else:
140
+ raise Exception("User does not sent code")
141
+ if isinstance(signed_in, User):
142
+ await self.send("✅", self.bot.me.id)
143
+ await self.storage.save()
144
+ return signed_in
145
+
146
+ if not signed_in:
147
+ await self.receive("No registered such phone number")
148
+
149
+ async def stop(self, block: bool = True):
150
+ await super().stop(block)
151
+ await self.bot.stop(block)
152
+
153
+ async def session_update(self, dc: int):
154
+ await self.session.stop()
155
+
156
+ await self.storage.dc_id(dc)
157
+ await self.storage.auth_key(
158
+ await Auth(self, await self.storage.dc_id(), await self.storage.test_mode()).create()
159
+ )
160
+ self.session = PyroSession(
161
+ self, await self.storage.dc_id(), await self.storage.auth_key(), await self.storage.test_mode()
162
+ )
163
+ if not self.proxy and not self.storage.session.proxy:
164
+ await Proxy.load_list(WSToken)
165
+ prx: Proxy = await Proxy.filter(valid=True).order_by("-updated_at").first()
166
+ self.storage.session.proxy = prx
167
+ await self.storage.session.save()
168
+ self.proxy = prx.dict()
169
+ await self.session.start()
170
+
171
+
172
+ async def main():
173
+ from x_auth import models
174
+ from x_model import init_db
175
+
176
+ from pyro_client.loader import WSToken, PG_DSN
177
+
178
+ _ = await init_db(PG_DSN, models, True)
179
+
180
+ logging.basicConfig(level=logging.INFO)
181
+
182
+ await models.Proxy.load_list(WSToken)
183
+ # session = await models.Session.filter(is_bot__isnull=True).order_by("-date").prefetch_related("proxy").first()
184
+ bc: BotClient = await BotClient("xyncnetbot")
185
+ uc: UserClient = await UserClient(5547330178, bc)
186
+ # try:
187
+ await uc.start()
188
+ # except Exception as e:
189
+ # print(e.MESSAGE)
190
+ # await uc.send(e.MESSAGE)
191
+ # await uc.storage.session.delete()
192
+ # finally:
193
+ await uc.stop()
194
+
195
+
196
+ if __name__ == "__main__":
197
+ from asyncio import run
198
+
199
+ run(main())
@@ -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")
@@ -0,0 +1,130 @@
1
+ import time
2
+ from typing import Any, Literal
3
+
4
+ from pyrogram import raw, utils
5
+ from pyrogram.storage import Storage
6
+ from x_auth.models import Username, Version, Session, Peer, UpdateState
7
+
8
+
9
+ def get_input_peer(peer_id: int, access_hash: int, peer_type: Literal["user", "bot", "group", "channel", "supergroup"]):
10
+ if peer_type in ["user", "bot"]:
11
+ return raw.types.InputPeerUser(user_id=peer_id, access_hash=access_hash)
12
+
13
+ if peer_type == "group":
14
+ return raw.types.InputPeerChat(chat_id=-peer_id)
15
+
16
+ if peer_type in ["channel", "supergroup"]:
17
+ return raw.types.InputPeerChannel(channel_id=utils.get_channel_id(peer_id), access_hash=access_hash)
18
+
19
+ raise ValueError(f"Invalid peer type: {peer_type}")
20
+
21
+
22
+ class PgStorage(Storage):
23
+ VERSION = 1
24
+ USERNAME_TTL = 8 * 60 * 60
25
+ session: Session
26
+ # me_id: int
27
+
28
+ async def open(self):
29
+ # self.me_id = int((uid_dc := self.name.split("_")).pop(0))
30
+ self.session = await Session[self.name]
31
+
32
+ async def save(self):
33
+ await self.date(int(time.time()))
34
+
35
+ async def close(self): ...
36
+
37
+ async def delete(self):
38
+ await Session.filter(id=self.name).delete()
39
+
40
+ async def update_peers(self, peers: list[tuple[int, int, str, str]]):
41
+ for peer in peers:
42
+ uid, ac_hsh, typ, phn = peer
43
+ un, _ = await Username.get_or_create(phn and {"phone": phn}, id=uid)
44
+ await Peer.update_or_create(
45
+ {"username": un, "type": typ, "phone_number": phn}, session_id=self.name, id=ac_hsh
46
+ )
47
+
48
+ async def update_usernames(self, usernames: list[tuple[int, list[str]]]):
49
+ for telegram_id, user_list in usernames:
50
+ for username in user_list:
51
+ await Username.update_or_create({"username": username}, id=telegram_id)
52
+
53
+ async def get_peer_by_id(self, peer_id_or_username: int | str):
54
+ attr = "id" if isinstance(peer_id_or_username, int) else "username"
55
+ if not (peer := await Peer.get_or_none(session_id=self.name, **{"username__" + attr: peer_id_or_username})):
56
+ raise KeyError(f"User not found: {peer_id_or_username}")
57
+ if peer.last_update_on:
58
+ if abs(time.time() - peer.last_update_on.timestamp()) > self.USERNAME_TTL:
59
+ raise KeyError(f"Username expired: {peer_id_or_username}")
60
+ return get_input_peer(peer.username_id, peer.id, peer.type)
61
+
62
+ async def get_peer_by_username(self, username: str):
63
+ return await self.get_peer_by_id(username)
64
+
65
+ async def update_state(self, value: tuple[int, int, int, int, int] = object):
66
+ if value is None:
67
+ return await UpdateState.filter(session_id=self.name)
68
+ elif isinstance(value, int):
69
+ await UpdateState.filter(session_id=self.name, id=value).delete()
70
+ else:
71
+ sid, pts, qts, date, seq = value
72
+ await UpdateState.get_or_create(
73
+ {"pts": pts, "qts": qts, "date": date, "seq": seq}, session_id=self.name, id=sid
74
+ )
75
+
76
+ async def get_peer_by_phone_number(self, phone_number: str):
77
+ if not (
78
+ peer := await Peer.filter(session_id=self.name, phone_number=phone_number).values_list(
79
+ "id", "access_hash", "type"
80
+ )
81
+ ):
82
+ raise KeyError(f"Phone number not found: {phone_number}")
83
+ return get_input_peer(*peer)
84
+
85
+ async def _get(self, attr: str):
86
+ return await Session.get(id=self.name).values_list(attr, flat=True)
87
+
88
+ async def _set(self, attr: str, value):
89
+ # if "__" in attr:
90
+ # table, attr = attr.split("__")
91
+ # rel = await self.session.__getattribute__(table)
92
+ # rel.__setattr__(attr, value)
93
+ # await rel.save()
94
+ # else:
95
+ await Session.update_or_create({attr: value}, id=self.name)
96
+
97
+ async def _accessor(self, attr: str, value: Any = object):
98
+ if value is object:
99
+ return await self._get(attr)
100
+ else:
101
+ await self._set(attr, value)
102
+
103
+ async def dc_id(self, value: int = object):
104
+ return await self._accessor("dc_id", value)
105
+
106
+ async def api_id(self, value: int = object):
107
+ return await self._accessor("api_id", value)
108
+
109
+ async def test_mode(self, value: bool = object):
110
+ return await self._accessor("test_mode", value)
111
+
112
+ async def auth_key(self, value: bytes = object):
113
+ return await self._accessor("auth_key", value)
114
+
115
+ async def date(self, value: int = object):
116
+ return await self._accessor("date", value)
117
+
118
+ async def user_id(self, value: int = object):
119
+ return await self._accessor("user_id", value)
120
+
121
+ async def is_bot(self, value: bool = object):
122
+ return await self._accessor("is_bot", value)
123
+
124
+ @staticmethod
125
+ async def version(value: int = object):
126
+ if value is object:
127
+ ver = await Version.first()
128
+ return ver.number
129
+ else:
130
+ 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
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,18 @@
1
+ .env.sample
2
+ .gitignore
3
+ .pre-commit-config.yaml
4
+ makefile
5
+ pyproject.toml
6
+ pyro_client/loader.py
7
+ pyro_client/storage.py
8
+ pyro_client/client/base.py
9
+ pyro_client/client/bot.py
10
+ pyro_client/client/dc.json
11
+ pyro_client/client/file.py
12
+ pyro_client/client/single.py
13
+ pyro_client/client/user.py
14
+ pyrogram_client.egg-info/PKG-INFO
15
+ pyrogram_client.egg-info/SOURCES.txt
16
+ pyrogram_client.egg-info/dependency_links.txt
17
+ pyrogram_client.egg-info/requires.txt
18
+ pyrogram_client.egg-info/top_level.txt
@@ -0,0 +1,10 @@
1
+ kurigram
2
+ tgcrypto
3
+ xn-auth
4
+
5
+ [dev]
6
+ build
7
+ pre-commit
8
+ python-dotenv
9
+ setuptools-scm
10
+ twine
@@ -0,0 +1 @@
1
+ pyro_client
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+