CliRemote 1.5.0__tar.gz → 1.6.1__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.5.0 → cliremote-1.6.1}/CliRemote.egg-info/PKG-INFO +1 -1
- {cliremote-1.5.0 → cliremote-1.6.1}/CliRemote.egg-info/SOURCES.txt +3 -1
- {cliremote-1.5.0 → cliremote-1.6.1}/PKG-INFO +1 -1
- {cliremote-1.5.0 → cliremote-1.6.1}/pyproject.toml +1 -1
- cliremote-1.6.1/remote/client_manager.py +247 -0
- cliremote-1.6.1/remote/client_picker.py +40 -0
- cliremote-1.6.1/remote/utils/sqlite_utils.py +45 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/setup.py +1 -1
- cliremote-1.5.0/remote/client_manager.py +0 -257
- cliremote-1.5.0/remote/client_picker.py +0 -109
- {cliremote-1.5.0 → cliremote-1.6.1}/CliRemote.egg-info/dependency_links.txt +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/CliRemote.egg-info/requires.txt +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/CliRemote.egg-info/top_level.txt +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/LICENSE +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/MANIFEST.in +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/README.md +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/__init__.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/account_manager.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/account_viewer.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/admin_manager.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/analytics_manager.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/batch_manager.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/block_manager.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/caption_manager.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/cleaner.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/config.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/device_manager.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/file_sender.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/getcode_controller.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/health.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/help_menu.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/init.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/join_controller.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/joiner.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/leave_controller.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/lefter.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/mention_manager.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/precise_engine.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/profile_info.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/profile_media.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/profile_privacy.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/spammer.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/speed_manager.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/stop_manager.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/text_manager.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/remote/username_manager.py +0 -0
- {cliremote-1.5.0 → cliremote-1.6.1}/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,247 @@
|
|
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())
|
232
|
+
|
233
|
+
|
234
|
+
def remove_client_from_pool(phone_number: str) -> None:
|
235
|
+
"""
|
236
|
+
کلاینت را از pool خارج و stop میکند (بهصورت غیرمسدودکننده).
|
237
|
+
"""
|
238
|
+
cli = client_pool.get(phone_number)
|
239
|
+
if cli is not None:
|
240
|
+
try:
|
241
|
+
asyncio.create_task(cli.stop())
|
242
|
+
logger.info("%s: scheduled stop()", phone_number)
|
243
|
+
except Exception as e:
|
244
|
+
logger.warning("%s: stop() scheduling error: %s: %s", phone_number, type(e).__name__, e)
|
245
|
+
client_pool.pop(phone_number, None)
|
246
|
+
client_locks.pop(phone_number, None)
|
247
|
+
logger.info("%s: removed from client_pool and client_locks", phone_number)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import random
|
2
|
+
import logging
|
3
|
+
from typing import Optional
|
4
|
+
from .client_manager import client_pool, get_active_accounts, get_or_start_client
|
5
|
+
|
6
|
+
logger = logging.getLogger(__name__)
|
7
|
+
|
8
|
+
async def get_any_client(message=None) -> Optional[object]:
|
9
|
+
"""
|
10
|
+
یک کلاینت آماده برمیگرداند:
|
11
|
+
1) اگر کلاینت متصل در pool هست، همان را برمیگرداند.
|
12
|
+
2) وگرنه از بین active_accounts یکی را استارت میکند.
|
13
|
+
"""
|
14
|
+
# Use any connected client
|
15
|
+
for phone, cli in list(client_pool.items()):
|
16
|
+
try:
|
17
|
+
if getattr(cli, "is_connected", False):
|
18
|
+
logger.debug("get_any_client: using connected client %s", phone)
|
19
|
+
return cli
|
20
|
+
except Exception:
|
21
|
+
pass
|
22
|
+
|
23
|
+
# Start one if needed
|
24
|
+
accs = list(get_active_accounts())
|
25
|
+
if not accs:
|
26
|
+
logger.warning("get_any_client: no active accounts")
|
27
|
+
return None
|
28
|
+
|
29
|
+
random.shuffle(accs)
|
30
|
+
for phone in accs:
|
31
|
+
try:
|
32
|
+
cli = await get_or_start_client(phone)
|
33
|
+
if cli and getattr(cli, "is_connected", False):
|
34
|
+
logger.info("get_any_client: started %s", phone)
|
35
|
+
return cli
|
36
|
+
except Exception as e:
|
37
|
+
logger.warning("get_any_client: failed start %s: %s: %s", phone, type(e).__name__, e)
|
38
|
+
|
39
|
+
logger.error("get_any_client: could not get any client")
|
40
|
+
return None
|
@@ -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.1",
|
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,257 +0,0 @@
|
|
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, Tuple
|
9
|
-
from pyrogram import Client, errors
|
10
|
-
|
11
|
-
# ============================================================
|
12
|
-
# ⚙️ دقیقترین لاگ برای دیباگ Pyrogram/SQLite
|
13
|
-
# ============================================================
|
14
|
-
os.makedirs("logs", exist_ok=True)
|
15
|
-
LOG_FILE = "logs/client_debug_log.txt"
|
16
|
-
logger = logging.getLogger(__name__)
|
17
|
-
logger.setLevel(logging.DEBUG)
|
18
|
-
|
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")
|
21
|
-
fmt = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S")
|
22
|
-
fh.setFormatter(fmt)
|
23
|
-
logger.addHandler(fh)
|
24
|
-
|
25
|
-
logger.info("🧩 Client Manager (DEBUG MODE) booted.")
|
26
|
-
|
27
|
-
# ============================================================
|
28
|
-
# 📁 مسیرها و استخرها
|
29
|
-
# ============================================================
|
30
|
-
ACCOUNTS_FOLDER = "acc"
|
31
|
-
ACCOUNTS_DATA_FOLDER = "acc_data"
|
32
|
-
os.makedirs(ACCOUNTS_FOLDER, exist_ok=True)
|
33
|
-
os.makedirs(ACCOUNTS_DATA_FOLDER, exist_ok=True)
|
34
|
-
|
35
|
-
client_pool: Dict[str, Client] = {}
|
36
|
-
client_locks: Dict[str, asyncio.Lock] = {}
|
37
|
-
|
38
|
-
# ============================================================
|
39
|
-
# 🔧 ابزارکها
|
40
|
-
# ============================================================
|
41
|
-
def _ensure_dir(path: str):
|
42
|
-
try:
|
43
|
-
os.makedirs(path, exist_ok=True)
|
44
|
-
except Exception as e:
|
45
|
-
logger.error(f"⚠️ Cannot create directory {path}: {type(e).__name__}: {e}")
|
46
|
-
|
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
|
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)
|
64
|
-
except Exception as e:
|
65
|
-
logger.error(f"{phone_number}: ⚠️ Error reading JSON - {type(e).__name__}: {e}")
|
66
|
-
return None
|
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}")
|
77
|
-
|
78
|
-
# ============================================================
|
79
|
-
# 🧱 ساخت کلاینت از JSON (بدون پسوند .session در name)
|
80
|
-
# ============================================================
|
81
|
-
def _make_client_from_json(phone_number: str) -> Optional[Client]:
|
82
|
-
try:
|
83
|
-
data_path = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
|
84
|
-
if not os.path.exists(data_path):
|
85
|
-
logger.error(f"{phone_number}: ⚠️ Account JSON not found → {data_path}")
|
86
|
-
return None
|
87
|
-
|
88
|
-
with open(data_path, "r", encoding="utf-8") as f:
|
89
|
-
account_data = json.load(f)
|
90
|
-
|
91
|
-
session_base = account_data.get("session")
|
92
|
-
if not session_base:
|
93
|
-
logger.error(f"{phone_number}: Missing 'session' key in JSON → {data_path}")
|
94
|
-
return None
|
95
|
-
|
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)
|
99
|
-
else:
|
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 ".")
|
104
|
-
|
105
|
-
api_id = account_data.get("api_id")
|
106
|
-
api_hash = account_data.get("api_hash")
|
107
|
-
if not api_id or not api_hash:
|
108
|
-
logger.error(f"{phone_number}: Missing API credentials in JSON → {data_path}")
|
109
|
-
return None
|
110
|
-
|
111
|
-
# Separate workdir per account to avoid file collisions
|
112
|
-
workdir = os.path.join("acc_temp", phone_number)
|
113
|
-
_ensure_dir(workdir)
|
114
|
-
|
115
|
-
cli = Client(
|
116
|
-
name=session_name, # <— NO .session here
|
117
|
-
api_id=int(api_id),
|
118
|
-
api_hash=str(api_hash),
|
119
|
-
sleep_threshold=30,
|
120
|
-
workdir=workdir,
|
121
|
-
no_updates=True,
|
122
|
-
)
|
123
|
-
|
124
|
-
if account_data.get("2fa_password"):
|
125
|
-
setattr(cli, "_twofa_password", account_data["2fa_password"])
|
126
|
-
|
127
|
-
logger.debug(f"{phone_number}: Prepared Client(name={session_name})")
|
128
|
-
return cli
|
129
|
-
|
130
|
-
except Exception as e:
|
131
|
-
tb = traceback.format_exc(limit=3)
|
132
|
-
logger.critical(f"{phone_number}: 💥 Error creating client - {type(e).__name__}: {e}\n{tb}")
|
133
|
-
return None
|
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
|
191
|
-
|
192
|
-
# ============================================================
|
193
|
-
# 🚀 Preload با فاصله ایمن
|
194
|
-
# ============================================================
|
195
|
-
async def preload_clients(limit: Optional[int] = None) -> None:
|
196
|
-
phones = list(get_active_accounts())
|
197
|
-
if limit is not None:
|
198
|
-
phones = phones[:max(0, int(limit))]
|
199
|
-
|
200
|
-
if not phones:
|
201
|
-
logger.info("⚙️ No accounts found for preload.")
|
202
|
-
return
|
203
|
-
|
204
|
-
logger.info(f"🚀 Preloading {len(phones)} clients...")
|
205
|
-
ok, bad = 0, 0
|
206
|
-
|
207
|
-
for idx, phone in enumerate(phones, 1):
|
208
|
-
logger.info(f"🔹 [{idx}/{len(phones)}] Loading client {phone}")
|
209
|
-
try:
|
210
|
-
cli = await get_or_start_client(phone)
|
211
|
-
if cli and getattr(cli, "is_connected", False):
|
212
|
-
ok += 1
|
213
|
-
logger.info(f"{phone}: ✅ Connected.")
|
214
|
-
else:
|
215
|
-
bad += 1
|
216
|
-
logger.warning(f"{phone}: ❌ Not connected after start().")
|
217
|
-
except Exception as e:
|
218
|
-
bad += 1
|
219
|
-
tb = traceback.format_exc(limit=3)
|
220
|
-
logger.error(f"{phone}: ❌ Exception during preload - {type(e).__name__}: {e}\n{tb}")
|
221
|
-
|
222
|
-
await asyncio.sleep(0.8 + random.uniform(0.1, 0.3))
|
223
|
-
|
224
|
-
logger.info(f"🎯 Preload completed: OK={ok} | FAIL={bad}")
|
225
|
-
|
226
|
-
# ============================================================
|
227
|
-
# 🧹 توقف تمام کلاینتها
|
228
|
-
# ============================================================
|
229
|
-
async def stop_all_clients() -> None:
|
230
|
-
logger.info("🧹 Stopping all clients...")
|
231
|
-
for phone, cli in list(client_pool.items()):
|
232
|
-
try:
|
233
|
-
await cli.stop()
|
234
|
-
logger.info(f"{phone}: 📴 Stopped successfully.")
|
235
|
-
except Exception as e:
|
236
|
-
tb = traceback.format_exc(limit=2)
|
237
|
-
logger.warning(f"{phone}: ⚠️ Error stopping client - {type(e).__name__}: {e}\n{tb}")
|
238
|
-
finally:
|
239
|
-
client_pool.pop(phone, None)
|
240
|
-
client_locks.pop(phone, None)
|
241
|
-
await asyncio.sleep(0.2)
|
242
|
-
logger.info("✅ All clients stopped cleanly.")
|
243
|
-
|
244
|
-
# ============================================================
|
245
|
-
# 📋 لیست اکانتهای فعال (بر اساس فایلهای .session)
|
246
|
-
# ============================================================
|
247
|
-
def accounts() -> List[str]:
|
248
|
-
accs: Set[str] = set()
|
249
|
-
if not os.path.isdir(ACCOUNTS_FOLDER):
|
250
|
-
return []
|
251
|
-
for acc in os.listdir(ACCOUNTS_FOLDER):
|
252
|
-
if acc.endswith(".session"):
|
253
|
-
accs.add(acc.split(".")[0])
|
254
|
-
return list(accs)
|
255
|
-
|
256
|
-
def get_active_accounts() -> Set[str]:
|
257
|
-
return set(accounts())
|
@@ -1,109 +0,0 @@
|
|
1
|
-
# remote/client_picker.py
|
2
|
-
import random
|
3
|
-
import asyncio
|
4
|
-
import logging
|
5
|
-
from typing import Optional, Callable, Iterable
|
6
|
-
|
7
|
-
logger = logging.getLogger(__name__)
|
8
|
-
|
9
|
-
# ---- وابستگیهای اصلی
|
10
|
-
# تلاش میکنیم client_manager را حتماً داشته باشیم
|
11
|
-
from . import client_manager
|
12
|
-
|
13
|
-
# get_active() را بهصورت ایمن تعیین میکنیم:
|
14
|
-
def _resolve_get_active() -> Callable[[], Iterable[str]]:
|
15
|
-
"""
|
16
|
-
سعی میکند منبع معتبر لیست اکانتهای فعال را پیدا کند:
|
17
|
-
1) client_manager.get_active_accounts()
|
18
|
-
2) account_manager.get_active_accounts()
|
19
|
-
3) account_manager.accounts() (fallback)
|
20
|
-
4) client_manager.accounts() (fallback)
|
21
|
-
و در نهایت اگر هیچکدام نبود، یک فانکشنِ خالی برمیگرداند.
|
22
|
-
"""
|
23
|
-
# 1) client_manager.get_active_accounts
|
24
|
-
if hasattr(client_manager, "get_active_accounts"):
|
25
|
-
return client_manager.get_active_accounts
|
26
|
-
|
27
|
-
# 2) account_manager...
|
28
|
-
try:
|
29
|
-
from . import account_manager # ممکن است وجود نداشته باشد
|
30
|
-
if hasattr(account_manager, "get_active_accounts"):
|
31
|
-
return account_manager.get_active_accounts
|
32
|
-
if hasattr(account_manager, "accounts"):
|
33
|
-
return lambda: set(account_manager.accounts())
|
34
|
-
except Exception:
|
35
|
-
pass
|
36
|
-
|
37
|
-
# 3) fallback به client_manager.accounts
|
38
|
-
if hasattr(client_manager, "accounts"):
|
39
|
-
return lambda: set(client_manager.accounts())
|
40
|
-
|
41
|
-
# 4) آخرین fallback: لیست خالی
|
42
|
-
return lambda: set()
|
43
|
-
|
44
|
-
_get_active_accounts = _resolve_get_active()
|
45
|
-
|
46
|
-
|
47
|
-
async def get_any_client(message=None, max_attempts: int = 3) -> Optional[object]:
|
48
|
-
"""
|
49
|
-
تلاش برای گرفتن یک کلاینت فعال از بین اکانتها.
|
50
|
-
- تا `max_attempts` بار با اکانتهای تصادفی امتحان میکند.
|
51
|
-
- اگر موفق نشد، پیام خطا (در صورت وجود message) ارسال میکند،
|
52
|
-
سپس stop_all_clients() فراخوانی میشود و در نهایت None برمیگرداند.
|
53
|
-
"""
|
54
|
-
try:
|
55
|
-
acc_iter = _get_active_accounts()
|
56
|
-
acc_list = list(acc_iter) if not isinstance(acc_iter, (list, set, tuple)) else list(acc_iter)
|
57
|
-
except Exception as e:
|
58
|
-
logger.error(f"❌ نتوانستم لیست اکانتها را بگیرم: {type(e).__name__} - {e}")
|
59
|
-
acc_list = []
|
60
|
-
|
61
|
-
if not acc_list:
|
62
|
-
if message:
|
63
|
-
try:
|
64
|
-
await message.reply("⚠️ هیچ اکانت فعالی برای اتصال وجود ندارد.")
|
65
|
-
except Exception:
|
66
|
-
pass
|
67
|
-
logger.warning("⚠️ هیچ اکانت فعالی در دسترس نیست.")
|
68
|
-
return None
|
69
|
-
|
70
|
-
tried = set()
|
71
|
-
|
72
|
-
for attempt in range(1, max_attempts + 1):
|
73
|
-
if len(tried) == len(acc_list):
|
74
|
-
break
|
75
|
-
|
76
|
-
phone = random.choice([p for p in acc_list if p not in tried])
|
77
|
-
tried.add(phone)
|
78
|
-
logger.info(f"🔁 تلاش {attempt}/{max_attempts} برای اتصال با اکانت {phone}")
|
79
|
-
|
80
|
-
try:
|
81
|
-
cli = await client_manager.get_or_start_client(phone)
|
82
|
-
if cli and getattr(cli, "is_connected", True):
|
83
|
-
logger.info(f"✅ اتصال موفق با اکانت {phone}")
|
84
|
-
return cli
|
85
|
-
else:
|
86
|
-
logger.warning(f"⚠️ اکانت {phone} وصل نیست یا کلاینت معتبر برنگشته.")
|
87
|
-
except Exception as e:
|
88
|
-
logger.error(f"❌ خطا در اتصال {phone}: {type(e).__name__} - {e}")
|
89
|
-
try:
|
90
|
-
await asyncio.sleep(1)
|
91
|
-
except Exception:
|
92
|
-
pass
|
93
|
-
|
94
|
-
# شکست پس از تلاشها
|
95
|
-
error_msg = f"❌ هیچ کلاینت فعالی پس از {max_attempts} تلاش یافت نشد. در حال ریست کامل کلاینتها..."
|
96
|
-
if message:
|
97
|
-
try:
|
98
|
-
await message.reply(error_msg)
|
99
|
-
except Exception:
|
100
|
-
pass
|
101
|
-
logger.error(error_msg)
|
102
|
-
|
103
|
-
try:
|
104
|
-
await client_manager.stop_all_clients()
|
105
|
-
logger.warning("🔄 تمام کلاینتها ریست شدند (stop_all_clients فراخوانی شد).")
|
106
|
-
except Exception as e:
|
107
|
-
logger.error(f"⚠️ خطا در ریست کلاینتها: {type(e).__name__} - {e}")
|
108
|
-
|
109
|
-
return None
|
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
|