CliRemote 1.5.0__py3-none-any.whl → 1.6.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CliRemote
3
- Version: 1.5.0
3
+ Version: 1.6.1
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
@@ -1,4 +1,4 @@
1
- cliremote-1.5.0.dist-info/licenses/LICENSE,sha256=O-0zMbcEi6wXz1DiSdVgzMlQjJcNqNe5KDv08uYzqR0,1055
1
+ cliremote-1.6.1.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,8 +8,8 @@ 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=RkYygo0t0jM-nFNVl4e_Hi3JY3jsSHNePG81_27_RNQ,10444
12
- remote/client_picker.py,sha256=3QYi1UQ8pZjqR5YU2oKgQlCYV7-1pSPWknCyRFMcFak,4428
11
+ remote/client_manager.py,sha256=lU4e7R_myESbk-uxpfXoro816BZ9TdQcbnuKeJ0rgn4,8908
12
+ remote/client_picker.py,sha256=DR-Q3Nt_7DoubqGKF0RfGiNtRUftNuvoXLO-MQtCSs0,1427
13
13
  remote/config.py,sha256=VK0e96gEINRViKIq99CYYuYyaVZTLtlWlPKKkBd41Cg,2377
14
14
  remote/device_manager.py,sha256=SUCONe1qa5jMHOMqqS27ATtv3CaqAT8cN9jNi7AI_Go,5813
15
15
  remote/file_sender.py,sha256=5_3ptTkoFejhJhaSyzh-8y5l_k7frxFq9LS_WL5jsGc,3657
@@ -31,7 +31,8 @@ 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.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,,
34
+ remote/utils/sqlite_utils.py,sha256=5i0oUXsBgKC_8qHZPJ-Gyhp9D1TwqKHVvuZRIhKpS6w,1260
35
+ cliremote-1.6.1.dist-info/METADATA,sha256=vdb8xaGfPGuE93R9w_LsiWLCoiE4e1H2518ojII1-X4,1202
36
+ cliremote-1.6.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
+ cliremote-1.6.1.dist-info/top_level.txt,sha256=yBZidJ6zCix_a2ubGlYaewvlzBFXWbckQt20dudxJ1E,7
38
+ cliremote-1.6.1.dist-info/RECORD,,
remote/client_manager.py CHANGED
@@ -5,253 +5,243 @@ import asyncio
5
5
  import logging
6
6
  import random
7
7
  import traceback
8
- from typing import Optional, Dict, List, Set, Tuple
8
+ from typing import Optional, Dict, List, Set
9
9
  from pyrogram import Client, errors
10
10
 
11
- # ============================================================
12
- # ⚙️ دقیق‌ترین لاگ برای دیباگ Pyrogram/SQLite
13
- # ============================================================
11
+ from .utils.sqlite_utils import (
12
+ ensure_dir, chmod_rw, cleanup_sqlite_sidecars, probe_sqlite
13
+ )
14
+
15
+ # =========================
16
+ # Logging
17
+ # =========================
14
18
  os.makedirs("logs", exist_ok=True)
15
19
  LOG_FILE = "logs/client_debug_log.txt"
16
20
  logger = logging.getLogger(__name__)
17
21
  logger.setLevel(logging.DEBUG)
18
-
19
22
  if not any(isinstance(h, logging.FileHandler) and getattr(h, "baseFilename", "").endswith(LOG_FILE) for h in logger.handlers):
20
23
  fh = logging.FileHandler(LOG_FILE, encoding="utf-8")
21
24
  fmt = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S")
22
25
  fh.setFormatter(fmt)
23
26
  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)
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)
34
38
 
35
39
  client_pool: Dict[str, Client] = {}
36
40
  client_locks: Dict[str, asyncio.Lock] = {}
37
41
 
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
42
+ def _strip_session_ext(x: str) -> str:
43
+ return x[:-8] if x.endswith(".session") else x
52
44
 
53
- # ============================================================
54
- # 💾 JSON helpers
55
- # ============================================================
45
+ # =========================
46
+ # JSON helpers
47
+ # =========================
56
48
  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}")
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)
60
52
  return None
61
53
  try:
62
- with open(file_path, "r", encoding="utf-8") as f:
54
+ with open(fp, "r", encoding="utf-8") as f:
63
55
  return json.load(f)
64
56
  except Exception as e:
