CliRemote 1.4.6__py3-none-any.whl → 1.6.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CliRemote
3
- Version: 1.4.6
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
@@ -1,4 +1,4 @@
1
- cliremote-1.4.6.dist-info/licenses/LICENSE,sha256=O-0zMbcEi6wXz1DiSdVgzMlQjJcNqNe5KDv08uYzqR0,1055
1
+ cliremote-1.6.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=7u5Izqf8wEVOZqsoK-sewWNYSjaXK2YvQgE_d42GXNY,8883
11
+ remote/client_manager.py,sha256=EP2pVasUOUwjr4h-vO-LWWf9_rMCGAsFrzVvnGXJcNI,8250
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,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.4.6.dist-info/METADATA,sha256=ax7Qizh4htZJb4FURfPTniKtE5Co--pMkhOCobQodzw,1202
35
- cliremote-1.4.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
36
- cliremote-1.4.6.dist-info/top_level.txt,sha256=yBZidJ6zCix_a2ubGlYaewvlzBFXWbckQt20dudxJ1E,7
37
- cliremote-1.4.6.dist-info/RECORD,,
34
+ remote/utils/sqlite_utils.py,sha256=5i0oUXsBgKC_8qHZPJ-Gyhp9D1TwqKHVvuZRIhKpS6w,1260
35
+ cliremote-1.6.0.dist-info/METADATA,sha256=TVNkkckVN52498m8gUoc5SFnEYyZ1cKFE6wDUZh6GTk,1202
36
+ cliremote-1.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
+ cliremote-1.6.0.dist-info/top_level.txt,sha256=yBZidJ6zCix_a2ubGlYaewvlzBFXWbckQt20dudxJ1E,7
38
+ cliremote-1.6.0.dist-info/RECORD,,
remote/client_manager.py CHANGED
@@ -1,223 +1,231 @@
1
+
1
2
  import os
2
3
  import json
3
4
  import asyncio
4
5
  import logging
5
6
  import random
6
7
  import traceback
7
- from typing import Optional, Dict, List, Set, Tuple
8
+ from typing import Optional, Dict, List, Set
8
9
  from pyrogram import Client, errors
9
10
 
10
- # ============================================================
11
- # ⚙️ تنظیم لاگ دقیق و مجزا برای دیباگ دیتابیس‌ها
12
- # ============================================================
11
+ from .utils.sqlite_utils import (
12
+ ensure_dir, chmod_rw, cleanup_sqlite_sidecars, probe_sqlite
13
+ )
14
+
15
+ # =========================
16
+ # Logging
17
+ # =========================
13
18
  os.makedirs("logs", exist_ok=True)
14
- log_file = "logs/client_debug_log.txt"
19
+ LOG_FILE = "logs/client_debug_log.txt"
15
20
  logger = logging.getLogger(__name__)
16
21
  logger.setLevel(logging.DEBUG)
17
-
18
- if not any(isinstance(h, logging.FileHandler) and h.baseFilename.endswith(log_file) for h in logger.handlers):
19
- fh = logging.FileHandler(log_file, encoding="utf-8")
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")
20
24
  fmt = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S")
21
25
  fh.setFormatter(fmt)
22
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)
23
38
 
24
- logger.info("🧩 Client Manager started in DEBUG MODE.")
25
-
26
- # ============================================================
27
- # ⚙️ ساختارهای ذخیره‌سازی
28
- # ============================================================
29
39
  client_pool: Dict[str, Client] = {}
30
40
  client_locks: Dict[str, asyncio.Lock] = {}
31
41
 
32
- ACCOUNTS_FOLDER = "acc"
33
- ACCOUNTS_DATA_FOLDER = "acc_data"
34
- os.makedirs(ACCOUNTS_FOLDER, exist_ok=True)
35
- os.makedirs(ACCOUNTS_DATA_FOLDER, exist_ok=True)
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
36
59
 
37
- # ============================================================
38
- # 🧠 ساخت یا دریافت کلاینت
39
- # ============================================================
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
+ # =========================
40
120
  async def get_or_start_client(phone_number: str) -> Optional[Client]:
41
- """
42
- ساخت یا گرفتن کلاینت از فایل سشن (با لاگ‌های بسیار دقیق برای تشخیص ارورهای SQLite)
43
- """
44
121
  cli = client_pool.get(phone_number)
45
122
  try:
46
123
  if cli is not None and getattr(cli, "is_connected", False):
47
- logger.debug(f"{phone_number}: Already connected {cli.session_name}")
124
+ logger.debug("%s: already connected (%s)", phone_number, getattr(cli, "name", "?"))
48
125
  return cli
49
126
 
50
127
  cli = _make_client_from_json(phone_number)
