CliRemote 1.7.13__py3-none-any.whl → 1.8.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.7.13.dist-info → cliremote-1.8.0.dist-info}/METADATA +1 -1
- {cliremote-1.7.13.dist-info → cliremote-1.8.0.dist-info}/RECORD +8 -8
- remote/admin_manager.py +7 -7
- remote/precise_engine.py +92 -35
- remote/spammer.py +5 -32
- {cliremote-1.7.13.dist-info → cliremote-1.8.0.dist-info}/WHEEL +0 -0
- {cliremote-1.7.13.dist-info → cliremote-1.8.0.dist-info}/licenses/LICENSE +0 -0
- {cliremote-1.7.13.dist-info → cliremote-1.8.0.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,8 @@
|
|
1
|
-
cliremote-1.
|
1
|
+
cliremote-1.8.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=TepnGIoE2hU-j_NmU5ByoS-JrboE0w3A1bYmQoQX5h8,15176
|
4
4
|
remote/account_viewer.py,sha256=j46KSjbgrBmBi7UxFeJ5tCwHIe0QvCvphkirGIbB2oo,5192
|
5
|
-
remote/admin_manager.py,sha256=
|
5
|
+
remote/admin_manager.py,sha256=fTtwWwlr9Kq7QGZ_gNJVa2JNVnnUEkByo4L1toUf5Ys,7422
|
6
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
|
@@ -22,18 +22,18 @@ remote/joiner.py,sha256=CXTQ-l6CysqZSkYP1GUV1LM1qlh5wD4jBj_og_FVW1I,7773
|
|
22
22
|
remote/leave_controller.py,sha256=TIShgxCMiKyBn9ZdQZYm6Kf89GgDaoPCRH7pxqq9FC8,1167
|
23
23
|
remote/lefter.py,sha256=jxZmkl3NIe1c-WeVES8WBX21hW-eocfxfePvxddHdi0,6090
|
24
24
|
remote/mention_manager.py,sha256=I75ezmVZM0lYrgk8OQ2p-VETLLwM5FiW83tiBgbQ1CY,3257
|
25
|
-
remote/precise_engine.py,sha256=
|
25
|
+
remote/precise_engine.py,sha256=EP-QMWj4YRXqh7IUckAQzHTARi-iFr-7WcspM9IHvrA,4092
|
26
26
|
remote/profile_info.py,sha256=Us5mz6sdV_5-5wRx6F3S6rtj2m6vaP_HOrCBLgHXpD4,1992
|
27
27
|
remote/profile_media.py,sha256=lD_uIb0U3jeH_dpa4SNbF0wr1DQiQ5OfbTCOubcm9Us,2264
|
28
28
|
remote/profile_privacy.py,sha256=LrRZbbqhaYdl4JzBMJrl-D-N8y1AG2LR1DDzv-tmlV0,2298
|
29
|
-
remote/spammer.py,sha256=
|
29
|
+
remote/spammer.py,sha256=lKALyfX3hInsJmR6qVb5xxAdM-GkW2s42e6gqTyIo1k,11820
|
30
30
|
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
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.
|
37
|
-
cliremote-1.
|
38
|
-
cliremote-1.
|
39
|
-
cliremote-1.
|
36
|
+
cliremote-1.8.0.dist-info/METADATA,sha256=I8rSH5Tx0v5tXy0EFk3ty1_cz0Yo4e_3UH2zGlRNX_U,1202
|
37
|
+
cliremote-1.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
38
|
+
cliremote-1.8.0.dist-info/top_level.txt,sha256=yBZidJ6zCix_a2ubGlYaewvlzBFXWbckQt20dudxJ1E,7
|
39
|
+
cliremote-1.8.0.dist-info/RECORD,,
|
remote/admin_manager.py
CHANGED
@@ -4,8 +4,8 @@ import os
|
|
4
4
|
import sys
|
5
5
|
import logging
|
6
6
|
from pathlib import Path
|
7
|
-
from pyrogram import filters
|
8
|
-
from .
|
7
|
+
from pyrogram import filters
|
8
|
+
from . import config
|
9
9
|
|
10
10
|
# =============================
|
11
11
|
# تنظیم logger فایل
|
@@ -53,8 +53,8 @@ def load_admins() -> list[int]:
|
|
53
53
|
بارگذاری لیست ادمینها از فایل.
|
54
54
|
همیشه OWNER_ID را هم به لیست اضافه میکند.
|
55
55
|
"""
|
56
|
-
logger.debug(f"Loading admins from file: {ADMINS_FILE} | OWNER_ID: {OWNER_ID}")
|
57
|
-
s = set(OWNER_ID)
|
56
|
+
logger.debug(f"Loading admins from file: {ADMINS_FILE} | OWNER_ID: {config.OWNER_ID}")
|
57
|
+
s = set(config.OWNER_ID)
|
58
58
|
try:
|
59
59
|
if os.path.exists(ADMINS_FILE):
|
60
60
|
with open(ADMINS_FILE, "r", encoding="utf-8") as f:
|
@@ -98,7 +98,7 @@ admin_filter = filters.create(
|
|
98
98
|
lambda _, __, m: bool(getattr(m, "from_user", None)) and int(m.from_user.id) in ADMINS
|
99
99
|
)
|
100
100
|
owner_filter = filters.create(
|
101
|
-
lambda _, __, m: bool(getattr(m, "from_user", None)) and int(m.from_user.id) in OWNER_ID
|
101
|
+
lambda _, __, m: bool(getattr(m, "from_user", None)) and int(m.from_user.id) in config.OWNER_ID
|
102
102
|
)
|
103
103
|
|
104
104
|
# =============================
|
@@ -119,7 +119,7 @@ async def add_admin_cmd(message):
|
|
119
119
|
uid = int(parts[1])
|
120
120
|
logger.debug(f"add_admin_cmd: parsed target uid={uid}")
|
121
121
|
|
122
|
-
if uid in OWNER_ID:
|
122
|
+
if uid in config.OWNER_ID:
|
123
123
|
logger.info(f"add_admin_cmd: uid={uid} is OWNER; skip append")
|
124
124
|
await message.reply("ادمین اصلی از قبل وجود دارد")
|
125
125
|
return
|
@@ -151,7 +151,7 @@ async def del_admin_cmd(message):
|
|
151
151
|
uid = int(parts[1])
|
152
152
|
logger.debug(f"del_admin_cmd: parsed target uid={uid}")
|
153
153
|
|
154
|
-
if uid in OWNER_ID:
|
154
|
+
if uid in config.OWNER_ID:
|
155
155
|
logger.info(f"del_admin_cmd: attempt to remove OWNER uid={uid} blocked")
|
156
156
|
await message.reply("❌ امکان حذف ادمین اصلی وجود ندارد")
|
157
157
|
return
|
remote/precise_engine.py
CHANGED
@@ -1,45 +1,102 @@
|
|
1
1
|
# antispam_core/precise_engine.py
|
2
2
|
import asyncio
|
3
3
|
import time
|
4
|
+
from dataclasses import dataclass
|
4
5
|
|
5
|
-
|
6
|
+
@dataclass
|
7
|
+
class TickerStats:
|
8
|
+
last_target_ns: int = 0
|
9
|
+
last_wakeup_ns: int = 0
|
10
|
+
last_jitter_ns: int = 0 # wakeup - target (>= 0)
|
11
|
+
|
12
|
+
class HiPrecisionTicker:
|
6
13
|
"""
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
Drift-free asyncio ticker with hybrid coarse-sleep + fine spin.
|
15
|
+
- Absolute scheduling: next_target = prev_target + period (no cumulative drift).
|
16
|
+
- Coarse sleep via asyncio, then busy-wait for final microseconds.
|
17
|
+
- Uses time.perf_counter_ns() (monotonic, high-resolution).
|
18
|
+
|
19
|
+
Notes:
|
20
|
+
* Sub-microsecond accuracy is NOT realistic on general OS with Python.
|
21
|
+
* 'spin' burns CPU on the running core; keep margins reasonable.
|
22
|
+
* Tune margins for your hardware/OS.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
interval_sec: desired period in seconds (float).
|
26
|
+
sleep_margin_ns: if remaining > (sleep_margin + spin_margin), we await asyncio.sleep.
|
27
|
+
spin_margin_ns: final window to busy-wait (e.g., 50-200 µs).
|
28
|
+
|
29
|
+
Stats:
|
30
|
+
.stats.last_jitter_ns gives (actual_wakeup - scheduled_target) in ns.
|
18
31
|
"""
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
32
|
+
|
33
|
+
__slots__ = (
|
34
|
+
"period_ns", "sleep_margin_ns", "spin_margin_ns",
|
35
|
+
"next_target_ns", "stats"
|
36
|
+
)
|
37
|
+
|
38
|
+
def __init__(self,
|
39
|
+
interval_sec: float,
|
40
|
+
sleep_margin_ns: int = 200_000, # 200 µs
|
41
|
+
spin_margin_ns: int = 50_000): # 50 µs
|
42
|
+
if interval_sec <= 0:
|
43
|
+
raise ValueError("interval_sec must be > 0")
|
44
|
+
self.period_ns = int(interval_sec * 1e9)
|
45
|
+
if self.period_ns <= 1_000: # sanity: <1 µs period is nonsense for Python
|
46
|
+
raise ValueError("interval too small for Python environment")
|
47
|
+
if sleep_margin_ns < 0 or spin_margin_ns < 0:
|
48
|
+
raise ValueError("margins must be non-negative")
|
49
|
+
self.sleep_margin_ns = int(sleep_margin_ns)
|
50
|
+
self.spin_margin_ns = int(spin_margin_ns)
|
51
|
+
self.next_target_ns: int | None = None
|
52
|
+
self.stats = TickerStats()
|
24
53
|
|
25
54
|
async def sleep(self):
|
26
|
-
now = time.
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
#
|
37
|
-
self.
|
38
|
-
|
39
|
-
|
40
|
-
self.
|
41
|
-
|
55
|
+
now = time.perf_counter_ns()
|
56
|
+
|
57
|
+
# First use: schedule at now + period (absolute target)
|
58
|
+
if self.next_target_ns is None:
|
59
|
+
self.next_target_ns = now + self.period_ns
|
60
|
+
|
61
|
+
remaining = self.next_target_ns - now
|
62
|
+
|
63
|
+
if remaining <= 0:
|
64
|
+
# We're already late: skip sleeping, jump target forward
|
65
|
+
# so we don't try to "catch up" with negative sleeps.
|
66
|
+
self.stats.last_target_ns = self.next_target_ns
|
67
|
+
self.stats.last_wakeup_ns = now
|
68
|
+
self.stats.last_jitter_ns = now - self.next_target_ns
|
69
|
+
self.next_target_ns = now + self.period_ns
|
70
|
+
return
|
71
|
+
|
72
|
+
# Coarse sleep: leave 'spin_margin_ns' to finish in busy-wait.
|
73
|
+
# Also keep a 'sleep_margin_ns' cushion to reduce overshoot risk.
|
74
|
+
coarse_threshold = self.sleep_margin_ns + self.spin_margin_ns
|
75
|
+
if remaining > coarse_threshold:
|
76
|
+
# target - spin_margin => leave that much for spinning
|
77
|
+
coarse_sleep_ns = remaining - self.spin_margin_ns
|
78
|
+
# asyncio.sleep takes seconds
|
79
|
+
await asyncio.sleep(coarse_sleep_ns / 1e9)
|
80
|
+
|
81
|
+
# Fine spin until target
|
82
|
+
target = self.next_target_ns
|
83
|
+
while True:
|
84
|
+
now = time.perf_counter_ns()
|
85
|
+
if now >= target:
|
86
|
+
break
|
87
|
+
# Tight spin: do nothing. If you see high CPU, you can insert
|
88
|
+
# a minimal pause on some platforms via time.sleep(0) sporadically,
|
89
|
+
# but that will hurt accuracy.
|
90
|
+
|
91
|
+
# Record stats
|
92
|
+
self.stats.last_target_ns = target
|
93
|
+
self.stats.last_wakeup_ns = now
|
94
|
+
self.stats.last_jitter_ns = now - target # >= 0
|
95
|
+
|
96
|
+
# Advance absolute target by exactly one period (no drift accumulation)
|
97
|
+
self.next_target_ns = target + self.period_ns
|
42
98
|
|
43
99
|
def reset(self):
|
44
|
-
"""Reset baseline
|
45
|
-
self.
|
100
|
+
"""Reset baseline; next sleep schedules from current time + period."""
|
101
|
+
self.next_target_ns = None
|
102
|
+
self.stats = TickerStats()
|
remote/spammer.py
CHANGED
@@ -30,7 +30,7 @@ if not any(
|
|
30
30
|
# ============================================================
|
31
31
|
# ✅ پیکربندی ایمن و پارامترهای پیشفرض
|
32
32
|
# ============================================================
|
33
|
-
MAX_RETRIES =
|
33
|
+
MAX_RETRIES = 3 # تلاش مجدد حداکثر برای هر ارسال
|
34
34
|
BASE_BACKOFF = 1.5 # ضریب بکآف برای retries (s)
|
35
35
|
FLOOD_COOLDOWN_CAP = 3600 # حداکثر زمان cooldown برای FloodWait (s)
|
36
36
|
|
@@ -45,23 +45,6 @@ if not hasattr(client_manager, "client_locks"):
|
|
45
45
|
# ============================================================
|
46
46
|
# 🧰 توابع کمکی
|
47
47
|
# ============================================================
|
48
|
-
def is_in_cooldown(phone: str) -> bool:
|
49
|
-
ts = _account_cooldowns.get(phone)
|
50
|
-
if not ts:
|
51
|
-
return False
|
52
|
-
return ts > asyncio.get_event_loop().time()
|
53
|
-
|
54
|
-
def set_cooldown(phone: str, seconds: float):
|
55
|
-
resume = asyncio.get_event_loop().time() + seconds
|
56
|
-
_account_cooldowns[phone] = resume
|
57
|
-
logger.info(f"{phone}: ⏳ Put on cooldown for {seconds:.1f}s (resume @ {resume:.1f})")
|
58
|
-
|
59
|
-
def clear_expired_cooldowns():
|
60
|
-
now = asyncio.get_event_loop().time()
|
61
|
-
expired = [p for p, t in _account_cooldowns.items() if t <= now]
|
62
|
-
for p in expired:
|
63
|
-
_account_cooldowns.pop(p, None)
|
64
|
-
logger.info(f"{p}: ✅ Cooldown expired")
|
65
48
|
|
66
49
|
async def _attempt_send(cli, acc_phone: str, target: str, text: str):
|
67
50
|
"""Try to send once, raising exceptions up to caller to handle specialized behavior."""
|
@@ -82,10 +65,6 @@ async def safe_send(acc_phone: str, spam_config: dict, text: str, remove_client_
|
|
82
65
|
بازگشت: True اگر ارسال موفق باشد، False در غیر این صورت.
|
83
66
|
"""
|
84
67
|
try:
|
85
|
-
# اگر اکانت در cooldown است، زود بازگردان False (بدون تلاش)
|
86
|
-
if is_in_cooldown(acc_phone):
|
87
|
-
logger.info(f"{acc_phone}: ⚠️ Skipping send because account is in cooldown.")
|
88
|
-
return False
|
89
68
|
|
90
69
|
cli = await client_manager.get_or_start_client(acc_phone)
|
91
70
|
if not cli:
|
@@ -122,8 +101,7 @@ async def safe_send(acc_phone: str, spam_config: dict, text: str, remove_client_
|
|
122
101
|
wait = e.value if hasattr(e, "value") else getattr(e, "x", 0)
|
123
102
|
# حداقل و حداکثر را منطقی حفظ کن
|
124
103
|
wait = max(1, min(wait, FLOOD_COOLDOWN_CAP))
|
125
|
-
logger.warning(f"{acc_phone}: ⏰ FloodWait {wait}s (code:{getattr(e, 'value', 'n/a')})")
|
126
|
-
set_cooldown(acc_phone, wait)
|
104
|
+
logger.warning(f"{acc_phone}: ⏰ FloodWait {wait}s (code:{getattr(e, 'value', 'n/a')})")
|
127
105
|
# صبر در این تابع به اندازهٔ wait ضروری نیست — cooldown کافی است.
|
128
106
|
return False
|
129
107
|
|
@@ -189,8 +167,7 @@ async def run_spammer(spam_config: dict, get_spam_texts, make_mention_html, remo
|
|
189
167
|
|
190
168
|
try:
|
191
169
|
while spam_config.get("run", False):
|
192
|
-
# cleanup expired cooldowns
|
193
|
-
clear_expired_cooldowns()
|
170
|
+
# cleanup expired cooldowns
|
194
171
|
|
195
172
|
# refresh active accounts (live snapshot)
|
196
173
|
active_accounts: Set[str] = set(client_manager.get_active_accounts())
|
@@ -200,12 +177,8 @@ async def run_spammer(spam_config: dict, get_spam_texts, make_mention_html, remo
|
|
200
177
|
continue
|
201
178
|
|
202
179
|
# filter out accounts currently in cooldown
|
203
|
-
acc_list = sorted([a for a in active_accounts
|
204
|
-
|
205
|
-
# همه در cooldown هستند — کمی صبر کن و ادامه بده
|
206
|
-
logger.info("تمام اکانتها در cooldown هستند؛ کمی صبر میکنیم...")
|
207
|
-
await asyncio.sleep(min(5, base_delay))
|
208
|
-
continue
|
180
|
+
acc_list = sorted([a for a in active_accounts])
|
181
|
+
|
209
182
|
|
210
183
|
texts = get_spam_texts()
|
211
184
|
if not texts:
|
File without changes
|
File without changes
|
File without changes
|