65
- logger.error(f"{phone_number}: ⚠️ Error reading JSON - {type(e).__name__}: {e}")
57
+ logger.error("%s: error reading JSON: %s: %s", phone_number, type(e).__name__, e)
66
58
  return None
67
59
 
68
60
  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")
61
+ fp = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
62
+ ensure_dir(ACCOUNTS_DATA_FOLDER)
71
63
  try:
72
- with open(file_path, "w", encoding="utf-8") as f:
64
+ with open(fp, "w", encoding="utf-8") as f:
73
65
  json.dump(data, f, ensure_ascii=False, indent=4)
74
- logger.info(f"{phone_number}: 💾 Account data saved → {file_path}")
66
+ logger.info("%s: JSON saved: %s", phone_number, fp)
75
67
  except Exception as e:
76
- logger.error(f"{phone_number}: ⚠️ Error saving JSON - {type(e).__name__}: {e}")
68
+ logger.error("%s: error saving JSON: %s: %s", phone_number, type(e).__name__, e)
77
69
 
78
- # ============================================================
79
- # 🧱 ساخت کلاینت از JSON (بدون پسوند .session در name)
80
- # ============================================================
70
+ # =========================
71
+ # Client builder
72
+ # =========================
81
73
  def _make_client_from_json(phone_number: str) -> Optional[Client]:
82
74
  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}")
75
+ data = get_account_data(phone_number)
76
+ if not data:
86
77
  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")
78
+ session_base = data.get("session")
92
79
  if not session_base:
93
- logger.error(f"{phone_number}: Missing 'session' key in JSON → {data_path}")
80
+ logger.error("%s: JSON missing 'session'", phone_number)
94
81
  return None
95
82
 
96
- # Build session name WITHOUT .session extension
83
+ # absolute name WITHOUT .session
97
84
  if os.path.isabs(session_base) or os.path.dirname(session_base):
98
85
  session_name = _strip_session_ext(session_base)
99
86
  else:
100
87
  session_name = os.path.join(ACCOUNTS_FOLDER, _strip_session_ext(session_base))
101
88
 
102
- # Ensure parent dir exists
103
- _ensure_dir(os.path.dirname(session_name) or ".")
89
+ ensure_dir(os.path.dirname(session_name) or BASE_DIR)
104
90
 
105
- api_id = account_data.get("api_id")
106
- api_hash = account_data.get("api_hash")
91
+ api_id = data.get("api_id")
92
+ api_hash = data.get("api_hash")
107
93
  if not api_id or not api_hash:
108
- logger.error(f"{phone_number}: Missing API credentials in JSON {data_path}")
94
+ logger.error("%s: JSON missing api_id/api_hash", phone_number)
109
95
  return None
110
96
 
111
- # Separate workdir per account to avoid file collisions
112
- workdir = os.path.join("acc_temp", phone_number)
113
- _ensure_dir(workdir)
97
+ workdir = os.path.join(ACC_TEMP, phone_number)
98
+ ensure_dir(workdir)
114
99
 
115
100
  cli = Client(
116
- name=session_name, # <— NO .session here
101
+ name=session_name, # NO .session here
117
102
  api_id=int(api_id),
118
103
  api_hash=str(api_hash),
119
104
  sleep_threshold=30,
120
105
  workdir=workdir,
121
106
  no_updates=True,
122
107
  )
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})")
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)
128
111
  return cli
129
-
130
112
  except Exception as e:
131
113
  tb = traceback.format_exc(limit=3)
132
- logger.critical(f"{phone_number}: 💥 Error creating client - {type(e).__name__}: {e}\n{tb}")
114
+ logger.critical("%s: client build failed: %s: %s\n%s", phone_number, type(e).__name__, e, tb)
133
115
  return None
134
116
 
135
- # ============================================================
136
- # 🧠 دریافت/استارت کلاینت
137
- # ============================================================
117
+ # =========================
118
+ # Start client (safe)
119
+ # =========================
138
120
  async def get_or_start_client(phone_number: str) -> Optional[Client]:
139
121
  cli = client_pool.get(phone_number)
140
122
  try:
141
123
  if cli is not None and getattr(cli, "is_connected", False):
142
- logger.debug(f"{phone_number}: Already connected {getattr(cli, 'name', '?')}")
124
+ logger.debug("%s: already connected (%s)", phone_number, getattr(cli, "name", "?"))
143
125
  return cli
144
126
 