51
128
  if cli is None:
52
- logger.error(f"{phone_number}: ❌ Client creation failed (make_client_from_json returned None)")
53
129
  return None
54
130
 
55
- # لاگ مسیر سشن
56
- session_db_path = f"{cli.session_name}.session"
57
- logger.debug(f"{phone_number}: Session file path → {session_db_path}")
131
+ session_file = f"{cli.name}.session"
132
+ parent = os.path.dirname(session_file) or BASE_DIR
133
+ ensure_dir(parent)
58
134
 
59
- # چک وجود فایل
60
- if not os.path.exists(session_db_path):
61
- logger.warning(f"{phone_number}: Session file missing → {session_db_path}")
135
+ # sanitize permissions + sidecars
136
+ if os.path.exists(session_file):
137
+ chmod_rw(session_file)
138
+ cleanup_sqlite_sidecars(cli.name)
62
139
  else:
63
- size = os.path.getsize(session_db_path)
64
- logger.debug(f"{phone_number}: Session file exists ({size} bytes)")
140
+ try:
141
+ os.chmod(parent, 0o777)
142
+ except Exception:
143
+ pass
65
144
 
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}")
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)
69
149
 
70
150
  try:
71
151
  await cli.start()
72
152
  await asyncio.sleep(0.4)
73
- logger.info(f"{phone_number}: Client started successfully")
153
+ logger.info("%s: client started", phone_number)
74
154
  except errors.SessionPasswordNeeded:
75
155
  twofa = getattr(cli, "_twofa_password", None)
76
156
  if twofa:
77
157
  await cli.check_password(twofa)
78
- logger.info(f"{phone_number}: 2FA password applied successfully.")
158
+ logger.info("%s: 2FA applied", phone_number)
79
159
  else:
80
- logger.error(f"{phone_number}: ⚠️ 2FA required but password missing.")
160
+ logger.error("%s: 2FA required but missing", phone_number)
81
161
  return None
162
+ except errors.AuthKeyDuplicated:
163
+ logger.error("%s: AuthKeyDuplicated (invalid session)", phone_number)
164
+ return None
82
165
  except Exception as e:
83
166
  tb = traceback.format_exc(limit=3)
84
- logger.error(f"{phone_number}: ❌ Pyrogram start() failed {type(e).__name__}: {e}\nTraceback:\n{tb}")
167
+ logger.error("%s: start failed: %s: %s\n%s", phone_number, type(e).__name__, e, tb)
85
168
  return None
86
169
 
87
170
  client_pool[phone_number] = cli
88
171
  client_locks.setdefault(phone_number, asyncio.Lock())
89
172
  return cli
90
-
91
- except Exception as e:
92
- tb = traceback.format_exc(limit=3)
93
- logger.critical(f"{phone_number}: 💥 Fatal error in get_or_start_client() → {type(e).__name__}: {e}\nTraceback:\n{tb}")
94
- return None
95
-
96
-
97
- # ============================================================
98
- # 🧱 ساخت کلاینت از JSON
99
- # ============================================================
100
- def _make_client_from_json(phone_number: str) -> Optional[Client]:
101
- try:
102
- data_path = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
103
- if not os.path.exists(data_path):
104
- logger.error(f"{phone_number}: ⚠️ JSON file not found → {data_path}")
105
- return None
106
-
107
- with open(data_path, "r", encoding="utf-8") as f:
108
- account_data = json.load(f)
109
-
110
- session_base = account_data.get("session")
111
- if not session_base:
112
- logger.error(f"{phone_number}: Missing 'session' in JSON → {data_path}")
113
- return None
114
-
115
- # مسیر دقیق سشن
116
- session_path = os.path.join(ACCOUNTS_FOLDER, session_base)
117
- if not session_path.endswith(".session"):
118
- session_path += ".session"
119
-
120
- # مسیر SQLite
121
- db_dir = os.path.dirname(os.path.abspath(session_path))
122
- if not os.path.exists(db_dir):
123
- logger.warning(f"{phone_number}: Creating missing folder for session → {db_dir}")
124
- os.makedirs(db_dir, exist_ok=True)
125
-
126
- logger.debug(f"{phone_number}: Final session path → {session_path}")
127
-
128
- # بررسی وجود فایل SQLite (قبل از start)
129
- if os.path.exists(session_path):
130
- logger.debug(f"{phone_number}: Session file already present ({os.path.getsize(session_path)} bytes)")
131
- else:
132
- logger.debug(f"{phone_number}: Session file will be created by Pyrogram")
133
-
134
- api_id = account_data.get("api_id")
135
- api_hash = account_data.get("api_hash")
136
- if not api_id or not api_hash:
137
- logger.error(f"{phone_number}: Missing api_id/api_hash in JSON → {data_path}")
138
- return None
139
-
140
- cli = Client(
141
- name=session_path,
142
- api_id=int(api_id),
143
- api_hash=str(api_hash),
144
- sleep_threshold=30,
145
- workdir=os.path.join("acc_temp", phone_number),
146
- no_updates=True
147
- )
148
-
149
- if account_data.get("2fa_password"):
150
- setattr(cli, "_twofa_password", account_data["2fa_password"])
151
-
152
- return cli
153
-
154
173
  except Exception as e:
