CliRemote 1.6.2__py3-none-any.whl → 1.7.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.6.2
3
+ Version: 1.7.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,9 +1,9 @@
1
- cliremote-1.6.2.dist-info/licenses/LICENSE,sha256=O-0zMbcEi6wXz1DiSdVgzMlQjJcNqNe5KDv08uYzqR0,1055
1
+ cliremote-1.7.1.dist-info/licenses/LICENSE,sha256=O-0zMbcEi6wXz1DiSdVgzMlQjJcNqNe5KDv08uYzqR0,1055
2
2
  remote/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- remote/account_manager.py,sha256=u8yqcyq7Vl8LZ1rNlrEfwlJEbJiBzoSAlL3z2hhTzyc,9829
3
+ remote/account_manager.py,sha256=coleW-uh2C35CjSoje0WifqAqKOJptL7epxUcksoLEE,16975
4
4
  remote/account_viewer.py,sha256=MQoH5lOz24651EjhlzVwi6O5hVUAT7Q5fFU6ZHjBlsM,4243
5
5
  remote/admin_manager.py,sha256=WiUUVmSs5JTUdXeSry8PkK_3TRemAdSZjm0G1ilAA-A,3532
6
- remote/analytics_manager.py,sha256=cQ_uATzOK345U1J88Fki3gOfNedS4Ur5NpLjazsxQkg,3625
6
+ remote/analytics_manager.py,sha256=6jPvwt_ELA4RMbQdD8W_ltfAoaSgILnEkOAp6HZAqsU,7382
7
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
@@ -33,7 +33,7 @@ remote/text_manager.py,sha256=C2wNSXPSCDu8NSD3RsfbKmUQMWOYd1B5N4tzy-Jsriw,2195
33
33
  remote/username_manager.py,sha256=nMNdke-2FIv86xR1Y6rR-43oUoQu_3Khw8wEo54noXI,3388
34
34
  remote/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
35
  remote/utils/sqlite_utils.py,sha256=5i0oUXsBgKC_8qHZPJ-Gyhp9D1TwqKHVvuZRIhKpS6w,1260
36
- cliremote-1.6.2.dist-info/METADATA,sha256=pXO0OVFiYv3MzWlY2qLDRHs0AhT33RGUf2hF3FGguDg,1202
37
- cliremote-1.6.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
- cliremote-1.6.2.dist-info/top_level.txt,sha256=yBZidJ6zCix_a2ubGlYaewvlzBFXWbckQt20dudxJ1E,7
39
- cliremote-1.6.2.dist-info/RECORD,,
36
+ cliremote-1.7.1.dist-info/METADATA,sha256=MIrlPE2ZHrDlikV6DohS620k4cbobNGj_LyccmbdaKg,1202
37
+ cliremote-1.7.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
+ cliremote-1.7.1.dist-info/top_level.txt,sha256=yBZidJ6zCix_a2ubGlYaewvlzBFXWbckQt20dudxJ1E,7
39
+ cliremote-1.7.1.dist-info/RECORD,,
remote/account_manager.py CHANGED
@@ -244,3 +244,150 @@ def accounts() -> List[str]:
244
244
 
245
245
  def get_active_accounts() -> Set[str]:
246
246
  return set(accounts())