145
127
  cli = _make_client_from_json(phone_number)
146
128
  if cli is None:
147
- logger.error(f"{phone_number}: ❌ Could not build client (invalid JSON/session)")
148
129
  return None
149
130
 
150
- # Compute expected session file path for logging
151
131
  session_file = f"{cli.name}.session"
152
- logger.debug(f"{phone_number}: Expected session file → {session_file}")
132
+ parent = os.path.dirname(session_file) or BASE_DIR
133
+ ensure_dir(parent)
134
+
135
+ # sanitize permissions + sidecars
153
136
  if os.path.exists(session_file):
137
+ chmod_rw(session_file)
138
+ cleanup_sqlite_sidecars(cli.name)
139
+ else:
154
140
  try:
155
- size = os.path.getsize(session_file)
156
- logger.debug(f"{phone_number}: Session file exists ({size} bytes)")
141
+ os.chmod(parent, 0o777)
157
142
  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")
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)
161
149
 
162
- # Start with careful handling
163
150
  try:
164
151
  await cli.start()
165
- await asyncio.sleep(0.4) # brief pause helps SQLite on some FS
166
- logger.info(f"{phone_number}: Client started.")
152
+ await asyncio.sleep(0.4)
153
+ logger.info("%s: client started", phone_number)
167
154
  except errors.SessionPasswordNeeded:
168
155
  twofa = getattr(cli, "_twofa_password", None)
169
156
  if twofa:
170
157
  await cli.check_password(twofa)
171
- logger.info(f"{phone_number}: 2FA password applied.")
158
+ logger.info("%s: 2FA applied", phone_number)
172
159
  else:
173
- logger.error(f"{phone_number}: ⚠️ 2FA required but missing.")
160
+ logger.error("%s: 2FA required but missing", phone_number)
174
161
  return None
175
162
  except errors.AuthKeyDuplicated:
176
- logger.error(f"{phone_number}: AuthKeyDuplicated (session invalid).")
163
+ logger.error("%s: AuthKeyDuplicated (invalid session)", phone_number)
177
164
  return None
178
165
  except Exception as e:
179
166
  tb = traceback.format_exc(limit=3)
180
- logger.error(f"{phone_number}: Start failed - {type(e).__name__}: {e}\n{tb}")
167
+ logger.error("%s: start failed: %s: %s\n%s", phone_number, type(e).__name__, e, tb)
181
168
  return None
182
169
 
183
170
  client_pool[phone_number] = cli
184
171
  client_locks.setdefault(phone_number, asyncio.Lock())
185
172
  return cli
186
-
187
173
  except Exception as e:
188
174
  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}")
175
+ logger.critical("%s: fatal in get_or_start_client: %s: %s\n%s", phone_number, type(e).__name__, e, tb)
190
176
  return None
191
177
 
192
- # ============================================================
193
- # 🚀 Preload با فاصله ایمن
194
- # ============================================================
178
+ # =========================
179
+ # Preload
180
+ # =========================
195
181
  async def preload_clients(limit: Optional[int] = None) -> None:
196
182
  phones = list(get_active_accounts())
197
183
  if limit is not None:
198
184
  phones = phones[:max(0, int(limit))]
199
-
200
185
  if not phones:
201
- logger.info("⚙️ No accounts found for preload.")
186
+ logger.info("no accounts to preload")
202
187
  return
203
188
 
204
- logger.info(f"🚀 Preloading {len(phones)} clients...")
205
- ok, bad = 0, 0
206
-
189
+ logger.info("preloading %d client(s)...", len(phones))
190
+ ok = bad = 0
207
191
  for idx, phone in enumerate(phones, 1):
208
- logger.info(f"🔹 [{idx}/{len(phones)}] Loading client {phone}")
192
+ logger.info("[%d/%d] preload %s", idx, len(phones), phone)
209
193
  try:
210
194
  cli = await get_or_start_client(phone)
211
195
  if cli and getattr(cli, "is_connected", False):
212
196
  ok += 1
213
- logger.info(f"{phone}: ✅ Connected.")
214
197
  else:
215
198
  bad += 1
216
- logger.warning(f"{phone}: ❌ Not connected after start().")
217
199
  except Exception as e:
218
200
  bad += 1
