remottxrea
Advanced tools
+1
-1
| Metadata-Version: 2.4 | ||
| Name: remottxrea | ||
| Version: 1.0.94 | ||
| Version: 1.0.95 | ||
| Summary: Remote client framework for Telegram automation using Pyrogram | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/MohammadAhmadi-R/remottxrea |
+1
-1
@@ -7,3 +7,3 @@ [build-system] | ||
| name = "remottxrea" | ||
| version = "1.0.94" | ||
| version = "1.0.95" | ||
| description = "Remote client framework for Telegram automation using Pyrogram" | ||
@@ -10,0 +10,0 @@ readme = "README.md" |
| Metadata-Version: 2.4 | ||
| Name: remottxrea | ||
| Version: 1.0.94 | ||
| Version: 1.0.95 | ||
| Summary: Remote client framework for Telegram automation using Pyrogram | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/MohammadAhmadi-R/remottxrea |
+1
-1
@@ -8,3 +8,3 @@ from setuptools import setup, find_packages | ||
| name="remottxrea", | ||
| version="1.0.94", | ||
| version="1.0.95", | ||
| author="MrAhmadiRad", | ||
@@ -11,0 +11,0 @@ author_email="mohammadahmadirad69@gmail.com", |
| # remottxrea/client/create_client.py | ||
| import os | ||
| import asyncio | ||
| from typing import Dict | ||
| from pyrogram import Client | ||
@@ -13,10 +16,110 @@ from pyrogram.errors import SessionPasswordNeeded | ||
| # ========================================================== | ||
| # GLOBAL SAFE CLIENT POOL (NO WATCHER / NO SQLITE LOCK) | ||
| # ========================================================== | ||
| class _ClientPool: | ||
| """ | ||
| Production-grade client pool | ||
| - race safe | ||
| - auto reconnect | ||
| - no watcher | ||
| """ | ||
| def __init__(self): | ||
| self.clients: Dict[str, Client] = {} | ||
| self._lock = asyncio.Lock() | ||
| self._running = False | ||
| self._reconnect_task = None | ||
| # ---------------- LOCK ---------------- | ||
| async def acquire(self): | ||
| await self._lock.acquire() | ||
| def release(self): | ||
| if self._lock.locked(): | ||
| self._lock.release() | ||
| def is_locked(self): | ||
| return self._lock.locked() | ||
| # ---------------- REGISTER ---------------- | ||
| async def register(self, phone: str, client: Client): | ||
| async with self._lock: | ||
| if phone in self.clients: | ||
| return | ||
| if not client.is_connected: | ||
| await client.start() | ||
| self.clients[phone] = client | ||
| # ---------------- AUTO RECONNECT LOOP ---------------- | ||
| async def _reconnect_loop(self): | ||
| while self._running: | ||
| await asyncio.sleep(15) | ||
| async with self._lock: | ||
| for phone, client in list(self.clients.items()): | ||
| try: | ||
| if not client.is_connected: | ||
| await client.start() | ||
| except Exception: | ||
| # اگر fail شد حذف نمیکنیم | ||
| # فقط اجازه میدیم سیکل بعدی دوباره تلاش کند | ||
| pass | ||
| async def start(self): | ||
| if self._running: | ||
| return | ||
| self._running = True | ||
| self._reconnect_task = asyncio.create_task( | ||
| self._reconnect_loop() | ||
| ) | ||
| async def stop(self): | ||
| self._running = False | ||
| if self._reconnect_task: | ||
| self._reconnect_task.cancel() | ||
| async with self._lock: | ||
| for client in self.clients.values(): | ||
| try: | ||
| await client.stop() | ||
| except Exception: | ||
| pass | ||
| self.clients.clear() | ||
| # Singleton | ||
| client_pool = _ClientPool() | ||
| # ========================================================== | ||
| # SESSION CREATOR | ||
| # ========================================================== | ||
| class SessionCreator: | ||
| def __init__(self): | ||
| self.pending = {} | ||
| self.pending = {} # phone -> {app, phone_code_hash} | ||
| # ---------- BUILD CLIENT ---------- | ||
| def build_client(self, phone, string_session=None): | ||
| # ====================================================== | ||
| # BUILD CLIENT (SAFE) | ||
| # ====================================================== | ||
| def build_client(self, phone: str, string_session: str | None = None): | ||
| saved_data = session_data_manager.load(phone) | ||
@@ -29,2 +132,4 @@ | ||
| api_id, api_hash = get_apis() | ||
| if string_session: | ||
@@ -37,4 +142,2 @@ session_name = string_session | ||
| api_id, api_hash = get_apis() | ||
| return Client( | ||
@@ -48,8 +151,13 @@ name=session_name, | ||
| lang_code=device["lang_code"], | ||
| in_memory=in_memory | ||
| in_memory=in_memory, | ||
| workdir=SESSIONS_DIR, | ||
| workers=1 # جلوگیری از race داخلی Pyrogram | ||
| ) | ||
| # ---------- SEND CODE ---------- | ||
| async def send_code(self, phone): | ||
| # ====================================================== | ||
| # SEND CODE | ||
| # ====================================================== | ||
| async def send_code(self, phone: str): | ||
| app = self.build_client(phone) | ||
@@ -66,5 +174,10 @@ | ||
| # ---------- LOGIN WITH CODE ---------- | ||
| async def login_with_code(self, phone, code): | ||
| return True | ||
| # ====================================================== | ||
| # LOGIN WITH CODE | ||
| # ====================================================== | ||
| async def login_with_code(self, phone: str, code: str): | ||
| data = self.pending.get(phone) | ||
@@ -86,16 +199,13 @@ if not data: | ||
| await self._save_session_data( | ||
| phone, | ||
| app, | ||
| password=None | ||
| ) | ||
| # ✅ ثبت مستقیم session بعد از لاگین | ||
| await self._finalize_login(phone, app, password=None) | ||
| await app.disconnect() | ||
| del self.pending[phone] | ||
| return True | ||
| # ---------- LOGIN WITH PASSWORD ---------- | ||
| async def login_with_password(self, phone, password): | ||
| # ====================================================== | ||
| # LOGIN WITH PASSWORD | ||
| # ====================================================== | ||
| async def login_with_password(self, phone: str, password: str): | ||
| data = self.pending.get(phone) | ||
@@ -109,21 +219,13 @@ if not data: | ||
| await self._save_session_data( | ||
| phone, | ||
| app, | ||
| password=password | ||
| ) | ||
| # ✅ ثبت مستقیم session بعد از لاگین | ||
| await self._finalize_login(phone, app, password=password) | ||
| await app.disconnect() | ||
| del self.pending[phone] | ||
| return True | ||
| # ---------- SAVE SESSION DATA ---------- | ||
| async def _save_session_data( | ||
| self, | ||
| phone, | ||
| app: Client, | ||
| password | ||
| ): | ||
| # ====================================================== | ||
| # FINALIZE LOGIN (CORE LOGIC) | ||
| # ====================================================== | ||
| async def _finalize_login(self, phone: str, app: Client, password): | ||
| me = await app.get_me() | ||
@@ -150,2 +252,13 @@ session_string = await app.export_session_string() | ||
| session_data_manager.save(phone, data) | ||
| # ذخیره JSON | ||
| session_data_manager.save(phone, data) | ||
| # ثبت مستقیم داخل pool | ||
| await client_pool.register(phone, app) | ||
| # پاک کردن pending | ||
| if phone in self.pending: | ||
| del self.pending[phone] | ||
| # استارت reconnect loop اگر قبلاً شروع نشده | ||
| await client_pool.start() |
| """ | ||
| Enterprise Add Account Handler | ||
| Compatible with MultiSessionRunner | ||
| Race-safe + Dynamic Cooldown | ||
@@ -12,3 +13,3 @@ """ | ||
| from ...core.logger import get_action_logger | ||
| from ...runner.multi_session_runner import runner | ||
| from ...runner.multi_session_runner import runner # ← مهم | ||
@@ -106,3 +107,2 @@ | ||
| await runner.acquire_pool_lock() | ||
| try: | ||
@@ -112,7 +112,5 @@ await self.creator.send_code(phone) | ||
| logger.exception(f"Send code failed → {e}") | ||
| runner.release_pool_lock() | ||
| return await message.reply_text("Failed to send code") | ||
| finally: | ||
| if runner.is_pool_locked(): | ||
| runner.release_pool_lock() | ||
| runner.release_pool_lock() | ||
@@ -155,3 +153,2 @@ state.update({ | ||
| await runner.acquire_pool_lock() | ||
| try: | ||
@@ -164,11 +161,12 @@ result = await self.creator.login_with_code( | ||
| logger.exception(f"Login error → {e}") | ||
| runner.release_pool_lock() | ||
| return await message.reply_text("Login failed") | ||
| finally: | ||
| if runner.is_pool_locked(): | ||
| runner.release_pool_lock() | ||
| runner.release_pool_lock() | ||
| if result is True: | ||
| logger.info("Login success") | ||
| # بعد از لاگین، watcher خودش sync میکند | ||
| self.states.pop(user_id, None) | ||
| return await message.reply_text( | ||
@@ -216,3 +214,2 @@ "Account added successfully ✅" | ||
| await runner.acquire_pool_lock() | ||
| try: | ||
@@ -225,3 +222,2 @@ success = await self.creator.login_with_password( | ||
| logger.exception(f"2FA error → {e}") | ||
| runner.release_pool_lock() | ||
| return await message.reply_text( | ||
@@ -231,8 +227,9 @@ "Password verification failed" | ||
| finally: | ||
| if runner.is_pool_locked(): | ||
| runner.release_pool_lock() | ||
| runner.release_pool_lock() | ||
| if success: | ||
| logger.info("Login success with 2FA") | ||
| self.states.pop(user_id, None) | ||
| return await message.reply_text( | ||
@@ -239,0 +236,0 @@ "Account added successfully ✅" |
@@ -9,3 +9,2 @@ # remottxrea/runner/multi_session_runner.py | ||
| from pyrogram.errors import FloodWait, RPCError | ||
| from watchfiles import awatch | ||
@@ -18,9 +17,7 @@ from ..config.main_config import SESSIONS_DIR | ||
| # ========================================================== | ||
| # MANAGED CLIENT | ||
| # ========================================================== | ||
| class ManagedClient: | ||
| """ | ||
| Wrapper around Pyrogram Client | ||
| Handles: | ||
| - auto reconnect | ||
| - per-client lock | ||
| """ | ||
@@ -51,6 +48,7 @@ def __init__(self, phone: str, client: Client): | ||
| # ========================================================== | ||
| # MULTI SESSION RUNNER (NO WATCHER VERSION) | ||
| # ========================================================== | ||
| class MultiSessionRunner: | ||
| """ | ||
| Production-grade client pool | ||
| """ | ||
@@ -60,57 +58,72 @@ def __init__(self): | ||
| self._registry_lock = asyncio.Lock() | ||
| self._watch_task = None | ||
| self._running = False | ||
| # ================================================== | ||
| # INITIAL LOAD | ||
| # INITIAL LOAD (ONLY ON STARTUP) | ||
| # ================================================== | ||
| async def load_all(self): | ||
| async with self._registry_lock: | ||
| await self._sync_with_disk() | ||
| # start all once | ||
| for file in os.listdir(SESSIONS_DIR): | ||
| if not file.endswith("_data.json"): | ||
| continue | ||
| phone = file.replace("_data.json", "") | ||
| if phone in self.clients: | ||
| continue | ||
| client = self._create_client(phone) | ||
| self.clients[phone] = ManagedClient(phone, client) | ||
| await self._start_all() | ||
| # ================================================== | ||
| # START ALL CLIENTS | ||
| # REGISTER NEW SESSION (🔥 مهم) | ||
| # ================================================== | ||
| async def _start_all(self): | ||
| for phone, managed in self.clients.items(): | ||
| try: | ||
| await managed.start() | ||
| print(f"[POOL] Started → {phone}") | ||
| except Exception as e: | ||
| print(f"[POOL] Failed to start {phone}: {e}") | ||
| async def register_session(self, phone: str): | ||
| async with self._registry_lock: | ||
| if phone in self.clients: | ||
| return | ||
| client = self._create_client(phone) | ||
| managed = ManagedClient(phone, client) | ||
| self.clients[phone] = managed | ||
| await managed.start() | ||
| print(f"[POOL] Registered → {phone}") | ||
| # ================================================== | ||
| # SYNC WITH DISK (SAFE) | ||
| # REMOVE SESSION | ||
| # ================================================== | ||
| async def _sync_with_disk(self): | ||
| async def remove_session(self, phone: str): | ||
| disk_phones = set() | ||
| async with self._registry_lock: | ||
| for file in os.listdir(SESSIONS_DIR): | ||
| if file.endswith("_data.json"): | ||
| phone = file.replace("_data.json", "") | ||
| disk_phones.add(phone) | ||
| managed = self.clients.pop(phone, None) | ||
| current_phones = set(self.clients.keys()) | ||
| if managed: | ||
| await managed.stop() | ||
| print(f"[POOL] Removed → {phone}") | ||
| # ---- ADD NEW ---- | ||
| for phone in disk_phones - current_phones: | ||
| client = self._create_client(phone) | ||
| self.clients[phone] = ManagedClient(phone, client) | ||
| print(f"[WATCHER] Added → {phone}") | ||
| # ================================================== | ||
| # START ALL | ||
| # ================================================== | ||
| # ---- REMOVE ---- | ||
| for phone in current_phones - disk_phones: | ||
| managed = self.clients.pop(phone) | ||
| async def _start_all(self): | ||
| for phone, managed in self.clients.items(): | ||
| try: | ||
| await managed.stop() | ||
| except: | ||
| pass | ||
| print(f"[WATCHER] Removed → {phone}") | ||
| await managed.start() | ||
| print(f"[POOL] Started → {phone}") | ||
| except Exception as e: | ||
| print(f"[POOL] Failed → {phone}: {e}") | ||
@@ -134,23 +147,7 @@ # ================================================== | ||
| lang_code=device["lang_code"], | ||
| workdir=SESSIONS_DIR | ||
| workdir=SESSIONS_DIR, | ||
| workers=1 # جلوگیری از race داخلی | ||
| ) | ||
| # ================================================== | ||
| # WATCHER (RACE SAFE) | ||
| # ================================================== | ||
| async def start_watcher(self): | ||
| if self._watch_task: | ||
| return | ||
| async def watcher(): | ||
| async for _ in awatch(SESSIONS_DIR): | ||
| async with self._registry_lock: | ||
| await self._sync_with_disk() | ||
| self._watch_task = asyncio.create_task(watcher()) | ||
| print("🔥 Session watcher started") | ||
| # ================================================== | ||
| # RUN ACTION SAFELY | ||
@@ -215,3 +212,3 @@ # ================================================== | ||
| # ================================================== | ||
| # SAFE LOGIN LOCK (NO SQLITE LOCK) | ||
| # SAFE LOGIN LOCK | ||
| # ================================================== | ||
@@ -223,2 +220,10 @@ | ||
| def release_pool_lock(self): | ||
| self._registry_lock.release() | ||
| if self._registry_lock.locked(): | ||
| self._registry_lock.release() | ||
| # ========================================================== | ||
| # 🔥 GLOBAL RUNNER INSTANCE (همین لازم داشتی) | ||
| # ========================================================== | ||
| runner = MultiSessionRunner() |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
142460
2.55%3834
1.67%