CliRemote 1.4.3__py3-none-any.whl → 1.4.5__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.3
3
+ Version: 1.4.5
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,6 +1,6 @@
1
- cliremote-1.4.3.dist-info/licenses/LICENSE,sha256=O-0zMbcEi6wXz1DiSdVgzMlQjJcNqNe5KDv08uYzqR0,1055
1
+ cliremote-1.4.5.dist-info/licenses/LICENSE,sha256=O-0zMbcEi6wXz1DiSdVgzMlQjJcNqNe5KDv08uYzqR0,1055
2
2
  remote/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- remote/account_manager.py,sha256=gPgtEtJuH7-VhBl1xpOv3qaLmIiU3vOGj2rTAcgPRQ4,5707
3
+ remote/account_manager.py,sha256=u8yqcyq7Vl8LZ1rNlrEfwlJEbJiBzoSAlL3z2hhTzyc,9829
4
4
  remote/account_viewer.py,sha256=MQoH5lOz24651EjhlzVwi6O5hVUAT7Q5fFU6ZHjBlsM,4243
5
5
  remote/admin_manager.py,sha256=WiUUVmSs5JTUdXeSry8PkK_3TRemAdSZjm0G1ilAA-A,3532
6
6
  remote/analytics_manager.py,sha256=cQ_uATzOK345U1J88Fki3gOfNedS4Ur5NpLjazsxQkg,3625
@@ -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.3.dist-info/METADATA,sha256=I9DYhIjUBPsrdH5eyBKpYd7hJzTtm0ShoD0zOzwUlfk,1202
35
- cliremote-1.4.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
36
- cliremote-1.4.3.dist-info/top_level.txt,sha256=yBZidJ6zCix_a2ubGlYaewvlzBFXWbckQt20dudxJ1E,7
37
- cliremote-1.4.3.dist-info/RECORD,,
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,,
remote/account_manager.py CHANGED
@@ -1,161 +1,246 @@
1
- # antispam_core/account_manager.py
2
- import os, asyncio, json, logging
3
- from typing import Optional, Dict, List
1
+ import os
2
+ import json
3
+ import asyncio
4
+ import logging
5
+ import random
6
+ import traceback
7
+ from typing import Optional, Dict, List, Set, Tuple
4
8
  from pyrogram import Client, errors
5
- from .client_manager import (
6
- ACCOUNTS_FOLDER,
7
- get_account_data,
8
- save_account_data,
9
- stop_all_clients,
10
- accounts,
11
- client_pool,
12
- client_locks
13
- )
14
9
 
10
+ # ============================================================
11
+ # ⚙️ تنظیم لاگ دقیق برای دیباگ Pyrogram و SQLite
12
+ # ============================================================
13
+ os.makedirs("logs", exist_ok=True)
14
+ log_file = "logs/client_debug_log.txt"
15
15
  logger = logging.getLogger(__name__)
16
- login = {}
16
+ logger.setLevel(logging.DEBUG)
17
17
 
18
- # ==========================
19
- # افزودن اکانت جدید
20
- # ==========================
21
- async def add_account_cmd(message, get_app_info):
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")
20
+ fmt = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S")
21
+ fh.setFormatter(fmt)
22
+ logger.addHandler(fh)
23
+
24
+ logger.info("🧩 Client Manager started in DEBUG MODE.")
25
+
26
+ # ============================================================
27
+ # 🧱 ساختار داده‌ها
28
+ # ============================================================
29
+ client_pool: Dict[str, Client] = {}
30
+ client_locks: Dict[str, asyncio.Lock] = {}
31
+
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)
36
+
37
+
38
+ # ============================================================
39
+ # 🧠 ساخت یا دریافت کلاینت
40
+ # ============================================================
41
+ async def get_or_start_client(phone_number: str) -> Optional[Client]:
42
+ cli = client_pool.get(phone_number)
22
43
  try:
23
- parts = message.text.split(' ', 1)
24
- if len(parts) < 2:
25
- await message.reply('مثال: `add +989123456789`')
26
- return
27
-
28
- phone_number = parts[1].strip()
29
- session_file = os.path.join(ACCOUNTS_FOLDER, f'{phone_number}.session')
30
-
31
- if os.path.exists(session_file):
32
- await message.reply('این اکانت وجود دارد!')
33
- return
34
-
35
- global login
36
- api = get_app_info()
37
- if not api or len(api) < 2:
38
- await message.reply('مشکل در API!')
39
- return
40
-
41
- login['id'] = int(api[1])
42
- login['hash'] = api[0]
43
- login['number'] = phone_number
44
- login['api_data'] = {
45
- 'api_id': api[1],
46
- 'api_hash': api[0],
47
- 'phone_number': phone_number,
48
- 'session': phone_number,
49
- '2fa_password': None
50
- }
44
+ if cli is not None and getattr(cli, "is_connected", False):
45
+ logger.debug(f"{phone_number}: Already connected → {cli.session_name}")
46
+ return cli
47
+
48
+ cli = _make_client_from_json(phone_number)
49
+ if cli is None:
50
+ logger.error(f"{phone_number}: ❌ Could not build client (no JSON or invalid data)")
51
+ return None
52
+
53
+ session_db_path = f"{cli.session_name}.session"
54
+ logger.debug(f"{phone_number}: Session DB path → {session_db_path}")
55
+
56
+ if not os.path.exists(session_db_path):
57
+ logger.warning(f"{phone_number}: Session file not found → {session_db_path}")
58
+ else:
59
+ size = os.path.getsize(session_db_path)
60
+ logger.debug(f"{phone_number}: Session file exists ({size} bytes)")
61
+ if not os.access(session_db_path, os.R_OK | os.W_OK):
62
+ logger.warning(f"{phone_number}: ⚠️ No read/write permission for {session_db_path}")
51
63
 
52
64
  try:
53
- login['client'] = Client(name=session_file.replace('.session', ''), api_id=login['id'], api_hash=login['hash'])
54
- await login['client'].connect()
55
- login['response'] = await login['client'].send_code(phone_number)
56
- await message.reply(f'✅ کد تأیید به {phone_number} ارسال شد.\n`code 12345`')
57
- except errors.BadRequest as e:
58
- await message.reply(f'Bad request: {str(e)}')
59
- except errors.FloodWait as e:
60
- await message.reply(f'Flood wait: {e.value} sec')
65
+ await cli.start()
66
+ await asyncio.sleep(0.4)
67
+ logger.info(f"{phone_number}: Client started successfully.")
68
+ except errors.SessionPasswordNeeded:
69
+ twofa = getattr(cli, "_twofa_password", None)
70
+ if twofa:
71
+ await cli.check_password(twofa)
72
+ logger.info(f"{phone_number}: ✅ 2FA password applied.")
73
+ else:
74
+ logger.error(f"{phone_number}: ⚠️ 2FA required but missing.")
75
+ return None
76
+ except errors.AuthKeyDuplicated:
77
+ logger.error(f"{phone_number}: ❌ AuthKeyDuplicated (session invalid).")
78
+ return None
61
79
  except Exception as e:
62
- await message.reply(f'Connection error: {str(e)}')
63
- except Exception as e:
64
- await message.reply(f'خطا: {str(e)}')
80
+ tb = traceback.format_exc(limit=3)
81
+ logger.error(f"{phone_number}: Start failed - {type(e).__name__}: {e}\n{tb}")
82
+ return None
65
83
 
84
+ client_pool[phone_number] = cli
85
+ client_locks.setdefault(phone_number, asyncio.Lock())
86
+ return cli
66
87
 
67
- # ==========================
68
- # تأیید کد ورود
69
- # ==========================
70
- async def set_code_cmd(message):
71
- global login
72
- parts = message.text.split(' ', 1)
73
- if len(parts) < 2:
74
- await message.reply('`code 12345`')
75
- return
76
- code = parts[1].strip()
88
+ except Exception as e:
89
+ tb = traceback.format_exc(limit=3)
90
+ logger.critical(f"{phone_number}: 💥 Fatal error in get_or_start_client - {type(e).__name__}: {e}\n{tb}")
91
+ return None
77
92
 
93
+
94
+ # ============================================================
95
+ # 🧩 ساخت کلاینت از JSON
96
+ # ============================================================
97
+ def _make_client_from_json(phone_number: str) -> Optional[Client]:
78
98
  try:
79
- await login['client'].sign_in(login['number'], login['response'].phone_code_hash, code)
80
- await login['client'].disconnect()
81
- save_account_data(login['number'], login['api_data'])
82
- await message.reply(f"✅ اکانت اضافه شد!\n├ شماره: {login['number']}")
83
- login = {}
84
- except errors.SessionPasswordNeeded:
85
- await message.reply('🔒 لطفا رمز را با `pass your_password` بدهید')
86
- except errors.BadRequest:
87
- await message.reply('ورود با مشکل مواجه شد')
99
+ data_path = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
100
+ if not os.path.exists(data_path):
101
+ logger.error(f"{phone_number}: ⚠️ Account JSON not found → {data_path}")
102
+ return None
103
+
104
+ with open(data_path, "r", encoding="utf-8") as f:
105
+ account_data = json.load(f)
106
+
107
+ session_base = account_data.get("session")
108
+ if not session_base:
109
+ logger.error(f"{phone_number}: Missing 'session' key in JSON → {data_path}")
110
+ return None
111
+
112
+ session_path = os.path.join(ACCOUNTS_FOLDER, session_base)
113
+ if not session_path.endswith(".session"):
114
+ session_path += ".session"
115
+
116
+ os.makedirs(os.path.dirname(session_path), exist_ok=True)
117
+
118
+ logger.debug(f"{phone_number}: Final session path → {session_path}")
119
+
120
+ api_id = account_data.get("api_id")
121
+ api_hash = account_data.get("api_hash")
122
+ if not api_id or not api_hash:
123
+ logger.error(f"{phone_number}: Missing API credentials in JSON → {data_path}")
124
+ return None
125
+
126
+ cli = Client(
127
+ name=session_path,
128
+ api_id=int(api_id),
129
+ api_hash=str(api_hash),
130
+ sleep_threshold=30,
131
+ workdir=os.path.join("acc_temp", phone_number),
132
+ no_updates=True,
133
+ )
134
+
135
+ if account_data.get("2fa_password"):
136
+ setattr(cli, "_twofa_password", account_data["2fa_password"])
137
+
138
+ return cli
139
+
88
140
  except Exception as e:
89
- await message.reply(f'⚠️ خطا در ورود: {e}')
141
+ tb = traceback.format_exc(limit=3)
142
+ logger.critical(f"{phone_number}: 💥 Error creating client - {type(e).__name__}: {e}\n{tb}")
143
+ return None
90
144
 
91
145
 
92
- # ==========================
93
- # افزودن رمز دومرحله‌ای
94
- # ==========================
95
- async def set_2fa_cmd(message):
96
- global login
97
- parts = message.text.split(' ', 1)
98
- if len(parts) < 2:
99
- await message.reply('`pass my_password`')
146
+ # ============================================================
147
+ # 🚀 Preload با لاگ کامل
148
+ # ============================================================
149
+ async def preload_clients(limit: Optional[int] = None) -> None:
150
+ phones = list(get_active_accounts())
151
+ if limit is not None:
152
+ phones = phones[:max(0, int(limit))]
153
+
154
+ if not phones:
155
+ logger.info("⚙️ No accounts found for preload.")
100
156
  return
101
- password = parts[1].strip()
102
- try:
103
- await login['client'].check_password(password)
104
- login['api_data']['2fa_password'] = password
105
- save_account_data(login['number'], login['api_data'])
106
- await message.reply(f"✅ اکانت با موفقیت اضافه شد!\n├ شماره: {login['number']}")
107
- await login['client'].disconnect()
108
- login = {}
109
- except errors.BadRequest:
110
- await message.reply('رمز اشتباه است!')
111
- except Exception as e:
112
- await message.reply(f'⚠️ خطا در ثبت پسورد: {e}')
113
157
 
158
+ logger.info(f"🚀 Preloading {len(phones)} clients...")
159
+ ok, bad = 0, 0
114
160
 
115
- # ==========================
116
- # حذف یک اکانت خاص
117
- # ==========================
118
- def remove_client_from_pool(phone_number: str):
119
- cli = client_pool.get(phone_number)
120
- if cli:
161
+ for idx, phone in enumerate(phones, 1):
162
+ logger.info(f"🔹 [{idx}/{len(phones)}] Loading client {phone}")
121
163
  try:
122
- asyncio.create_task(cli.stop())
123
- except:
124
- pass
125
- client_pool.pop(phone_number, None)
126
- client_locks.pop(phone_number, None)
164
+ cli = await get_or_start_client(phone)
165
+ if cli and getattr(cli, "is_connected", False):
166
+ ok += 1
167
+ logger.info(f"{phone}: ✅ Connected.")
168
+ else:
169
+ bad += 1
170
+ logger.warning(f"{phone}: ❌ Not connected after start().")
171
+ except Exception as e:
172
+ bad += 1
173
+ tb = traceback.format_exc(limit=3)
174
+ logger.error(f"{phone}: ❌ Exception during preload - {type(e).__name__}: {e}\n{tb}")
127
175
 