219
- tb = traceback.format_exc(limit=3)
220
- logger.error(f"{phone}: ❌ Exception during preload - {type(e).__name__}: {e}\n{tb}")
221
-
201
+ logger.error("%s: exception during preload: %s: %s", phone, type(e).__name__, e)
222
202
  await asyncio.sleep(0.8 + random.uniform(0.1, 0.3))
203
+ logger.info("preload done: ok=%d fail=%d", ok, bad)
223
204
 
224
- logger.info(f"🎯 Preload completed: OK={ok} | FAIL={bad}")
225
-
226
- # ============================================================
227
- # 🧹 توقف تمام کلاینت‌ها
228
- # ============================================================
205
+ # =========================
206
+ # Stop all
207
+ # =========================
229
208
  async def stop_all_clients() -> None:
230
- logger.info("🧹 Stopping all clients...")
209
+ logger.info("stopping all clients...")
231
210
  for phone, cli in list(client_pool.items()):
232
211
  try:
233
212
  await cli.stop()
234
- logger.info(f"{phone}: 📴 Stopped successfully.")
213
+ logger.info("%s: stopped", phone)
235
214
  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}")
215
+ logger.warning("%s: stop error: %s: %s", phone, type(e).__name__, e)
238
216
  finally:
239
217
  client_pool.pop(phone, None)
240
218
  client_locks.pop(phone, None)
241
219
  await asyncio.sleep(0.2)
242
- logger.info(" All clients stopped cleanly.")
220
+ logger.info("all clients stopped")
243
221
 
244
- # ============================================================
245
- # 📋 لیست اکانت‌های فعال (بر اساس فایل‌های .session)
246
- # ============================================================
222
+ # =========================
223
+ # Enumerate accounts
224
+ # =========================
247
225
  def accounts() -> List[str]:
248
- accs: Set[str] = set()
249
226
  if not os.path.isdir(ACCOUNTS_FOLDER):
250
227
  return []
251
- for acc in os.listdir(ACCOUNTS_FOLDER):
252
- if acc.endswith(".session"):
253
- accs.add(acc.split(".")[0])
254
- return list(accs)
228
+ return [f[:-8] for f in os.listdir(ACCOUNTS_FOLDER) if f.endswith(".session")]
255
229
 
256
230
  def get_active_accounts() -> Set[str]:
257
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)
remote/client_picker.py CHANGED
@@ -1,109 +1,40 @@
1
- # remote/client_picker.py
2
1
  import random
3
- import asyncio
4
2
  import logging
5
- from typing import Optional, Callable, Iterable
3
+ from typing import Optional
4
+ from .client_manager import client_pool, get_active_accounts, get_or_start_client
6
5
 
7
6
  logger = logging.getLogger(__name__)
8
7
 
9
- # ---- وابستگی‌های اصلی
10
- # تلاش می‌کنیم client_manager را حتماً داشته باشیم
11
- from . import client_manager
12
-
13
- # get_active() را به‌صورت ایمن تعیین می‌کنیم:
14
- def _resolve_get_active() -> Callable[[], Iterable[str]]:
8
+ async def get_any_client(message=None) -> Optional[object]:
15
9
  """
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
- و در نهایت اگر هیچ‌کدام نبود، یک فانکشنِ خالی برمی‌گرداند.
10
+ یک کلاینت آماده برمی‌گرداند:
11
+ 1) اگر کلاینت متصل در pool هست، همان را برمی‌گرداند.
12
+ 2) وگرنه از بین active_accounts یکی را استارت می‌کند.
22
13
  """
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 = []
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
60
22
 
61
- if not acc_list:
62
- if message:
63
- try:
64
- await message.reply("⚠️ هیچ اکانت فعالی برای اتصال وجود ندارد.")
65
- except Exception:
66
- pass
67
- logger.warning("⚠️ هیچ اکانت فعالی در دسترس نیست.")
23
+ # Start one if needed
24
+ accs = list(get_active_accounts())
25
+ if not accs:
26
+ logger.warning("get_any_client: no active accounts")
68
27
  return None
69
28
 
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
-
29
+ random.shuffle(accs)
30
+ for phone in accs:
80
31
  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}")
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)
84
35
  return cli
85
- else:
86
- logger.warning(f"⚠️ اکانت {phone} وصل نیست یا کلاینت معتبر برنگشته.")
87
36
  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}")
37
+ logger.warning("get_any_client: failed start %s: %s: %s", phone, type(e).__name__, e)
108
38
 
109
- return None
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