247
+
248
+ # ============================================================
249
+ # 🗑️ حذف اکانت
250
+ # ============================================================
251
+ async def delete_account_cmd(message) -> None:
252
+ """
253
+ حذف اکانت مشخص شده
254
+ دستور: /del <phone_number>
255
+ """
256
+ try:
257
+ # استخراج شماره تلفن از پیام
258
+ command_parts = message.text.split()
259
+ if len(command_parts) < 2:
260
+ await message.reply_text("⚠️ لطفاً شماره تلفن اکانت را وارد کنید:\n`/del 989123456789`")
261
+ return
262
+
263
+ phone_number = command_parts[1].strip()
264
+
265
+ # بررسی وجود اکانت
266
+ if phone_number not in get_active_accounts():
267
+ await message.reply_text(f"❌ اکانت `{phone_number}` یافت نشد.")
268
+ return
269
+
270
+ # توقف کلاینت اگر در حال اجراست
271
+ if phone_number in client_pool:
272
+ try:
273
+ cli = client_pool[phone_number]
274
+ if getattr(cli, "is_connected", False):
275
+ await cli.stop()
276
+ client_pool.pop(phone_number, None)
277
+ client_locks.pop(phone_number, None)
278
+ logger.info(f"{phone_number}: 📴 Client stopped for deletion.")
279
+ except Exception as e:
280
+ logger.warning(f"{phone_number}: ⚠️ Error stopping client before deletion - {e}")
281
+
282
+ # حذف فایل‌های session
283
+ session_deleted = False
284
+ data_deleted = False
285
+
286
+ session_files = [
287
+ os.path.join(ACCOUNTS_FOLDER, f"{phone_number}.session"),
288
+ os.path.join(ACCOUNTS_FOLDER, phone_number), # برای حالت‌های مختلف نام session
289
+ f"{phone_number}.session", # در صورت وجود در مسیر جاری
290
+ ]
291
+
292
+ for session_file in session_files:
293
+ if os.path.exists(session_file):
294
+ try:
295
+ os.remove(session_file)
296
+ session_deleted = True
297
+ logger.info(f"{phone_number}: 🗑️ Session file deleted → {session_file}")
298
+ except Exception as e:
299
+ logger.error(f"{phone_number}: ⚠️ Error deleting session file {session_file} - {e}")
300
+
301
+ # حذف فایل داده‌های اکانت
302
+ data_file = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
303
+ if os.path.exists(data_file):
304
+ try:
305
+ os.remove(data_file)
306
+ data_deleted = True
307
+ logger.info(f"{phone_number}: 🗑️ Account data deleted → {data_file}")
308
+ except Exception as e:
309
+ logger.error(f"{phone_number}: ⚠️ Error deleting account data {data_file} - {e}")
310
+
311
+ # ارسال نتیجه به کاربر
312
+ if session_deleted or data_deleted:
313
+ await message.reply_text(f"✅ اکانت `{phone_number}` با موفقیت حذف شد.\n"
314
+ f"• فایل session: {'✅' if session_deleted else '❌'}\n"
315
+ f"• فایل داده: {'✅' if data_deleted else '❌'}")
316
+ logger.info(f"{phone_number}: ✅ Account deletion completed.")
317
+ else:
318
+ await message.reply_text(f"⚠️ هیچ فایلی برای اکانت `{phone_number}` یافت نشد.")
319
+
320
+ except Exception as e:
321
+ error_msg = f"💥 خطا در حذف اکانت: {str(e)}"
322
+ logger.error(f"delete_account_cmd error: {traceback.format_exc()}")
323
+ await message.reply_text(error_msg)
324
+
325
+
326
+ # ============================================================
327
+ # 🗑️ حذف تمامی اکانت‌ها
328
+ # ============================================================
329
+ async def delete_all_accounts_cmd(message) -> None:
330
+ """
331
+ حذف تمامی اکانت‌ها
332
+ دستور: /delall
333
+ """
334
+ try:
335
+ # گرفتن تایید از کاربر
336
+ confirm_text = "⚠️ **آیا مطمئن هستید که می‌خواهید تمامی اکانت‌ها را حذف کنید؟**\n\n"
337
+ confirm_text += "این عمل غیرقابل بازگشت است!\n"
338
+ confirm_text += "برای تایید، دستور زیر را ارسال کنید:\n`/delall confirm`"
339
+
340
+ command_parts = message.text.split()
341
+ if len(command_parts) < 2 or command_parts[1].strip().lower() != "confirm":
342
+ await message.reply_text(confirm_text)
343
+ return
344
+
345
+ # توقف تمام کلاینت‌ها
346
+ await stop_all_clients()
347
+
348
+ # لیست تمام اکانت‌ها
349
+ all_accounts = get_active_accounts()
350
+ deleted_sessions = 0
351
+ deleted_data_files = 0
352
+
353
+ # حذف تمام فایل‌های session
354
+ if os.path.exists(ACCOUNTS_FOLDER):
355
+ for filename in os.listdir(ACCOUNTS_FOLDER):
356
+ if filename.endswith('.session'):
357
+ try:
358
+ file_path = os.path.join(ACCOUNTS_FOLDER, filename)
359
+ os.remove(file_path)
360
+ deleted_sessions += 1
361
+ logger.info(f"🗑️ Session file deleted → {filename}")
362
+ except Exception as e:
363
+ logger.error(f"⚠️ Error deleting session file {filename} - {e}")
364
+
365
+ # حذف تمام فایل‌های داده
366
+ if os.path.exists(ACCOUNTS_DATA_FOLDER):
367
+ for filename in os.listdir(ACCOUNTS_DATA_FOLDER):
368
+ if filename.endswith('.json'):
369
+ try:
370
+ file_path = os.path.join(ACCOUNTS_DATA_FOLDER, filename)
371
+ os.remove(file_path)
372
+ deleted_data_files += 1
373
+ logger.info(f"🗑️ Account data deleted → {filename}")
374
+ except Exception as e:
375
+ logger.error(f"⚠️ Error deleting account data {filename} - {e}")
376
+
377
+ # پاک کردن کش داخلی
378
+ client_pool.clear()
379
+ client_locks.clear()
380
+
381
+ # ارسال نتیجه به کاربر
382
+ result_msg = (f"✅ **حذف کامل اکانت‌ها انجام شد**\n\n"
383
+ f"• تعداد فایل‌های session حذف شده: `{deleted_sessions}`\n"
384
+ f"• تعداد فایل‌های داده حذف شده: `{deleted_data_files}`\n"
385
+ f"• تعداد اکانت‌های شناسایی شده: `{len(all_accounts)}`")
386
+
387
+ await message.reply_text(result_msg)
388
+ logger.info(f"🎯 All accounts deletion completed: {deleted_sessions} sessions, {deleted_data_files} data files")
389
+
390
+ except Exception as e:
391
+ error_msg = f"💥 خطا در حذف کامل اکانت‌ها: {str(e)}"
392
+ logger.error(f"delete_all_accounts_cmd error: {traceback.format_exc()}")
393
+ await message.reply_text(error_msg)
@@ -1,98 +1,199 @@
1
- # antispam_core/analytics_manager.py
2
- import json, os, logging
1
+ import os
2
+ import json
3
+ import logging
4
+ import traceback
5
+ import asyncio
3
6
  from datetime import datetime
