CliRemote 1.6.1__py3-none-any.whl → 1.7.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.
- {cliremote-1.6.1.dist-info → cliremote-1.7.0.dist-info}/METADATA +1 -1
- {cliremote-1.6.1.dist-info → cliremote-1.7.0.dist-info}/RECORD +9 -8
- remote/analytics_manager.py +181 -80
- remote/client_manager.py +34 -0
- remote/client_picker.py +0 -36
- remote/utils/__init__.py +0 -0
- {cliremote-1.6.1.dist-info → cliremote-1.7.0.dist-info}/WHEEL +0 -0
- {cliremote-1.6.1.dist-info → cliremote-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {cliremote-1.6.1.dist-info → cliremote-1.7.0.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,15 @@
|
|
1
|
-
cliremote-1.
|
1
|
+
cliremote-1.7.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
|
5
5
|
remote/admin_manager.py,sha256=WiUUVmSs5JTUdXeSry8PkK_3TRemAdSZjm0G1ilAA-A,3532
|
6
|
-
remote/analytics_manager.py,sha256=
|
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
|
10
10
|
remote/cleaner.py,sha256=gvPtkosGsf7Eb3zmYyeC44xB4HtCxlzKH3e3qLuVos4,5742
|
11
|
-
remote/client_manager.py,sha256=
|
12
|
-
remote/client_picker.py,sha256=
|
11
|
+
remote/client_manager.py,sha256=dI9ZKOVPDXsq6_9Hi5Y_E_zKWyhSaqOxjiyRlr0hG5Y,10158
|
12
|
+
remote/client_picker.py,sha256=6WDIabYhcXSwcCAuRUfJfh6V1ZXP2EiFdAnutK2qLTk,139
|
13
13
|
remote/config.py,sha256=VK0e96gEINRViKIq99CYYuYyaVZTLtlWlPKKkBd41Cg,2377
|
14
14
|
remote/device_manager.py,sha256=SUCONe1qa5jMHOMqqS27ATtv3CaqAT8cN9jNi7AI_Go,5813
|
15
15
|
remote/file_sender.py,sha256=5_3ptTkoFejhJhaSyzh-8y5l_k7frxFq9LS_WL5jsGc,3657
|
@@ -31,8 +31,9 @@ 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
|
+
remote/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
34
35
|
remote/utils/sqlite_utils.py,sha256=5i0oUXsBgKC_8qHZPJ-Gyhp9D1TwqKHVvuZRIhKpS6w,1260
|
35
|
-
cliremote-1.
|
36
|
-
cliremote-1.
|
37
|
-
cliremote-1.
|
38
|
-
cliremote-1.
|
36
|
+
cliremote-1.7.0.dist-info/METADATA,sha256=VGS_PWhg3OeLc54ljTRbmGiAWIfTjn2snD3-Fwr2rk8,1202
|
37
|
+
cliremote-1.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
38
|
+
cliremote-1.7.0.dist-info/top_level.txt,sha256=yBZidJ6zCix_a2ubGlYaewvlzBFXWbckQt20dudxJ1E,7
|
39
|
+
cliremote-1.7.0.dist-info/RECORD,,
|
remote/analytics_manager.py
CHANGED
@@ -1,98 +1,199 @@
|
|
1
|
-
|
2
|
-
import json
|
1
|
+
import os
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
import traceback
|
5
|
+
import asyncio
|
3
6
|
from datetime import datetime
|
4
|
-
from
|
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
|
-
|
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
|
-
|
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.
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
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
|
26
|
-
|
27
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
45
|
-
|
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
|
-
|
53
|
-
|
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
|
-
|
56
|
-
self.stats["account_performance"][account] = {"success": 0, "total": 0}
|
152
|
+
data["updated_at"] = now
|
57
153
|
|
58
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
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.")
|
remote/client_manager.py
CHANGED
@@ -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
|
remote/client_picker.py
CHANGED
@@ -2,39 +2,3 @@ import random
|
|
2
2
|
import logging
|
3
3
|
from typing import Optional
|
4
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
|
remote/utils/__init__.py
ADDED
File without changes
|
File without changes
|
File without changes
|
File without changes
|