176
+ await asyncio.sleep(1.0)
128
177
 
129
- async def delete_account_cmd(message):
178
+ logger.info(f"🎯 Preload completed: OK={ok} | FAIL={bad}")
179
+
180
+
181
+ # ============================================================
182
+ # 🧹 توقف تمام کلاینت‌ها
183
+ # ============================================================
184
+ async def stop_all_clients() -> None:
185
+ logger.info("🧹 Stopping all clients...")
186
+ for phone, cli in list(client_pool.items()):
187
+ try:
188
+ await cli.stop()
189
+ logger.info(f"{phone}: 📴 Stopped successfully.")
190
+ except Exception as e:
191
+ tb = traceback.format_exc(limit=2)
192
+ logger.warning(f"{phone}: ⚠️ Error stopping client - {type(e).__name__}: {e}\n{tb}")
193
+ finally:
194
+ client_pool.pop(phone, None)
195
+ await asyncio.sleep(0.3)
196
+ logger.info("✅ All clients stopped cleanly.")
197
+
198
+
199
+ # ============================================================
200
+ # 📦 مدیریت JSON داده‌های اکانت
201
+ # ============================================================
202
+ def get_account_data(phone_number: str) -> Optional[Dict]:
203
+ """
204
+ خواندن داده‌های JSON اکانت از acc_data/{phone}.json
205
+ """
206
+ file_path = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
207
+ if not os.path.exists(file_path):
208
+ logger.warning(f"{phone_number}: ⚠️ Account JSON not found at {file_path}")
209
+ return None
130
210
  try:
131
- phone_number = message.text.split()[1]
132
- main_path = f'{ACCOUNTS_FOLDER}/{phone_number}.session'
133
- remove_client_from_pool(phone_number)
134
- if os.path.isfile(main_path):
135
- os.unlink(main_path)
136
- await message.reply('<b>Account deleted successfully.</b>')
137
- else:
138
- await message.reply('<b>Account not found in database.</b>')
139
- except IndexError:
140
- await message.reply('Please enter phone number')
211
+ with open(file_path, "r", encoding="utf-8") as f:
212
+ return json.load(f)
141
213
  except Exception as e:
142
- await message.reply(f'<b>Error deleting account: {str(e)}</b>')
214
+ logger.error(f"{phone_number}: ⚠️ Error reading JSON - {type(e).__name__}: {e}")
215
+ return None
143
216
 
144
217
 
145
- # ==========================
146
- # حذف تمام اکانت‌ها
147
- # ==========================
148
- async def delete_all_accounts_cmd(message):
218
+ def save_account_data(phone_number: str, data: Dict) -> None:
219
+ """
220
+ ذخیره اطلاعات JSON اکانت در acc_data/{phone}.json
221
+ """
222
+ os.makedirs(ACCOUNTS_DATA_FOLDER, exist_ok=True)
223
+ file_path = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
149
224
  try:
150
- accountss = accounts()
151
- count = len(accountss)
152
- await stop_all_clients()
153
- for session in accountss:
154
- main_path = f'{ACCOUNTS_FOLDER}/{session}.session'
155
- try:
156
- os.unlink(main_path)
157
- except Exception:
158
- pass
159
- await message.reply(f'<b>{count} accounts deleted.</b>')
225
+ with open(file_path, "w", encoding="utf-8") as f:
226
+ json.dump(data, f, ensure_ascii=False, indent=4)
227
+ logger.info(f"{phone_number}: 💾 Account data saved successfully → {file_path}")
160
228
  except Exception as e:
161
- await message.reply(f'<b>Error deleting all accounts: {str(e)}</b>')
229
+ logger.error(f"{phone_number}: ⚠️ Error saving JSON - {type(e).__name__}: {e}")
230
+
231
+
232
+ # ============================================================
233
+ # 📋 لیست اکانت‌های فعال
234
+ # ============================================================
235
+ def accounts() -> List[str]:
236
+ accs: Set[str] = set()
237
+ if not os.path.isdir(ACCOUNTS_FOLDER):
238
+ return []
239
+ for acc in os.listdir(ACCOUNTS_FOLDER):
240
+ if acc.endswith(".session"):
241
+ accs.add(acc.split(".")[0])
242
+ return list(accs)
243
+
244
+
245
+ def get_active_accounts() -> Set[str]:
246
+ return set(accounts())