CliRemote 1.4.6__py3-none-any.whl → 1.5.0__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.
- {cliremote-1.4.6.dist-info → cliremote-1.5.0.dist-info}/METADATA +1 -1
- {cliremote-1.4.6.dist-info → cliremote-1.5.0.dist-info}/RECORD +6 -6
- remote/client_manager.py +134 -100
- {cliremote-1.4.6.dist-info → cliremote-1.5.0.dist-info}/WHEEL +0 -0
- {cliremote-1.4.6.dist-info → cliremote-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {cliremote-1.4.6.dist-info → cliremote-1.5.0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
cliremote-1.
|
1
|
+
cliremote-1.5.0.dist-info/licenses/LICENSE,sha256=O-0zMbcEi6wXz1DiSdVgzMlQjJcNqNe5KDv08uYzqR0,1055
|
2
2
|
remote/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
remote/account_manager.py,sha256=u8yqcyq7Vl8LZ1rNlrEfwlJEbJiBzoSAlL3z2hhTzyc,9829
|
4
4
|
remote/account_viewer.py,sha256=MQoH5lOz24651EjhlzVwi6O5hVUAT7Q5fFU6ZHjBlsM,4243
|
@@ -8,7 +8,7 @@ remote/batch_manager.py,sha256=jVGhYVwHMKJd7f7JxcWjKlwr03dq0RaGD1KdkyYdb00,1051
|
|
8
8
|
remote/block_manager.py,sha256=R7UaQigr-hTRtjxjG3OvJdKhvp0mDpLaESp3Of1AYhs,5692
|
9
9
|
remote/caption_manager.py,sha256=ekgcZ_D1q8C24WP18TXxlM5eWTknJmw-KNXDfqlsnEw,966
|
10
10
|
remote/cleaner.py,sha256=gvPtkosGsf7Eb3zmYyeC44xB4HtCxlzKH3e3qLuVos4,5742
|
11
|
-
remote/client_manager.py,sha256=
|
11
|
+
remote/client_manager.py,sha256=RkYygo0t0jM-nFNVl4e_Hi3JY3jsSHNePG81_27_RNQ,10444
|
12
12
|
remote/client_picker.py,sha256=3QYi1UQ8pZjqR5YU2oKgQlCYV7-1pSPWknCyRFMcFak,4428
|
13
13
|
remote/config.py,sha256=VK0e96gEINRViKIq99CYYuYyaVZTLtlWlPKKkBd41Cg,2377
|
14
14
|
remote/device_manager.py,sha256=SUCONe1qa5jMHOMqqS27ATtv3CaqAT8cN9jNi7AI_Go,5813
|
@@ -31,7 +31,7 @@ remote/speed_manager.py,sha256=fIWSQAP9qW8AHZtMZq0MrC4_nvxcTFU1SBU75kpRzB8,1115
|
|
31
31
|
remote/stop_manager.py,sha256=UXzKJTblEyQqCjp7fenvQ51Q96Unx05WeOiuFMdj25M,1151
|
32
32
|
remote/text_manager.py,sha256=C2wNSXPSCDu8NSD3RsfbKmUQMWOYd1B5N4tzy-Jsriw,2195
|
33
33
|
remote/username_manager.py,sha256=nMNdke-2FIv86xR1Y6rR-43oUoQu_3Khw8wEo54noXI,3388
|
34
|
-
cliremote-1.
|
35
|
-
cliremote-1.
|
36
|
-
cliremote-1.
|
37
|
-
cliremote-1.
|
34
|
+
cliremote-1.5.0.dist-info/METADATA,sha256=6kXa1qqSclZOY8X8LcZRi3iqgfG0lv-r5-OoWYMiNug,1202
|
35
|
+
cliremote-1.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
36
|
+
cliremote-1.5.0.dist-info/top_level.txt,sha256=yBZidJ6zCix_a2ubGlYaewvlzBFXWbckQt20dudxJ1E,7
|
37
|
+
cliremote-1.5.0.dist-info/RECORD,,
|
remote/client_manager.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
|
1
2
|
import os
|
2
3
|
import json
|
3
4
|
import asyncio
|
@@ -8,100 +9,80 @@ from typing import Optional, Dict, List, Set, Tuple
|
|
8
9
|
from pyrogram import Client, errors
|
9
10
|
|
10
11
|
# ============================================================
|
11
|
-
# ⚙️
|
12
|
+
# ⚙️ دقیقترین لاگ برای دیباگ Pyrogram/SQLite
|
12
13
|
# ============================================================
|
13
14
|
os.makedirs("logs", exist_ok=True)
|
14
|
-
|
15
|
+
LOG_FILE = "logs/client_debug_log.txt"
|
15
16
|
logger = logging.getLogger(__name__)
|
16
17
|
logger.setLevel(logging.DEBUG)
|
17
18
|
|
18
|
-
if not any(isinstance(h, logging.FileHandler) and h
|
19
|
-
fh = logging.FileHandler(
|
19
|
+
if not any(isinstance(h, logging.FileHandler) and getattr(h, "baseFilename", "").endswith(LOG_FILE) for h in logger.handlers):
|
20
|
+
fh = logging.FileHandler(LOG_FILE, encoding="utf-8")
|
20
21
|
fmt = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S")
|
21
22
|
fh.setFormatter(fmt)
|
22
23
|
logger.addHandler(fh)
|
23
24
|
|
24
|
-
logger.info("🧩 Client Manager
|
25
|
+
logger.info("🧩 Client Manager (DEBUG MODE) booted.")
|
25
26
|
|
26
27
|
# ============================================================
|
27
|
-
#
|
28
|
+
# 📁 مسیرها و استخرها
|
28
29
|
# ============================================================
|
29
|
-
client_pool: Dict[str, Client] = {}
|
30
|
-
client_locks: Dict[str, asyncio.Lock] = {}
|
31
|
-
|
32
30
|
ACCOUNTS_FOLDER = "acc"
|
33
31
|
ACCOUNTS_DATA_FOLDER = "acc_data"
|
34
32
|
os.makedirs(ACCOUNTS_FOLDER, exist_ok=True)
|
35
33
|
os.makedirs(ACCOUNTS_DATA_FOLDER, exist_ok=True)
|
36
34
|
|
35
|
+
client_pool: Dict[str, Client] = {}
|
36
|
+
client_locks: Dict[str, asyncio.Lock] = {}
|
37
|
+
|
37
38
|
# ============================================================
|
38
|
-
#
|
39
|
+
# 🔧 ابزارکها
|
39
40
|
# ============================================================
|
40
|
-
|
41
|
-
"""
|
42
|
-
ساخت یا گرفتن کلاینت از فایل سشن (با لاگهای بسیار دقیق برای تشخیص ارورهای SQLite)
|
43
|
-
"""
|
44
|
-
cli = client_pool.get(phone_number)
|
41
|
+
def _ensure_dir(path: str):
|
45
42
|
try:
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
43
|
+
os.makedirs(path, exist_ok=True)
|
44
|
+
except Exception as e:
|
45
|
+
logger.error(f"⚠️ Cannot create directory {path}: {type(e).__name__}: {e}")
|
86
46
|
|
87
|
-
|
88
|
-
|
89
|
-
|
47
|
+
def _strip_session_ext(session_base: str) -> str:
|
48
|
+
# "acc/123.session" -> "acc/123"; "123.session" -> "123"
|
49
|
+
if session_base.endswith(".session"):
|
50
|
+
return session_base[:-8]
|
51
|
+
return session_base
|
90
52
|
|
53
|
+
# ============================================================
|
54
|
+
# 💾 JSON helpers
|
55
|
+
# ============================================================
|
56
|
+
def get_account_data(phone_number: str) -> Optional[Dict]:
|
57
|
+
file_path = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
|
58
|
+
if not os.path.exists(file_path):
|
59
|
+
logger.warning(f"{phone_number}: ⚠️ Account JSON not found at {file_path}")
|
60
|
+
return None
|
61
|
+
try:
|
62
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
63
|
+
return json.load(f)
|
91
64
|
except Exception as e:
|
92
|
-
|
93
|
-
logger.critical(f"{phone_number}: 💥 Fatal error in get_or_start_client() → {type(e).__name__}: {e}\nTraceback:\n{tb}")
|
65
|
+
logger.error(f"{phone_number}: ⚠️ Error reading JSON - {type(e).__name__}: {e}")
|
94
66
|
return None
|
95
67
|
|
68
|
+
def save_account_data(phone_number: str, data: Dict) -> None:
|
69
|
+
_ensure_dir(ACCOUNTS_DATA_FOLDER)
|
70
|
+
file_path = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
|
71
|
+
try:
|
72
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
73
|
+
json.dump(data, f, ensure_ascii=False, indent=4)
|
74
|
+
logger.info(f"{phone_number}: 💾 Account data saved → {file_path}")
|
75
|
+
except Exception as e:
|
76
|
+
logger.error(f"{phone_number}: ⚠️ Error saving JSON - {type(e).__name__}: {e}")
|
96
77
|
|
97
78
|
# ============================================================
|
98
|
-
# 🧱 ساخت کلاینت از JSON
|
79
|
+
# 🧱 ساخت کلاینت از JSON (بدون پسوند .session در name)
|
99
80
|
# ============================================================
|
100
81
|
def _make_client_from_json(phone_number: str) -> Optional[Client]:
|
101
82
|
try:
|
102
83
|
data_path = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
|
103
84
|
if not os.path.exists(data_path):
|
104
|
-
logger.error(f"{phone_number}: ⚠️ JSON
|
85
|
+
logger.error(f"{phone_number}: ⚠️ Account JSON not found → {data_path}")
|
105
86
|
return None
|
106
87
|
|
107
88
|
with open(data_path, "r", encoding="utf-8") as f:
|
@@ -109,106 +90,159 @@ def _make_client_from_json(phone_number: str) -> Optional[Client]:
|
|
109
90
|
|
110
91
|
session_base = account_data.get("session")
|
111
92
|
if not session_base:
|
112
|
-
logger.error(f"{phone_number}: Missing 'session' in JSON → {data_path}")
|
93
|
+
logger.error(f"{phone_number}: Missing 'session' key in JSON → {data_path}")
|
113
94
|
return None
|
114
95
|
|
115
|
-
#
|
116
|
-
|
117
|
-
|
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)")
|
96
|
+
# Build session name WITHOUT .session extension
|
97
|
+
if os.path.isabs(session_base) or os.path.dirname(session_base):
|
98
|
+
session_name = _strip_session_ext(session_base)
|
131
99
|
else:
|
132
|
-
|
100
|
+
session_name = os.path.join(ACCOUNTS_FOLDER, _strip_session_ext(session_base))
|
101
|
+
|
102
|
+
# Ensure parent dir exists
|
103
|
+
_ensure_dir(os.path.dirname(session_name) or ".")
|
133
104
|
|
134
105
|
api_id = account_data.get("api_id")
|
135
106
|
api_hash = account_data.get("api_hash")
|
136
107
|
if not api_id or not api_hash:
|
137
|
-
logger.error(f"{phone_number}: Missing
|
108
|
+
logger.error(f"{phone_number}: Missing API credentials in JSON → {data_path}")
|
138
109
|
return None
|
139
110
|
|
111
|
+
# Separate workdir per account to avoid file collisions
|
112
|
+
workdir = os.path.join("acc_temp", phone_number)
|
113
|
+
_ensure_dir(workdir)
|
114
|
+
|
140
115
|
cli = Client(
|
141
|
-
name=
|
116
|
+
name=session_name, # <— NO .session here
|
142
117
|
api_id=int(api_id),
|
143
118
|
api_hash=str(api_hash),
|
144
119
|
sleep_threshold=30,
|
145
|
-
workdir=
|
146
|
-
no_updates=True
|
120
|
+
workdir=workdir,
|
121
|
+
no_updates=True,
|
147
122
|
)
|
148
123
|
|
149
124
|
if account_data.get("2fa_password"):
|
150
125
|
setattr(cli, "_twofa_password", account_data["2fa_password"])
|
151
126
|
|
127
|
+
logger.debug(f"{phone_number}: Prepared Client(name={session_name})")
|
152
128
|
return cli
|
153
129
|
|
154
130
|
except Exception as e:
|
155
131
|
tb = traceback.format_exc(limit=3)
|
156
|
-
logger.critical(f"{phone_number}: 💥 Error creating client
|
132
|
+
logger.critical(f"{phone_number}: 💥 Error creating client - {type(e).__name__}: {e}\n{tb}")
|
157
133
|
return None
|
158
134
|
|
135
|
+
# ============================================================
|
136
|
+
# 🧠 دریافت/استارت کلاینت
|
137
|
+
# ============================================================
|
138
|
+
async def get_or_start_client(phone_number: str) -> Optional[Client]:
|
139
|
+
cli = client_pool.get(phone_number)
|
140
|
+
try:
|
141
|
+
if cli is not None and getattr(cli, "is_connected", False):
|
142
|
+
logger.debug(f"{phone_number}: Already connected → {getattr(cli, 'name', '?')}")
|
143
|
+
return cli
|
144
|
+
|
145
|
+
cli = _make_client_from_json(phone_number)
|
146
|
+
if cli is None:
|
147
|
+
logger.error(f"{phone_number}: ❌ Could not build client (invalid JSON/session)")
|
148
|
+
return None
|
149
|
+
|
150
|
+
# Compute expected session file path for logging
|
151
|
+
session_file = f"{cli.name}.session"
|
152
|
+
logger.debug(f"{phone_number}: Expected session file → {session_file}")
|
153
|
+
if os.path.exists(session_file):
|
154
|
+
try:
|
155
|
+
size = os.path.getsize(session_file)
|
156
|
+
logger.debug(f"{phone_number}: Session file exists ({size} bytes)")
|
157
|
+
except Exception:
|
158
|
+
logger.debug(f"{phone_number}: Session file exists (size unknown)")
|
159
|
+
else:
|
160
|
+
logger.debug(f"{phone_number}: Session file will be created by Pyrogram")
|
161
|
+
|
162
|
+
# Start with careful handling
|
163
|
+
try:
|
164
|
+
await cli.start()
|
165
|
+
await asyncio.sleep(0.4) # brief pause helps SQLite on some FS
|
166
|
+
logger.info(f"{phone_number}: ✅ Client started.")
|
167
|
+
except errors.SessionPasswordNeeded:
|
168
|
+
twofa = getattr(cli, "_twofa_password", None)
|
169
|
+
if twofa:
|
170
|
+
await cli.check_password(twofa)
|
171
|
+
logger.info(f"{phone_number}: ✅ 2FA password applied.")
|
172
|
+
else:
|
173
|
+
logger.error(f"{phone_number}: ⚠️ 2FA required but missing.")
|
174
|
+
return None
|
175
|
+
except errors.AuthKeyDuplicated:
|
176
|
+
logger.error(f"{phone_number}: ❌ AuthKeyDuplicated (session invalid).")
|
177
|
+
return None
|
178
|
+
except Exception as e:
|
179
|
+
tb = traceback.format_exc(limit=3)
|
180
|
+
logger.error(f"{phone_number}: ❌ Start failed - {type(e).__name__}: {e}\n{tb}")
|
181
|
+
return None
|
182
|
+
|
183
|
+
client_pool[phone_number] = cli
|
184
|
+
client_locks.setdefault(phone_number, asyncio.Lock())
|
185
|
+
return cli
|
186
|
+
|
187
|
+
except Exception as e:
|
188
|
+
tb = traceback.format_exc(limit=3)
|
189
|
+
logger.critical(f"{phone_number}: 💥 Fatal error in get_or_start_client - {type(e).__name__}: {e}\n{tb}")
|
190
|
+
return None
|
159
191
|
|
160
192
|
# ============================================================
|
161
|
-
#
|
193
|
+
# 🚀 Preload با فاصله ایمن
|
162
194
|
# ============================================================
|
163
195
|
async def preload_clients(limit: Optional[int] = None) -> None:
|
164
196
|
phones = list(get_active_accounts())
|
165
197
|
if limit is not None:
|
166
198
|
phones = phones[:max(0, int(limit))]
|
167
199
|
|
168
|
-
|
200
|
+
if not phones:
|
201
|
+
logger.info("⚙️ No accounts found for preload.")
|
202
|
+
return
|
169
203
|
|
204
|
+
logger.info(f"🚀 Preloading {len(phones)} clients...")
|
170
205
|
ok, bad = 0, 0
|
206
|
+
|
171
207
|
for idx, phone in enumerate(phones, 1):
|
172
|
-
logger.info(f"🔹 [{idx}/{len(phones)}]
|
208
|
+
logger.info(f"🔹 [{idx}/{len(phones)}] Loading client {phone}")
|
173
209
|
try:
|
174
210
|
cli = await get_or_start_client(phone)
|
175
211
|
if cli and getattr(cli, "is_connected", False):
|
176
212
|
ok += 1
|
177
|
-
logger.info(f"{phone}: ✅ Connected
|
213
|
+
logger.info(f"{phone}: ✅ Connected.")
|
178
214
|
else:
|
179
215
|
bad += 1
|
180
216
|
logger.warning(f"{phone}: ❌ Not connected after start().")
|
181
217
|
except Exception as e:
|
182
218
|
bad += 1
|
183
219
|
tb = traceback.format_exc(limit=3)
|
184
|
-
logger.error(f"{phone}: ❌ Exception during preload
|
220
|
+
logger.error(f"{phone}: ❌ Exception during preload - {type(e).__name__}: {e}\n{tb}")
|
185
221
|
|
186
|
-
|
187
|
-
await asyncio.sleep(1.0)
|
222
|
+
await asyncio.sleep(0.8 + random.uniform(0.1, 0.3))
|
188
223
|
|
189
224
|
logger.info(f"🎯 Preload completed: OK={ok} | FAIL={bad}")
|
190
225
|
|
191
|
-
|
192
226
|
# ============================================================
|
193
|
-
# 🧹
|
227
|
+
# 🧹 توقف تمام کلاینتها
|
194
228
|
# ============================================================
|
195
229
|
async def stop_all_clients() -> None:
|
196
230
|
logger.info("🧹 Stopping all clients...")
|
197
231
|
for phone, cli in list(client_pool.items()):
|
198
232
|
try:
|
199
233
|
await cli.stop()
|
200
|
-
logger.info(f"{phone}: 📴 Stopped
|
234
|
+
logger.info(f"{phone}: 📴 Stopped successfully.")
|
201
235
|
except Exception as e:
|
202
236
|
tb = traceback.format_exc(limit=2)
|
203
|
-
logger.warning(f"{phone}: ⚠️ Error stopping client
|
237
|
+
logger.warning(f"{phone}: ⚠️ Error stopping client - {type(e).__name__}: {e}\n{tb}")
|
204
238
|
finally:
|
205
239
|
client_pool.pop(phone, None)
|
206
|
-
|
207
|
-
|
208
|
-
|
240
|
+
client_locks.pop(phone, None)
|
241
|
+
await asyncio.sleep(0.2)
|
242
|
+
logger.info("✅ All clients stopped cleanly.")
|
209
243
|
|
210
244
|
# ============================================================
|
211
|
-
#
|
245
|
+
# 📋 لیست اکانتهای فعال (بر اساس فایلهای .session)
|
212
246
|
# ============================================================
|
213
247
|
def accounts() -> List[str]:
|
214
248
|
accs: Set[str] = set()
|
File without changes
|
File without changes
|
File without changes
|