4
- from . import admin_manager
7
+ from typing import Dict, Any
5
8
 
9
+ # =========================
10
+ # لاگ
11
+ # =========================
12
+ os.makedirs("logs", exist_ok=True)
6
13
  logger = logging.getLogger(__name__)
7
- STATS_FILE = "analytics_stats.json"
14
+ logger.setLevel(logging.INFO)
15
+ if not any(getattr(h, "baseFilename", "").endswith("logs/analytics_log.txt") for h in logger.handlers if hasattr(h, "baseFilename")):
16
+ fh = logging.FileHandler("logs/analytics_log.txt", encoding="utf-8")
17
+ fmt = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S")
18
+ fh.setFormatter(fmt)
19
+ logger.addHandler(fh)
8
20
 
9
- class Analytics:
21
+ # =========================
22
+ # ابزار مسیر امن
23
+ # =========================
24
+ BASE_DIR = os.path.abspath(os.getcwd())
25
+ AN_DIR = os.path.join(BASE_DIR, "analytics")
26
+ os.makedirs(AN_DIR, exist_ok=True)
27
+ try:
28
+ os.chmod(AN_DIR, 0o777)
29
+ except Exception:
30
+ pass
31
+
32
+ def _sanitize_target(t: Any) -> str:
33
+ """
34
+ target (chat_id / username / لینک) را به نام فایل امن تبدیل می‌کند.
35
+ مثال: -100123..., user_name, t.me/+hash → chat_-100123..., user_name, invite_hash_xxx
36
+ """
37
+ s = str(t)
38
+ # دسته‌بندی ساده برای خوانایی فایل‌ها
39
+ if s.lstrip("-").isdigit():
40
+ prefix = "chat_"
41
+ elif "joinchat" in s or s.startswith("https://t.me/+") or s.startswith("+"):
42
+ prefix = "invite_"
43
+ else:
44
+ prefix = "name_"
45
+
46
+ # کاراکترهای ناامن را با '_' جایگزین کن
47
+ safe = []
48
+ for ch in s:
49
+ if ch.isalnum() or ch in ("-", "_"):
50
+ safe.append(ch)
51
+ else:
52
+ safe.append("_")
53
+ name = prefix + "".join(safe)
54
+ # طول زیاد را کوتاه کن
55
+ if len(name) > 120:
56
+ name = name[:120]
57
+ return name
58
+
59
+ def _file_for_target(target: Any) -> str:
60
+ return os.path.join(AN_DIR, f"{_sanitize_target(target)}.json")
61
+
62
+ # =========================
63
+ # ذخیره/خواندن اتمیک
64
+ # =========================
65
+ def _atomic_write_json(path: str, data: Dict[str, Any]) -> None:
66
+ tmp = path + ".tmp"
67
+ # ensure parent
68
+ os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
69
+ with open(tmp, "w", encoding="utf-8") as f:
70
+ json.dump(data, f, ensure_ascii=False, indent=2)
71
+ os.replace(tmp, path) # اتمیک روی همان پارتیشن
72
+
73
+ def _load_json(path: str) -> Dict[str, Any]:
74
+ if not os.path.exists(path):
75
+ return {}
76
+ try:
77
+ with open(path, "r", encoding="utf-8") as f:
78
+ return json.load(f)
79
+ except Exception as e:
80
+ # اگر خراب شده بود، بک‌آپ بگیریم و از صفر شروع کنیم
81
+ try:
82
+ bak = path + ".corrupt." + datetime.now().strftime("%Y%m%d_%H%M%S")
83
+ os.rename(path, bak)
84
+ logger.warning("analytics: corrupted file moved to %s", bak)
85
+ except Exception:
86
+ pass
87
+ logger.error("analytics: read error %s: %s: %s", path, type(e).__name__, e)
88
+ return {}
89
+
90
+ # =========================
91
+ # مدیر آمار
92
+ # =========================
93
+ class _Analytics:
10
94
  def __init__(self):
