CliRemote 1.6.1__tar.gz → 1.7.0__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.
Files changed (48) hide show
  1. {cliremote-1.6.1 → cliremote-1.7.0}/CliRemote.egg-info/PKG-INFO +1 -1
  2. {cliremote-1.6.1 → cliremote-1.7.0}/CliRemote.egg-info/SOURCES.txt +2 -0
  3. {cliremote-1.6.1 → cliremote-1.7.0}/PKG-INFO +1 -1
  4. {cliremote-1.6.1 → cliremote-1.7.0}/pyproject.toml +1 -1
  5. cliremote-1.7.0/remote/analytics_manager.py +199 -0
  6. {cliremote-1.6.1 → cliremote-1.7.0}/remote/client_manager.py +34 -0
  7. cliremote-1.7.0/remote/client_picker.py +4 -0
  8. cliremote-1.7.0/remote/utils/__init__.py +0 -0
  9. {cliremote-1.6.1 → cliremote-1.7.0}/setup.py +1 -1
  10. cliremote-1.6.1/remote/analytics_manager.py +0 -98
  11. cliremote-1.6.1/remote/client_picker.py +0 -40
  12. {cliremote-1.6.1 → cliremote-1.7.0}/CliRemote.egg-info/dependency_links.txt +0 -0
  13. {cliremote-1.6.1 → cliremote-1.7.0}/CliRemote.egg-info/requires.txt +0 -0
  14. {cliremote-1.6.1 → cliremote-1.7.0}/CliRemote.egg-info/top_level.txt +0 -0
  15. {cliremote-1.6.1 → cliremote-1.7.0}/LICENSE +0 -0
  16. {cliremote-1.6.1 → cliremote-1.7.0}/MANIFEST.in +0 -0
  17. {cliremote-1.6.1 → cliremote-1.7.0}/README.md +0 -0
  18. {cliremote-1.6.1 → cliremote-1.7.0}/remote/__init__.py +0 -0
  19. {cliremote-1.6.1 → cliremote-1.7.0}/remote/account_manager.py +0 -0
  20. {cliremote-1.6.1 → cliremote-1.7.0}/remote/account_viewer.py +0 -0
  21. {cliremote-1.6.1 → cliremote-1.7.0}/remote/admin_manager.py +0 -0
  22. {cliremote-1.6.1 → cliremote-1.7.0}/remote/batch_manager.py +0 -0
  23. {cliremote-1.6.1 → cliremote-1.7.0}/remote/block_manager.py +0 -0
  24. {cliremote-1.6.1 → cliremote-1.7.0}/remote/caption_manager.py +0 -0
  25. {cliremote-1.6.1 → cliremote-1.7.0}/remote/cleaner.py +0 -0
  26. {cliremote-1.6.1 → cliremote-1.7.0}/remote/config.py +0 -0
  27. {cliremote-1.6.1 → cliremote-1.7.0}/remote/device_manager.py +0 -0
  28. {cliremote-1.6.1 → cliremote-1.7.0}/remote/file_sender.py +0 -0
  29. {cliremote-1.6.1 → cliremote-1.7.0}/remote/getcode_controller.py +0 -0
  30. {cliremote-1.6.1 → cliremote-1.7.0}/remote/health.py +0 -0
  31. {cliremote-1.6.1 → cliremote-1.7.0}/remote/help_menu.py +0 -0
  32. {cliremote-1.6.1 → cliremote-1.7.0}/remote/init.py +0 -0
  33. {cliremote-1.6.1 → cliremote-1.7.0}/remote/join_controller.py +0 -0
  34. {cliremote-1.6.1 → cliremote-1.7.0}/remote/joiner.py +0 -0
  35. {cliremote-1.6.1 → cliremote-1.7.0}/remote/leave_controller.py +0 -0
  36. {cliremote-1.6.1 → cliremote-1.7.0}/remote/lefter.py +0 -0
  37. {cliremote-1.6.1 → cliremote-1.7.0}/remote/mention_manager.py +0 -0
  38. {cliremote-1.6.1 → cliremote-1.7.0}/remote/precise_engine.py +0 -0
  39. {cliremote-1.6.1 → cliremote-1.7.0}/remote/profile_info.py +0 -0
  40. {cliremote-1.6.1 → cliremote-1.7.0}/remote/profile_media.py +0 -0
  41. {cliremote-1.6.1 → cliremote-1.7.0}/remote/profile_privacy.py +0 -0
  42. {cliremote-1.6.1 → cliremote-1.7.0}/remote/spammer.py +0 -0
  43. {cliremote-1.6.1 → cliremote-1.7.0}/remote/speed_manager.py +0 -0
  44. {cliremote-1.6.1 → cliremote-1.7.0}/remote/stop_manager.py +0 -0
  45. {cliremote-1.6.1 → cliremote-1.7.0}/remote/text_manager.py +0 -0
  46. {cliremote-1.6.1 → cliremote-1.7.0}/remote/username_manager.py +0 -0
  47. {cliremote-1.6.1 → cliremote-1.7.0}/remote/utils/sqlite_utils.py +0 -0
  48. {cliremote-1.6.1 → cliremote-1.7.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CliRemote
3
- Version: 1.6.1
3
+ Version: 1.7.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
@@ -36,6 +36,7 @@ setup.py
36
36
  ./remote/stop_manager.py
37
37
  ./remote/text_manager.py
38
38
  ./remote/username_manager.py
39
+ ./remote/utils/__init__.py
39
40
  ./remote/utils/sqlite_utils.py
40
41
  CliRemote.egg-info/PKG-INFO
41
42
  CliRemote.egg-info/SOURCES.txt
@@ -74,4 +75,5 @@ remote/speed_manager.py
74
75
  remote/stop_manager.py
75
76
  remote/text_manager.py
76
77
  remote/username_manager.py
78
+ remote/utils/__init__.py
77
79
  remote/utils/sqlite_utils.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CliRemote
3
- Version: 1.6.1
3
+ Version: 1.7.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "CliRemote"
7
- version = "1.6.1"
7
+ version = "1.7.0"
8
8
  description = "Remote client framework for Telegram automation using Pyrogram"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -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.")
@@ -245,3 +245,37 @@ def remove_client_from_pool(phone_number: str) -> None:
245
245
  client_pool.pop(phone_number, None)
246
246
  client_locks.pop(phone_number, None)
247
247
  logger.info("%s: removed from client_pool and client_locks", phone_number)
248
+
249
+ async def get_any_client(message=None) -> Optional[object]:
250
+ """
251
+ یک کلاینت آماده برمی‌گرداند:
252
+ 1) اگر کلاینت متصل در pool هست، همان را برمی‌گرداند.
253
+ 2) وگرنه از بین active_accounts یکی را استارت می‌کند.
254
+ """
255
+ # Use any connected client
256
+ for phone, cli in list(client_pool.items()):
257
+ try:
258
+ if getattr(cli, "is_connected", False):
259
+ logger.debug("get_any_client: using connected client %s", phone)
260
+ return cli
261
+ except Exception:
262
+ pass
263
+
264
+ # Start one if needed
265
+ accs = list(get_active_accounts())
266
+ if not accs:
267
+ logger.warning("get_any_client: no active accounts")
268
+ return None
269
+
270
+ random.shuffle(accs)
271
+ for phone in accs:
272
+ try:
273
+ cli = await get_or_start_client(phone)
274
+ if cli and getattr(cli, "is_connected", False):
275
+ logger.info("get_any_client: started %s", phone)
276
+ return cli
277
+ except Exception as e:
278
+ logger.warning("get_any_client: failed start %s: %s: %s", phone, type(e).__name__, e)
279
+
280
+ logger.error("get_any_client: could not get any client")
281
+ return None
@@ -0,0 +1,4 @@
1
+ import random
2
+ import logging
3
+ from typing import Optional
4
+ from .client_manager import client_pool, get_active_accounts, get_or_start_client
File without changes
@@ -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.6.1",
8
+ version="1.7.0",
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}")
@@ -1,40 +0,0 @@
1
- import random
2
- import logging
3
- from typing import Optional
4
- from .client_manager import client_pool, get_active_accounts, get_or_start_client
5
-
6
- logger = logging.getLogger(__name__)
7
-
8
- async def get_any_client(message=None) -> Optional[object]:
9
- """
10
- یک کلاینت آماده برمی‌گرداند:
11
- 1) اگر کلاینت متصل در pool هست، همان را برمی‌گرداند.
12
- 2) وگرنه از بین active_accounts یکی را استارت می‌کند.
13
- """
14
- # Use any connected client
15
- for phone, cli in list(client_pool.items()):
16
- try:
17
- if getattr(cli, "is_connected", False):
18
- logger.debug("get_any_client: using connected client %s", phone)
19
- return cli
20
- except Exception:
21
- pass
22
-
23
- # Start one if needed
24
- accs = list(get_active_accounts())
25
- if not accs:
26
- logger.warning("get_any_client: no active accounts")
27
- return None
28
-
29
- random.shuffle(accs)
30
- for phone in accs:
31
- try:
32
- cli = await get_or_start_client(phone)
33
- if cli and getattr(cli, "is_connected", False):
34
- logger.info("get_any_client: started %s", phone)
35
- return cli
36
- except Exception as e:
37
- logger.warning("get_any_client: failed start %s: %s: %s", phone, type(e).__name__, e)
38
-
39
- logger.error("get_any_client: could not get any client")
40
- return None
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