CliRemote 1.4.6__tar.gz → 1.6.0__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.
- {cliremote-1.4.6 → cliremote-1.6.0}/CliRemote.egg-info/PKG-INFO +1 -1
- {cliremote-1.4.6 → cliremote-1.6.0}/CliRemote.egg-info/SOURCES.txt +3 -1
- {cliremote-1.4.6 → cliremote-1.6.0}/PKG-INFO +1 -1
- {cliremote-1.4.6 → cliremote-1.6.0}/pyproject.toml +1 -1
- cliremote-1.6.0/remote/client_manager.py +231 -0
- cliremote-1.6.0/remote/utils/sqlite_utils.py +45 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/setup.py +1 -1
- cliremote-1.4.6/remote/client_manager.py +0 -223
- {cliremote-1.4.6 → cliremote-1.6.0}/CliRemote.egg-info/dependency_links.txt +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/CliRemote.egg-info/requires.txt +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/CliRemote.egg-info/top_level.txt +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/LICENSE +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/MANIFEST.in +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/README.md +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/__init__.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/account_manager.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/account_viewer.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/admin_manager.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/analytics_manager.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/batch_manager.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/block_manager.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/caption_manager.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/cleaner.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/client_picker.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/config.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/device_manager.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/file_sender.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/getcode_controller.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/health.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/help_menu.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/init.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/join_controller.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/joiner.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/leave_controller.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/lefter.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/mention_manager.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/precise_engine.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/profile_info.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/profile_media.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/profile_privacy.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/spammer.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/speed_manager.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/stop_manager.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/text_manager.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/remote/username_manager.py +0 -0
- {cliremote-1.4.6 → cliremote-1.6.0}/setup.cfg +0 -0
@@ -36,6 +36,7 @@ setup.py
|
|
36
36
|
./remote/stop_manager.py
|
37
37
|
./remote/text_manager.py
|
38
38
|
./remote/username_manager.py
|
39
|
+
./remote/utils/sqlite_utils.py
|
39
40
|
CliRemote.egg-info/PKG-INFO
|
40
41
|
CliRemote.egg-info/SOURCES.txt
|
41
42
|
CliRemote.egg-info/dependency_links.txt
|
@@ -72,4 +73,5 @@ remote/spammer.py
|
|
72
73
|
remote/speed_manager.py
|
73
74
|
remote/stop_manager.py
|
74
75
|
remote/text_manager.py
|
75
|
-
remote/username_manager.py
|
76
|
+
remote/username_manager.py
|
77
|
+
remote/utils/sqlite_utils.py
|
@@ -0,0 +1,231 @@
|
|
1
|
+
|
2
|
+
import os
|
3
|
+
import json
|
4
|
+
import asyncio
|
5
|
+
import logging
|
6
|
+
import random
|
7
|
+
import traceback
|
8
|
+
from typing import Optional, Dict, List, Set
|
9
|
+
from pyrogram import Client, errors
|
10
|
+
|
11
|
+
from .utils.sqlite_utils import (
|
12
|
+
ensure_dir, chmod_rw, cleanup_sqlite_sidecars, probe_sqlite
|
13
|
+
)
|
14
|
+
|
15
|
+
# =========================
|
16
|
+
# Logging
|
17
|
+
# =========================
|
18
|
+
os.makedirs("logs", exist_ok=True)
|
19
|
+
LOG_FILE = "logs/client_debug_log.txt"
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
logger.setLevel(logging.DEBUG)
|
22
|
+
if not any(isinstance(h, logging.FileHandler) and getattr(h, "baseFilename", "").endswith(LOG_FILE) for h in logger.handlers):
|
23
|
+
fh = logging.FileHandler(LOG_FILE, encoding="utf-8")
|
24
|
+
fmt = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S")
|
25
|
+
fh.setFormatter(fmt)
|
26
|
+
logger.addHandler(fh)
|
27
|
+
logger.info("🧩 client_manager loaded (v1.6.0, DEBUG).")
|
28
|
+
|
29
|
+
# =========================
|
30
|
+
# Paths
|
31
|
+
# =========================
|
32
|
+
BASE_DIR = os.path.abspath(os.getcwd())
|
33
|
+
ACCOUNTS_FOLDER = os.path.join(BASE_DIR, "acc")
|
34
|
+
ACCOUNTS_DATA_FOLDER = os.path.join(BASE_DIR, "acc_data")
|
35
|
+
ACC_TEMP = os.path.join(BASE_DIR, "acc_temp")
|
36
|
+
for p in (ACCOUNTS_FOLDER, ACCOUNTS_DATA_FOLDER, ACC_TEMP):
|
37
|
+
ensure_dir(p)
|
38
|
+
|
39
|
+
client_pool: Dict[str, Client] = {}
|
40
|
+
client_locks: Dict[str, asyncio.Lock] = {}
|
41
|
+
|
42
|
+
def _strip_session_ext(x: str) -> str:
|
43
|
+
return x[:-8] if x.endswith(".session") else x
|
44
|
+
|
45
|
+
# =========================
|
46
|
+
# JSON helpers
|
47
|
+
# =========================
|
48
|
+
def get_account_data(phone_number: str) -> Optional[Dict]:
|
49
|
+
fp = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
|
50
|
+
if not os.path.exists(fp):
|
51
|
+
logger.warning("%s: account JSON not found: %s", phone_number, fp)
|
52
|
+
return None
|
53
|
+
try:
|
54
|
+
with open(fp, "r", encoding="utf-8") as f:
|
55
|
+
return json.load(f)
|
56
|
+
except Exception as e:
|
57
|
+
logger.error("%s: error reading JSON: %s: %s", phone_number, type(e).__name__, e)
|
58
|
+
return None
|
59
|
+
|
60
|
+
def save_account_data(phone_number: str, data: Dict) -> None:
|
61
|
+
fp = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
|
62
|
+
ensure_dir(ACCOUNTS_DATA_FOLDER)
|
63
|
+
try:
|
64
|
+
with open(fp, "w", encoding="utf-8") as f:
|
65
|
+
json.dump(data, f, ensure_ascii=False, indent=4)
|
66
|
+
logger.info("%s: JSON saved: %s", phone_number, fp)
|
67
|
+
except Exception as e:
|
68
|
+
logger.error("%s: error saving JSON: %s: %s", phone_number, type(e).__name__, e)
|
69
|
+
|
70
|
+
# =========================
|
71
|
+
# Client builder
|
72
|
+
# =========================
|
73
|
+
def _make_client_from_json(phone_number: str) -> Optional[Client]:
|
74
|
+
try:
|
75
|
+
data = get_account_data(phone_number)
|
76
|
+
if not data:
|
77
|
+
return None
|
78
|
+
session_base = data.get("session")
|
79
|
+
if not session_base:
|
80
|
+
logger.error("%s: JSON missing 'session'", phone_number)
|
81
|
+
return None
|
82
|
+
|
83
|
+
# absolute name WITHOUT .session
|
84
|
+
if os.path.isabs(session_base) or os.path.dirname(session_base):
|
85
|
+
session_name = _strip_session_ext(session_base)
|
86
|
+
else:
|
87
|
+
session_name = os.path.join(ACCOUNTS_FOLDER, _strip_session_ext(session_base))
|
88
|
+
|
89
|
+
ensure_dir(os.path.dirname(session_name) or BASE_DIR)
|
90
|
+
|
91
|
+
api_id = data.get("api_id")
|
92
|
+
api_hash = data.get("api_hash")
|
93
|
+
if not api_id or not api_hash:
|
94
|
+
logger.error("%s: JSON missing api_id/api_hash", phone_number)
|
95
|
+
return None
|
96
|
+
|
97
|
+
workdir = os.path.join(ACC_TEMP, phone_number)
|
98
|
+
ensure_dir(workdir)
|
99
|
+
|
100
|
+
cli = Client(
|
101
|
+
name=session_name, # NO .session here
|
102
|
+
api_id=int(api_id),
|
103
|
+
api_hash=str(api_hash),
|
104
|
+
sleep_threshold=30,
|
105
|
+
workdir=workdir,
|
106
|
+
no_updates=True,
|
107
|
+
)
|
108
|
+
if data.get("2fa_password"):
|
109
|
+
setattr(cli, "_twofa_password", data["2fa_password"])
|
110
|
+
logger.debug("%s: Prepared Client(name=%s)", phone_number, session_name)
|
111
|
+
return cli
|
112
|
+
except Exception as e:
|
113
|
+
tb = traceback.format_exc(limit=3)
|
114
|
+
logger.critical("%s: client build failed: %s: %s\n%s", phone_number, type(e).__name__, e, tb)
|
115
|
+
return None
|
116
|
+
|
117
|
+
# =========================
|
118
|
+
# Start client (safe)
|
119
|
+
# =========================
|
120
|
+
async def get_or_start_client(phone_number: str) -> Optional[Client]:
|
121
|
+
cli = client_pool.get(phone_number)
|
122
|
+
try:
|
123
|
+
if cli is not None and getattr(cli, "is_connected", False):
|
124
|
+
logger.debug("%s: already connected (%s)", phone_number, getattr(cli, "name", "?"))
|
125
|
+
return cli
|
126
|
+
|
127
|
+
cli = _make_client_from_json(phone_number)
|
128
|
+
if cli is None:
|
129
|
+
return None
|
130
|
+
|
131
|
+
session_file = f"{cli.name}.session"
|
132
|
+
parent = os.path.dirname(session_file) or BASE_DIR
|
133
|
+
ensure_dir(parent)
|
134
|
+
|
135
|
+
# sanitize permissions + sidecars
|
136
|
+
if os.path.exists(session_file):
|
137
|
+
chmod_rw(session_file)
|
138
|
+
cleanup_sqlite_sidecars(cli.name)
|
139
|
+
else:
|
140
|
+
try:
|
141
|
+
os.chmod(parent, 0o777)
|
142
|
+
except Exception:
|
143
|
+
pass
|
144
|
+
|
145
|
+
if os.path.exists(session_file) and not probe_sqlite(session_file):
|
146
|
+
logger.warning("%s: sqlite probe failed; retrying after cleanup", phone_number)
|
147
|
+
chmod_rw(session_file)
|
148
|
+
cleanup_sqlite_sidecars(cli.name)
|
149
|
+
|
150
|
+
try:
|
151
|
+
await cli.start()
|
152
|
+
await asyncio.sleep(0.4)
|
153
|
+
logger.info("%s: client started", phone_number)
|
154
|
+
except errors.SessionPasswordNeeded:
|
155
|
+
twofa = getattr(cli, "_twofa_password", None)
|
156
|
+
if twofa:
|
157
|
+
await cli.check_password(twofa)
|
158
|
+
logger.info("%s: 2FA applied", phone_number)
|
159
|
+
else:
|
160
|
+
logger.error("%s: 2FA required but missing", phone_number)
|
161
|
+
return None
|
162
|
+
except errors.AuthKeyDuplicated:
|
163
|
+
logger.error("%s: AuthKeyDuplicated (invalid session)", phone_number)
|
164
|
+
return None
|
165
|
+
except Exception as e:
|
166
|
+
tb = traceback.format_exc(limit=3)
|
167
|
+
logger.error("%s: start failed: %s: %s\n%s", phone_number, type(e).__name__, e, tb)
|
168
|
+
return None
|
169
|
+
|
170
|
+
client_pool[phone_number] = cli
|
171
|
+
client_locks.setdefault(phone_number, asyncio.Lock())
|
172
|
+
return cli
|
173
|
+
except Exception as e:
|
174
|
+
tb = traceback.format_exc(limit=3)
|
175
|
+
logger.critical("%s: fatal in get_or_start_client: %s: %s\n%s", phone_number, type(e).__name__, e, tb)
|
176
|
+
return None
|
177
|
+
|
178
|
+
# =========================
|
179
|
+
# Preload
|
180
|
+
# =========================
|
181
|
+
async def preload_clients(limit: Optional[int] = None) -> None:
|
182
|
+
phones = list(get_active_accounts())
|
183
|
+
if limit is not None:
|
184
|
+
phones = phones[:max(0, int(limit))]
|
185
|
+
if not phones:
|
186
|
+
logger.info("no accounts to preload")
|
187
|
+
return
|
188
|
+
|
189
|
+
logger.info("preloading %d client(s)...", len(phones))
|
190
|
+
ok = bad = 0
|
191
|
+
for idx, phone in enumerate(phones, 1):
|
192
|
+
logger.info("[%d/%d] preload %s", idx, len(phones), phone)
|
193
|
+
try:
|
194
|
+
cli = await get_or_start_client(phone)
|
195
|
+
if cli and getattr(cli, "is_connected", False):
|
196
|
+
ok += 1
|
197
|
+
else:
|
198
|
+
bad += 1
|
199
|
+
except Exception as e:
|
200
|
+
bad += 1
|
201
|
+
logger.error("%s: exception during preload: %s: %s", phone, type(e).__name__, e)
|
202
|
+
await asyncio.sleep(0.8 + random.uniform(0.1, 0.3))
|
203
|
+
logger.info("preload done: ok=%d fail=%d", ok, bad)
|
204
|
+
|
205
|
+
# =========================
|
206
|
+
# Stop all
|
207
|
+
# =========================
|
208
|
+
async def stop_all_clients() -> None:
|
209
|
+
logger.info("stopping all clients...")
|
210
|
+
for phone, cli in list(client_pool.items()):
|
211
|
+
try:
|
212
|
+
await cli.stop()
|
213
|
+
logger.info("%s: stopped", phone)
|
214
|
+
except Exception as e:
|
215
|
+
logger.warning("%s: stop error: %s: %s", phone, type(e).__name__, e)
|
216
|
+
finally:
|
217
|
+
client_pool.pop(phone, None)
|
218
|
+
client_locks.pop(phone, None)
|
219
|
+
await asyncio.sleep(0.2)
|
220
|
+
logger.info("all clients stopped")
|
221
|
+
|
222
|
+
# =========================
|
223
|
+
# Enumerate accounts
|
224
|
+
# =========================
|
225
|
+
def accounts() -> List[str]:
|
226
|
+
if not os.path.isdir(ACCOUNTS_FOLDER):
|
227
|
+
return []
|
228
|
+
return [f[:-8] for f in os.listdir(ACCOUNTS_FOLDER) if f.endswith(".session")]
|
229
|
+
|
230
|
+
def get_active_accounts() -> Set[str]:
|
231
|
+
return set(accounts())
|
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
import os
|
3
|
+
import sqlite3
|
4
|
+
import logging
|
5
|
+
|
6
|
+
logger = logging.getLogger(__name__)
|
7
|
+
|
8
|
+
SIDE_SUFFIXES = (".session-wal", ".session-shm", ".session-journal")
|
9
|
+
|
10
|
+
def ensure_dir(path: str):
|
11
|
+
try:
|
12
|
+
os.makedirs(path, exist_ok=True)
|
13
|
+
try:
|
14
|
+
os.chmod(path, 0o777)
|
15
|
+
except Exception:
|
16
|
+
pass
|
17
|
+
except Exception as e:
|
18
|
+
logger.error("Cannot create directory %s: %s: %s", path, type(e).__name__, e)
|
19
|
+
|
20
|
+
def chmod_rw(path: str):
|
21
|
+
try:
|
22
|
+
os.chmod(path, 0o666)
|
23
|
+
except Exception:
|
24
|
+
pass
|
25
|
+
|
26
|
+
def cleanup_sqlite_sidecars(db_without_ext: str):
|
27
|
+
base = f"{db_without_ext}.session"
|
28
|
+
for suf in ("-wal", "-shm", "-journal"):
|
29
|
+
f = f"{base}{suf}"
|
30
|
+
if os.path.exists(f):
|
31
|
+
try:
|
32
|
+
os.remove(f)
|
33
|
+
logger.debug("Removed sqlite sidecar: %s", f)
|
34
|
+
except Exception as e:
|
35
|
+
logger.debug("Cannot remove sidecar %s: %s", f, e)
|
36
|
+
|
37
|
+
def probe_sqlite(db_file: str) -> bool:
|
38
|
+
try:
|
39
|
+
conn = sqlite3.connect(db_file, timeout=1)
|
40
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
41
|
+
conn.close()
|
42
|
+
return True
|
43
|
+
except Exception as e:
|
44
|
+
logger.debug("SQLite probe failed for %s: %s: %s", db_file, type(e).__name__, e)
|
45
|
+
return False
|
@@ -5,7 +5,7 @@ with open("README.md", encoding="utf-8") as f:
|
|
5
5
|
|
6
6
|
setup(
|
7
7
|
name="CliRemote",
|
8
|
-
version="1.
|
8
|
+
version="1.6.0",
|
9
9
|
author="MrAhmadiRad",
|
10
10
|
author_email="mohammadahmadirad69@gmail.com",
|
11
11
|
description="A precise, async-safe, Telegram automation core (Python 3.8+)",
|
@@ -1,223 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
import json
|
3
|
-
import asyncio
|
4
|
-
import logging
|
5
|
-
import random
|
6
|
-
import traceback
|
7
|
-
from typing import Optional, Dict, List, Set, Tuple
|
8
|
-
from pyrogram import Client, errors
|
9
|
-
|
10
|
-
# ============================================================
|
11
|
-
# ⚙️ تنظیم لاگ دقیق و مجزا برای دیباگ دیتابیسها
|
12
|
-
# ============================================================
|
13
|
-
os.makedirs("logs", exist_ok=True)
|
14
|
-
log_file = "logs/client_debug_log.txt"
|
15
|
-
logger = logging.getLogger(__name__)
|
16
|
-
logger.setLevel(logging.DEBUG)
|
17
|
-
|
18
|
-
if not any(isinstance(h, logging.FileHandler) and h.baseFilename.endswith(log_file) for h in logger.handlers):
|
19
|
-
fh = logging.FileHandler(log_file, encoding="utf-8")
|
20
|
-
fmt = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S")
|
21
|
-
fh.setFormatter(fmt)
|
22
|
-
logger.addHandler(fh)
|
23
|
-
|
24
|
-
logger.info("🧩 Client Manager started in DEBUG MODE.")
|
25
|
-
|
26
|
-
# ============================================================
|
27
|
-
# ⚙️ ساختارهای ذخیرهسازی
|
28
|
-
# ============================================================
|
29
|
-
client_pool: Dict[str, Client] = {}
|
30
|
-
client_locks: Dict[str, asyncio.Lock] = {}
|
31
|
-
|
32
|
-
ACCOUNTS_FOLDER = "acc"
|
33
|
-
ACCOUNTS_DATA_FOLDER = "acc_data"
|
34
|
-
os.makedirs(ACCOUNTS_FOLDER, exist_ok=True)
|
35
|
-
os.makedirs(ACCOUNTS_DATA_FOLDER, exist_ok=True)
|
36
|
-
|
37
|
-
# ============================================================
|
38
|
-
# 🧠 ساخت یا دریافت کلاینت
|
39
|
-
# ============================================================
|
40
|
-
async def get_or_start_client(phone_number: str) -> Optional[Client]:
|
41
|
-
"""
|
42
|
-
ساخت یا گرفتن کلاینت از فایل سشن (با لاگهای بسیار دقیق برای تشخیص ارورهای SQLite)
|
43
|
-
"""
|
44
|
-
cli = client_pool.get(phone_number)
|
45
|
-
try:
|
46
|
-
if cli is not None and getattr(cli, "is_connected", False):
|
47
|
-
logger.debug(f"{phone_number}: Already connected → {cli.session_name}")
|
48
|
-
return cli
|
49
|
-
|
50
|
-
cli = _make_client_from_json(phone_number)
|
51
|
-
if cli is None:
|
52
|
-
logger.error(f"{phone_number}: ❌ Client creation failed (make_client_from_json returned None)")
|
53
|
-
return None
|
54
|
-
|
55
|
-
# لاگ مسیر سشن
|
56
|
-
session_db_path = f"{cli.session_name}.session"
|
57
|
-
logger.debug(f"{phone_number}: Session file path → {session_db_path}")
|
58
|
-
|
59
|
-
# چک وجود فایل
|
60
|
-
if not os.path.exists(session_db_path):
|
61
|
-
logger.warning(f"{phone_number}: Session file missing → {session_db_path}")
|
62
|
-
else:
|
63
|
-
size = os.path.getsize(session_db_path)
|
64
|
-
logger.debug(f"{phone_number}: Session file exists ({size} bytes)")
|
65
|
-
|
66
|
-
# چک دسترسی
|
67
|
-
if not os.access(session_db_path, os.R_OK | os.W_OK):
|
68
|
-
logger.warning(f"{phone_number}: ⚠️ No read/write permission for {session_db_path}")
|
69
|
-
|
70
|
-
try:
|
71
|
-
await cli.start()
|
72
|
-
await asyncio.sleep(0.4)
|
73
|
-
logger.info(f"{phone_number}: ✅ Client started successfully")
|
74
|
-
except errors.SessionPasswordNeeded:
|
75
|
-
twofa = getattr(cli, "_twofa_password", None)
|
76
|
-
if twofa:
|
77
|
-
await cli.check_password(twofa)
|
78
|
-
logger.info(f"{phone_number}: ✅ 2FA password applied successfully.")
|
79
|
-
else:
|
80
|
-
logger.error(f"{phone_number}: ⚠️ 2FA required but password missing.")
|
81
|
-
return None
|
82
|
-
except Exception as e:
|
83
|
-
tb = traceback.format_exc(limit=3)
|
84
|
-
logger.error(f"{phone_number}: ❌ Pyrogram start() failed → {type(e).__name__}: {e}\nTraceback:\n{tb}")
|
85
|
-
return None
|
86
|
-
|
87
|
-
client_pool[phone_number] = cli
|
88
|
-
client_locks.setdefault(phone_number, asyncio.Lock())
|
89
|
-
return cli
|
90
|
-
|
91
|
-
except Exception as e:
|
92
|
-
tb = traceback.format_exc(limit=3)
|
93
|
-
logger.critical(f"{phone_number}: 💥 Fatal error in get_or_start_client() → {type(e).__name__}: {e}\nTraceback:\n{tb}")
|
94
|
-
return None
|
95
|
-
|
96
|
-
|
97
|
-
# ============================================================
|
98
|
-
# 🧱 ساخت کلاینت از JSON
|
99
|
-
# ============================================================
|
100
|
-
def _make_client_from_json(phone_number: str) -> Optional[Client]:
|
101
|
-
try:
|
102
|
-
data_path = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
|
103
|
-
if not os.path.exists(data_path):
|
104
|
-
logger.error(f"{phone_number}: ⚠️ JSON file not found → {data_path}")
|
105
|
-
return None
|
106
|
-
|
107
|
-
with open(data_path, "r", encoding="utf-8") as f:
|
108
|
-
account_data = json.load(f)
|
109
|
-
|
110
|
-
session_base = account_data.get("session")
|
111
|
-
if not session_base:
|
112
|
-
logger.error(f"{phone_number}: Missing 'session' in JSON → {data_path}")
|
113
|
-
return None
|
114
|
-
|
115
|
-
# مسیر دقیق سشن
|
116
|
-
session_path = os.path.join(ACCOUNTS_FOLDER, session_base)
|
117
|
-
if not session_path.endswith(".session"):
|
118
|
-
session_path += ".session"
|
119
|
-
|
120
|
-
# مسیر SQLite
|
121
|
-
db_dir = os.path.dirname(os.path.abspath(session_path))
|
122
|
-
if not os.path.exists(db_dir):
|
123
|
-
logger.warning(f"{phone_number}: Creating missing folder for session → {db_dir}")
|
124
|
-
os.makedirs(db_dir, exist_ok=True)
|
125
|
-
|
126
|
-
logger.debug(f"{phone_number}: Final session path → {session_path}")
|
127
|
-
|
128
|
-
# بررسی وجود فایل SQLite (قبل از start)
|
129
|
-
if os.path.exists(session_path):
|
130
|
-
logger.debug(f"{phone_number}: Session file already present ({os.path.getsize(session_path)} bytes)")
|
131
|
-
else:
|
132
|
-
logger.debug(f"{phone_number}: Session file will be created by Pyrogram")
|
133
|
-
|
134
|
-
api_id = account_data.get("api_id")
|
135
|
-
api_hash = account_data.get("api_hash")
|
136
|
-
if not api_id or not api_hash:
|
137
|
-
logger.error(f"{phone_number}: Missing api_id/api_hash in JSON → {data_path}")
|
138
|
-
return None
|
139
|
-
|
140
|
-
cli = Client(
|
141
|
-
name=session_path,
|
142
|
-
api_id=int(api_id),
|
143
|
-
api_hash=str(api_hash),
|
144
|
-
sleep_threshold=30,
|
145
|
-
workdir=os.path.join("acc_temp", phone_number),
|
146
|
-
no_updates=True
|
147
|
-
)
|
148
|
-
|
149
|
-
if account_data.get("2fa_password"):
|
150
|
-
setattr(cli, "_twofa_password", account_data["2fa_password"])
|
151
|
-
|
152
|
-
return cli
|
153
|
-
|
154
|
-
except Exception as e:
|
155
|
-
tb = traceback.format_exc(limit=3)
|
156
|
-
logger.critical(f"{phone_number}: 💥 Error creating client instance → {type(e).__name__}: {e}\nTraceback:\n{tb}")
|
157
|
-
return None
|
158
|
-
|
159
|
-
|
160
|
-
# ============================================================
|
161
|
-
# 🧩 Preload Clients (دقیق)
|
162
|
-
# ============================================================
|
163
|
-
async def preload_clients(limit: Optional[int] = None) -> None:
|
164
|
-
phones = list(get_active_accounts())
|
165
|
-
if limit is not None:
|
166
|
-
phones = phones[:max(0, int(limit))]
|
167
|
-
|
168
|
-
logger.info(f"🚀 Starting preload for {len(phones)} account(s)...")
|
169
|
-
|
170
|
-
ok, bad = 0, 0
|
171
|
-
for idx, phone in enumerate(phones, 1):
|
172
|
-
logger.info(f"🔹 [{idx}/{len(phones)}] Preloading {phone} ...")
|
173
|
-
try:
|
174
|
-
cli = await get_or_start_client(phone)
|
175
|
-
if cli and getattr(cli, "is_connected", False):
|
176
|
-
ok += 1
|
177
|
-
logger.info(f"{phone}: ✅ Connected OK.")
|
178
|
-
else:
|
179
|
-
bad += 1
|
180
|
-
logger.warning(f"{phone}: ❌ Not connected after start().")
|
181
|
-
except Exception as e:
|
182
|
-
bad += 1
|
183
|
-
tb = traceback.format_exc(limit=3)
|
184
|
-
logger.error(f"{phone}: ❌ Exception during preload → {type(e).__name__}: {e}\n{tb}")
|
185
|
-
|
186
|
-
# فاصله ایمن بین استارتها برای SQLite
|
187
|
-
await asyncio.sleep(1.0)
|
188
|
-
|
189
|
-
logger.info(f"🎯 Preload completed: OK={ok} | FAIL={bad}")
|
190
|
-
|
191
|
-
|
192
|
-
# ============================================================
|
193
|
-
# 🧹 Stop all
|
194
|
-
# ============================================================
|
195
|
-
async def stop_all_clients() -> None:
|
196
|
-
logger.info("🧹 Stopping all clients...")
|
197
|
-
for phone, cli in list(client_pool.items()):
|
198
|
-
try:
|
199
|
-
await cli.stop()
|
200
|
-
logger.info(f"{phone}: 📴 Stopped cleanly.")
|
201
|
-
except Exception as e:
|
202
|
-
tb = traceback.format_exc(limit=2)
|
203
|
-
logger.warning(f"{phone}: ⚠️ Error stopping client → {type(e).__name__}: {e}\n{tb}")
|
204
|
-
finally:
|
205
|
-
client_pool.pop(phone, None)
|
206
|
-
await asyncio.sleep(0.3)
|
207
|
-
logger.info("✅ stop_all_clients finished.")
|
208
|
-
|
209
|
-
|
210
|
-
# ============================================================
|
211
|
-
# 🔍 کمکیها
|
212
|
-
# ============================================================
|
213
|
-
def accounts() -> List[str]:
|
214
|
-
accs: Set[str] = set()
|
215
|
-
if not os.path.isdir(ACCOUNTS_FOLDER):
|
216
|
-
return []
|
217
|
-
for acc in os.listdir(ACCOUNTS_FOLDER):
|
218
|
-
if acc.endswith(".session"):
|
219
|
-
accs.add(acc.split(".")[0])
|
220
|
-
return list(accs)
|
221
|
-
|
222
|
-
def get_active_accounts() -> Set[str]:
|
223
|
-
return set(accounts())
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|