155
174
  tb = traceback.format_exc(limit=3)
156
- logger.critical(f"{phone_number}: 💥 Error creating client instance {type(e).__name__}: {e}\nTraceback:\n{tb}")
175
+ logger.critical("%s: fatal in get_or_start_client: %s: %s\n%s", phone_number, type(e).__name__, e, tb)
157
176
  return None
158
177
 
159
-
160
- # ============================================================
161
- # 🧩 Preload Clients (دقیق)
162
- # ============================================================
178
+ # =========================
179
+ # Preload
180
+ # =========================
163
181
  async def preload_clients(limit: Optional[int] = None) -> None:
164
182
  phones = list(get_active_accounts())
165
183
  if limit is not None:
166
184
  phones = phones[:max(0, int(limit))]
185
+ if not phones:
186
+ logger.info("no accounts to preload")
187
+ return
167
188
 
168
- logger.info(f"🚀 Starting preload for {len(phones)} account(s)...")
169
-
170
- ok, bad = 0, 0
189
+ logger.info("preloading %d client(s)...", len(phones))
190
+ ok = bad = 0
171
191
  for idx, phone in enumerate(phones, 1):
172
- logger.info(f"🔹 [{idx}/{len(phones)}] Preloading {phone} ...")
192
+ logger.info("[%d/%d] preload %s", idx, len(phones), phone)
173
193
  try:
174
194
  cli = await get_or_start_client(phone)
175
195
  if cli and getattr(cli, "is_connected", False):
176
196
  ok += 1
177
- logger.info(f"{phone}: ✅ Connected OK.")
178
197
  else:
179
198
  bad += 1
180
- logger.warning(f"{phone}: ❌ Not connected after start().")
181
199
  except Exception as e:
182
200
  bad += 1
183
- tb = traceback.format_exc(limit=3)
184
- logger.error(f"{phone}: Exception during preload → {type(e).__name__}: {e}\n{tb}")
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)
185
204
 
186
- # فاصله ایمن بین استارت‌ها برای SQLite
187
- await asyncio.sleep(1.0)
188
-
189
- logger.info(f"🎯 Preload completed: OK={ok} | FAIL={bad}")
190
-
191
-
192
- # ============================================================
193
- # 🧹 Stop all
194
- # ============================================================
205
+ # =========================
206
+ # Stop all
207
+ # =========================
195
208
  async def stop_all_clients() -> None:
196
- logger.info("🧹 Stopping all clients...")
209
+ logger.info("stopping all clients...")
197
210
  for phone, cli in list(client_pool.items()):
198
211
  try:
199
212
  await cli.stop()
200
- logger.info(f"{phone}: 📴 Stopped cleanly.")
213
+ logger.info("%s: stopped", phone)
201
214
  except Exception as e:
202
- tb = traceback.format_exc(limit=2)
203
- logger.warning(f"{phone}: ⚠️ Error stopping client → {type(e).__name__}: {e}\n{tb}")
215
+ logger.warning("%s: stop error: %s: %s", phone, type(e).__name__, e)
204
216
  finally:
205
217
  client_pool.pop(phone, None)
206
- await asyncio.sleep(0.3)
207
- logger.info("✅ stop_all_clients finished.")
208
-
218
+ client_locks.pop(phone, None)
219
+ await asyncio.sleep(0.2)
220
+ logger.info("all clients stopped")
209
221
 
210
- # ============================================================
211
- # 🔍 کمکی‌ها
212
- # ============================================================
222
+ # =========================
223
+ # Enumerate accounts
224
+ # =========================
213
225
  def accounts() -> List[str]:
214
- accs: Set[str] = set()
215
226
  if not os.path.isdir(ACCOUNTS_FOLDER):
216
227
  return []
217
- for acc in os.listdir(ACCOUNTS_FOLDER):
218
- if acc.endswith(".session"):
219
- accs.add(acc.split(".")[0])
220
- return list(accs)
228
+ return [f[:-8] for f in os.listdir(ACCOUNTS_FOLDER) if f.endswith(".session")]
221
229
 
222
230
  def get_active_accounts() -> Set[str]:
223
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