11
- self.stats = {
12
- "total_sent": 0,
13
- "successful": 0,
14
- "failed": 0,
15
- "account_performance": {},
16
- "hourly_activity": {h: 0 for h in range(24)},
95
+ self._locks: Dict[str, asyncio.Lock] = {}
96
+
97
+ def _lock_for(self, target_path: str) -> asyncio.Lock:
98
+ if target_path not in self._locks:
99
+ self._locks[target_path] = asyncio.Lock()
100
+ return self._locks[target_path]
101
+
102
+ async def update_stats(self, account: str, success: bool, target: Any) -> None:
103
+ """
104
+ استات‌ها را برای target بروز می‌کند.
105
+ ساختار فایل:
106
+ {
107
+ "target": "<sanitized>",
108
+ "created_at": "...",
109
+ "updated_at": "...",
110
+ "total": 123,
111
+ "success": 100,
112
+ "fail": 23,
113
+ "by_account": {
114
+ "<phone>": {"total": X, "success": Y, "fail": Z, "last": "..."}
115
+ }
17
116
  }
18
- self.load()
117
+ """
118
+ path = _file_for_target(target)
119
+ lock = self._lock_for(path)
19
120
 
20
- # --------------------------
21
- # ذخیره و بارگذاری داده‌ها
22
- # --------------------------
23
- def save(self):
24
121
  try:
25
- with open(STATS_FILE, "w", encoding="utf-8") as f:
26
- json.dump(self.stats, f, ensure_ascii=False, indent=2)
27
- except Exception as e:
28
- logger.error(f"Error saving stats: {e}")
122
+ async with lock:
123
+ data = _load_json(path)
124
+ now = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
29
125
 
30
- def load(self):
31
- try:
32
- if os.path.exists(STATS_FILE):
33
- with open(STATS_FILE, "r", encoding="utf-8") as f:
34
- data = json.load(f)
35
- if isinstance(data, dict):
36
- self.stats.update(data)
37
- logger.info("📈 Analytics data loaded successfully.")
38
- except Exception as e:
39
- logger.error(f"Error loading stats: {e}")
126
+ if not data:
127
+ data = {
128
+ "target": _sanitize_target(target),
129
+ "created_at": now,
130
+ "updated_at": now,
131
+ "total": 0,
132
+ "success": 0,
133
+ "fail": 0,
134
+ "by_account": {},
135
+ }
40
136
 
41
- # --------------------------
42
- # بروزرسانی آمارها
43
- # --------------------------
44
- async def update_stats(self, account: str, success: bool, chat_id: int = None):
45
- try:
46
- self.stats["total_sent"] += 1
47
- if success:
48
- self.stats["successful"] += 1
49
- else:
50
- self.stats["failed"] += 1
137
+ data["total"] = int(data.get("total", 0)) + 1
138
+ if success:
139
+ data["success"] = int(data.get("success", 0)) + 1
140
+ else:
141
+ data["fail"] = int(data.get("fail", 0)) + 1
51
142
 
52
- hour = datetime.now().hour
53
- self.stats["hourly_activity"][hour] += 1
143
+ ba = data.setdefault("by_account", {})
144
+ acc = ba.setdefault(str(account), {"total": 0, "success": 0, "fail": 0, "last": now})
145
+ acc["total"] = int(acc.get("total", 0)) + 1
146
+ if success:
147
+ acc["success"] = int(acc.get("success", 0)) + 1
148
+ else:
149
+ acc["fail"] = int(acc.get("fail", 0)) + 1
150
+ acc["last"] = now
54
151
 
55
- if account not in self.stats["account_performance"]:
56
- self.stats["account_performance"][account] = {"success": 0, "total": 0}
152
+ data["updated_at"] = now
57
153
 
58
- self.stats["account_performance"][account]["total"] += 1
59
- if success:
60
- self.stats["account_performance"][account]["success"] += 1
154
+ _atomic_write_json(path, data)
61
155
 
62
- # ذخیره خودکار پس از هر بروزرسانی
63
- self.save()
64
156
  except Exception as e:
65
- logger.error(f"Error in update_stats: {e}")
157
+ # این لاگ تمام جزئیات خطا را می‌نویسد؛ اگر قبلاً فقط عدد 20 می‌دیدی، اینجا traceback کامل می‌بینی
158
+ tb = traceback.format_exc()
159
+ logger.error(
160
+ "Error in update_stats → account=%s success=%s target=%s path=%s\n%s",
161
+ account, success, target, path, tb
162
+ )
163
+
164
+ async def show_stats_cmd(self, message):
165
+ """
166
+ نمایش خلاصه‌ای از آمار همه‌ی فایل‌ها (برای دستور /stats)
167
+ """
168
+ try:
169
+ rows = []
170
+ for fn in os.listdir(AN_DIR):
171
+ if not fn.endswith(".json"):
172
+ continue
173
+ path = os.path.join(AN_DIR, fn)
174
+ data = _load_json(path)
175
+ if not data:
176
+ continue
177
+ total = int(data.get("total", 0))
178
+ succ = int(data.get("success", 0))
179
+ fail = int(data.get("fail", 0))
180
+ rows.append((fn[:-5], total, succ, fail))
66
181
 
182
+ if not rows:
183
+ await message.reply("📊 هنوز آماری ثبت نشده است.")
184
+ return
67
185
 
68
- analytics = Analytics()
186
+ rows.sort(key=lambda x: x[1], reverse=True)
187
+ text = ["📊 **Analytics Summary**"]
188
+ for name, total, succ, fail in rows[:20]:
189
+ text.append(f"- `{name}` → total: **{total}**, ✅ {succ}, ❌ {fail}")
190
+ await message.reply("\n".join(text))
191
+ except Exception as e:
192
+ tb = traceback.format_exc()
193
+ logger.error("show_stats_cmd error: %s: %s\n%s", type(e).__name__, e, tb)
194
+ await message.reply(f"❌ show_stats error: `{type(e).__name__}`")
69
195
 
70
- # --------------------------
71
- # دستور /stats
72
- # --------------------------
73
- async def show_stats_cmd(message):
74
- try:
75
- stats = analytics.stats
76
- total = stats["total_sent"]
77
- success_rate = stats["successful"] / total * 100 if total > 0 else 0
78
-
79
- report_msg = (
80
- "📊 <b>آمار کلی ارسال‌ها:</b>\n"
81
- f"🔹 کل ارسال‌ها: {stats['total_sent']}\n"
82
- f"✅ موفق: {stats['successful']}\n"
83
- f"❌ ناموفق: {stats['failed']}\n"
84
- f"📈 نرخ موفقیت: {success_rate:.2f}%\n\n"
85
- )
86
-
87
- # نمایش آمار بر اساس حساب‌ها
88
- if stats["account_performance"]:
89
- report_msg += "<b>📋 عملکرد هر اکانت:</b>\n"
90
- for acc, data in stats["account_performance"].items():
91
- total_acc = data["total"]
92
- rate = (data["success"] / total_acc * 100) if total_acc else 0
93
- report_msg += f"• {acc}: {rate:.1f}% ({data['success']}/{total_acc})\n"
94
-
95
- await message.reply(report_msg)
96
- except Exception as e:
97
- logger.error(f"Error showing stats: {e}")
98
- await message.reply(f"خطا در نمایش آمار: {e}")
196
+ # singleton
197
+ analytics = _Analytics()
198
+
199
+ logger.info("📈 Analytics manager initialized successfully.")