CliRemote 1.6.2__tar.gz → 1.7.1__tar.gz
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.
- {cliremote-1.6.2 → cliremote-1.7.1}/CliRemote.egg-info/PKG-INFO +1 -1
- {cliremote-1.6.2 → cliremote-1.7.1}/PKG-INFO +1 -1
- {cliremote-1.6.2 → cliremote-1.7.1}/pyproject.toml +1 -1
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/account_manager.py +147 -0
- cliremote-1.7.1/remote/analytics_manager.py +199 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/setup.py +1 -1
- cliremote-1.6.2/remote/analytics_manager.py +0 -98
- {cliremote-1.6.2 → cliremote-1.7.1}/CliRemote.egg-info/SOURCES.txt +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/CliRemote.egg-info/dependency_links.txt +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/CliRemote.egg-info/requires.txt +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/CliRemote.egg-info/top_level.txt +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/LICENSE +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/MANIFEST.in +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/README.md +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/__init__.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/account_viewer.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/admin_manager.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/batch_manager.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/block_manager.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/caption_manager.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/cleaner.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/client_manager.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/client_picker.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/config.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/device_manager.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/file_sender.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/getcode_controller.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/health.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/help_menu.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/init.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/join_controller.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/joiner.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/leave_controller.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/lefter.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/mention_manager.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/precise_engine.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/profile_info.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/profile_media.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/profile_privacy.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/spammer.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/speed_manager.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/stop_manager.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/text_manager.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/username_manager.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/utils/__init__.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/remote/utils/sqlite_utils.py +0 -0
- {cliremote-1.6.2 → cliremote-1.7.1}/setup.cfg +0 -0
@@ -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)
|
@@ -0,0 +1,199 @@
|
|
1
|
+
import os
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
import traceback
|
5
|
+
import asyncio
|
6
|
+
from datetime import datetime
|
7
|
+
from typing import Dict, Any
|
8
|
+
|
9
|
+
# =========================
|
10
|
+
# لاگ
|
11
|
+
# =========================
|
12
|
+
os.makedirs("logs", exist_ok=True)
|
13
|
+
logger = logging.getLogger(__name__)
|
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)
|
20
|
+
|
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:
|
94
|
+
def __init__(self):
|
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
|
+
}
|
116
|
+
}
|
117
|
+
"""
|
118
|
+
path = _file_for_target(target)
|
119
|
+
lock = self._lock_for(path)
|
120
|
+
|
121
|
+
try:
|
122
|
+
async with lock:
|
123
|
+
data = _load_json(path)
|
124
|
+
now = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
125
|
+
|
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
|
+
}
|
136
|
+
|
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
|
142
|
+
|
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
|
151
|
+
|
152
|
+
data["updated_at"] = now
|
153
|
+
|
154
|
+
_atomic_write_json(path, data)
|
155
|
+
|
156
|
+
except Exception as 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))
|
181
|
+
|
182
|
+
if not rows:
|
183
|
+
await message.reply("📊 هنوز آماری ثبت نشده است.")
|
184
|
+
return
|
185
|
+
|
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__}`")
|
195
|
+
|
196
|
+
# singleton
|
197
|
+
analytics = _Analytics()
|
198
|
+
|
199
|
+
logger.info("📈 Analytics manager initialized successfully.")
|
@@ -5,7 +5,7 @@ with open("README.md", encoding="utf-8") as f:
|
|
5
5
|
|
6
6
|
setup(
|
7
7
|
name="CliRemote",
|
8
|
-
version="1.
|
8
|
+
version="1.7.1",
|
9
9
|
author="MrAhmadiRad",
|
10
10
|
author_email="mohammadahmadirad69@gmail.com",
|
11
11
|
description="A precise, async-safe, Telegram automation core (Python 3.8+)",
|
@@ -1,98 +0,0 @@
|
|
1
|
-
# antispam_core/analytics_manager.py
|
2
|
-
import json, os, logging
|
3
|
-
from datetime import datetime
|
4
|
-
from . import admin_manager
|
5
|
-
|
6
|
-
logger = logging.getLogger(__name__)
|
7
|
-
STATS_FILE = "analytics_stats.json"
|
8
|
-
|
9
|
-
class Analytics:
|
10
|
-
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)},
|
17
|
-
}
|
18
|
-
self.load()
|
19
|
-
|
20
|
-
# --------------------------
|
21
|
-
# ذخیره و بارگذاری دادهها
|
22
|
-
# --------------------------
|
23
|
-
def save(self):
|
24
|
-
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}")
|
29
|
-
|
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}")
|
40
|
-
|
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
|
51
|
-
|
52
|
-
hour = datetime.now().hour
|
53
|
-
self.stats["hourly_activity"][hour] += 1
|
54
|
-
|
55
|
-
if account not in self.stats["account_performance"]:
|
56
|
-
self.stats["account_performance"][account] = {"success": 0, "total": 0}
|
57
|
-
|
58
|
-
self.stats["account_performance"][account]["total"] += 1
|
59
|
-
if success:
|
60
|
-
self.stats["account_performance"][account]["success"] += 1
|
61
|
-
|
62
|
-
# ذخیره خودکار پس از هر بروزرسانی
|
63
|
-
self.save()
|
64
|
-
except Exception as e:
|
65
|
-
logger.error(f"Error in update_stats: {e}")
|
66
|
-
|
67
|
-
|
68
|
-
analytics = Analytics()
|
69
|
-
|
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}")
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|