CliRemote 1.4.5__py3-none-any.whl → 1.5.0__py3-none-any.whl

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