CliRemote 1.5.0__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.
Files changed (46) hide show
  1. {cliremote-1.5.0 → cliremote-1.6.0}/CliRemote.egg-info/PKG-INFO +1 -1
  2. {cliremote-1.5.0 → cliremote-1.6.0}/CliRemote.egg-info/SOURCES.txt +3 -1
  3. {cliremote-1.5.0 → cliremote-1.6.0}/PKG-INFO +1 -1
  4. {cliremote-1.5.0 → cliremote-1.6.0}/pyproject.toml +1 -1
  5. cliremote-1.6.0/remote/client_manager.py +231 -0
  6. cliremote-1.6.0/remote/utils/sqlite_utils.py +45 -0
  7. {cliremote-1.5.0 → cliremote-1.6.0}/setup.py +1 -1
  8. cliremote-1.5.0/remote/client_manager.py +0 -257
  9. {cliremote-1.5.0 → cliremote-1.6.0}/CliRemote.egg-info/dependency_links.txt +0 -0
  10. {cliremote-1.5.0 → cliremote-1.6.0}/CliRemote.egg-info/requires.txt +0 -0
  11. {cliremote-1.5.0 → cliremote-1.6.0}/CliRemote.egg-info/top_level.txt +0 -0
  12. {cliremote-1.5.0 → cliremote-1.6.0}/LICENSE +0 -0
  13. {cliremote-1.5.0 → cliremote-1.6.0}/MANIFEST.in +0 -0
  14. {cliremote-1.5.0 → cliremote-1.6.0}/README.md +0 -0
  15. {cliremote-1.5.0 → cliremote-1.6.0}/remote/__init__.py +0 -0
  16. {cliremote-1.5.0 → cliremote-1.6.0}/remote/account_manager.py +0 -0
  17. {cliremote-1.5.0 → cliremote-1.6.0}/remote/account_viewer.py +0 -0
  18. {cliremote-1.5.0 → cliremote-1.6.0}/remote/admin_manager.py +0 -0
  19. {cliremote-1.5.0 → cliremote-1.6.0}/remote/analytics_manager.py +0 -0
  20. {cliremote-1.5.0 → cliremote-1.6.0}/remote/batch_manager.py +0 -0
  21. {cliremote-1.5.0 → cliremote-1.6.0}/remote/block_manager.py +0 -0
  22. {cliremote-1.5.0 → cliremote-1.6.0}/remote/caption_manager.py +0 -0
  23. {cliremote-1.5.0 → cliremote-1.6.0}/remote/cleaner.py +0 -0
  24. {cliremote-1.5.0 → cliremote-1.6.0}/remote/client_picker.py +0 -0
  25. {cliremote-1.5.0 → cliremote-1.6.0}/remote/config.py +0 -0
  26. {cliremote-1.5.0 → cliremote-1.6.0}/remote/device_manager.py +0 -0
  27. {cliremote-1.5.0 → cliremote-1.6.0}/remote/file_sender.py +0 -0
  28. {cliremote-1.5.0 → cliremote-1.6.0}/remote/getcode_controller.py +0 -0
  29. {cliremote-1.5.0 → cliremote-1.6.0}/remote/health.py +0 -0
  30. {cliremote-1.5.0 → cliremote-1.6.0}/remote/help_menu.py +0 -0
  31. {cliremote-1.5.0 → cliremote-1.6.0}/remote/init.py +0 -0
  32. {cliremote-1.5.0 → cliremote-1.6.0}/remote/join_controller.py +0 -0
  33. {cliremote-1.5.0 → cliremote-1.6.0}/remote/joiner.py +0 -0
  34. {cliremote-1.5.0 → cliremote-1.6.0}/remote/leave_controller.py +0 -0
  35. {cliremote-1.5.0 → cliremote-1.6.0}/remote/lefter.py +0 -0
  36. {cliremote-1.5.0 → cliremote-1.6.0}/remote/mention_manager.py +0 -0
  37. {cliremote-1.5.0 → cliremote-1.6.0}/remote/precise_engine.py +0 -0
  38. {cliremote-1.5.0 → cliremote-1.6.0}/remote/profile_info.py +0 -0
  39. {cliremote-1.5.0 → cliremote-1.6.0}/remote/profile_media.py +0 -0
  40. {cliremote-1.5.0 → cliremote-1.6.0}/remote/profile_privacy.py +0 -0
  41. {cliremote-1.5.0 → cliremote-1.6.0}/remote/spammer.py +0 -0
  42. {cliremote-1.5.0 → cliremote-1.6.0}/remote/speed_manager.py +0 -0
  43. {cliremote-1.5.0 → cliremote-1.6.0}/remote/stop_manager.py +0 -0
  44. {cliremote-1.5.0 → cliremote-1.6.0}/remote/text_manager.py +0 -0
  45. {cliremote-1.5.0 → cliremote-1.6.0}/remote/username_manager.py +0 -0
  46. {cliremote-1.5.0 → cliremote-1.6.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CliRemote
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Remote client framework for Telegram automation using Pyrogram
5
5
  Home-page: https://github.com/MohammadAhmadi-R/CliRemote
6
6
  Author: MrAhmadiRad
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CliRemote
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Remote client framework for Telegram automation using Pyrogram
5
5
  Home-page: https://github.com/MohammadAhmadi-R/CliRemote
6
6
  Author: MrAhmadiRad
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "CliRemote"
7
- version = "1.5.0"
7
+ version = "1.6.0"
8
8
  description = "Remote client framework for Telegram automation using Pyrogram"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -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.5.0",
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,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())
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