CliRemote 1.7.13__tar.gz → 1.8.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 (47) hide show
  1. {cliremote-1.7.13 → cliremote-1.8.0}/CliRemote.egg-info/PKG-INFO +1 -1
  2. {cliremote-1.7.13 → cliremote-1.8.0}/PKG-INFO +1 -1
  3. {cliremote-1.7.13 → cliremote-1.8.0}/pyproject.toml +1 -1
  4. {cliremote-1.7.13 → cliremote-1.8.0}/remote/admin_manager.py +7 -7
  5. cliremote-1.8.0/remote/precise_engine.py +102 -0
  6. {cliremote-1.7.13 → cliremote-1.8.0}/remote/spammer.py +5 -32
  7. {cliremote-1.7.13 → cliremote-1.8.0}/setup.py +1 -1
  8. cliremote-1.7.13/remote/precise_engine.py +0 -45
  9. {cliremote-1.7.13 → cliremote-1.8.0}/CliRemote.egg-info/SOURCES.txt +0 -0
  10. {cliremote-1.7.13 → cliremote-1.8.0}/CliRemote.egg-info/dependency_links.txt +0 -0
  11. {cliremote-1.7.13 → cliremote-1.8.0}/CliRemote.egg-info/requires.txt +0 -0
  12. {cliremote-1.7.13 → cliremote-1.8.0}/CliRemote.egg-info/top_level.txt +0 -0
  13. {cliremote-1.7.13 → cliremote-1.8.0}/LICENSE +0 -0
  14. {cliremote-1.7.13 → cliremote-1.8.0}/MANIFEST.in +0 -0
  15. {cliremote-1.7.13 → cliremote-1.8.0}/README.md +0 -0
  16. {cliremote-1.7.13 → cliremote-1.8.0}/remote/__init__.py +0 -0
  17. {cliremote-1.7.13 → cliremote-1.8.0}/remote/account_manager.py +0 -0
  18. {cliremote-1.7.13 → cliremote-1.8.0}/remote/account_viewer.py +0 -0
  19. {cliremote-1.7.13 → cliremote-1.8.0}/remote/analytics_manager.py +0 -0
  20. {cliremote-1.7.13 → cliremote-1.8.0}/remote/batch_manager.py +0 -0
  21. {cliremote-1.7.13 → cliremote-1.8.0}/remote/block_manager.py +0 -0
  22. {cliremote-1.7.13 → cliremote-1.8.0}/remote/caption_manager.py +0 -0
  23. {cliremote-1.7.13 → cliremote-1.8.0}/remote/cleaner.py +0 -0
  24. {cliremote-1.7.13 → cliremote-1.8.0}/remote/client_manager.py +0 -0
  25. {cliremote-1.7.13 → cliremote-1.8.0}/remote/client_picker.py +0 -0
  26. {cliremote-1.7.13 → cliremote-1.8.0}/remote/config.py +0 -0
  27. {cliremote-1.7.13 → cliremote-1.8.0}/remote/device_manager.py +0 -0
  28. {cliremote-1.7.13 → cliremote-1.8.0}/remote/file_sender.py +0 -0
  29. {cliremote-1.7.13 → cliremote-1.8.0}/remote/getcode_controller.py +0 -0
  30. {cliremote-1.7.13 → cliremote-1.8.0}/remote/health.py +0 -0
  31. {cliremote-1.7.13 → cliremote-1.8.0}/remote/help_menu.py +0 -0
  32. {cliremote-1.7.13 → cliremote-1.8.0}/remote/init.py +0 -0
  33. {cliremote-1.7.13 → cliremote-1.8.0}/remote/join_controller.py +0 -0
  34. {cliremote-1.7.13 → cliremote-1.8.0}/remote/joiner.py +0 -0
  35. {cliremote-1.7.13 → cliremote-1.8.0}/remote/leave_controller.py +0 -0
  36. {cliremote-1.7.13 → cliremote-1.8.0}/remote/lefter.py +0 -0
  37. {cliremote-1.7.13 → cliremote-1.8.0}/remote/mention_manager.py +0 -0
  38. {cliremote-1.7.13 → cliremote-1.8.0}/remote/profile_info.py +0 -0
  39. {cliremote-1.7.13 → cliremote-1.8.0}/remote/profile_media.py +0 -0
  40. {cliremote-1.7.13 → cliremote-1.8.0}/remote/profile_privacy.py +0 -0
  41. {cliremote-1.7.13 → cliremote-1.8.0}/remote/speed_manager.py +0 -0
  42. {cliremote-1.7.13 → cliremote-1.8.0}/remote/stop_manager.py +0 -0
  43. {cliremote-1.7.13 → cliremote-1.8.0}/remote/text_manager.py +0 -0
  44. {cliremote-1.7.13 → cliremote-1.8.0}/remote/username_manager.py +0 -0
  45. {cliremote-1.7.13 → cliremote-1.8.0}/remote/utils/__init__.py +0 -0
  46. {cliremote-1.7.13 → cliremote-1.8.0}/remote/utils/sqlite_utils.py +0 -0
  47. {cliremote-1.7.13 → cliremote-1.8.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CliRemote
3
- Version: 1.7.13
3
+ Version: 1.8.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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CliRemote
3
- Version: 1.7.13
3
+ Version: 1.8.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.7.13"
7
+ version = "1.8.0"
8
8
  description = "Remote client framework for Telegram automation using Pyrogram"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -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 .config import OWNER_ID
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
@@ -0,0 +1,102 @@
1
+ # antispam_core/precise_engine.py
2
+ import asyncio
3
+ import time
4
+ from dataclasses import dataclass
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:
13
+ """
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.
31
+ """
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()
53
+
54
+ async def sleep(self):
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
98
+
99
+ def reset(self):
100
+ """Reset baseline; next sleep schedules from current time + period."""
101
+ self.next_target_ns = None
102
+ self.stats = TickerStats()
@@ -30,7 +30,7 @@ if not any(
30
30
  # ============================================================
31
31
  # ✅ پیکربندی ایمن و پارامترهای پیش‌فرض
32
32
  # ============================================================
33
- MAX_RETRIES = 2 # تلاش مجدد حداکثر برای هر ارسال
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 if not is_in_cooldown(a)])
204
- if not acc_list:
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:
@@ -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.7.13",
8
+ version="1.8.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,45 +0,0 @@
1
- # antispam_core/precise_engine.py
2
- import asyncio
3
- import time
4
-
5
- class PreciseTicker:
6
- """
7
- A drift-free ticker.
8
- Usage:
9
- ticker = PreciseTicker(interval=1.0)
10
- while running:
11
- # do work
12
- await ticker.sleep()
13
- Behavior:
14
- - Ensures interval spacing between wakes without accumulating drift.
15
- - If the work took longer than the interval, it schedules next wake as now + interval
16
- (so it doesn't try to "catch up" by sleeping negative amounts).
17
- - On first use, it sets the baseline relative to time.perf_counter().
18
- """
19
- def __init__(self, interval: float):
20
- if interval <= 0:
21
- raise ValueError("interval must be > 0")
22
- self.interval = float(interval)
23
- self.next_tick = None
24
-
25
- async def sleep(self):
26
- now = time.perf_counter()
27
- if self.next_tick is None:
28
- # first tick: schedule next at now + interval
29
- self.next_tick = now + self.interval
30
-
31
- delay = self.next_tick - now
32
-
33
- if delay > 0:
34
- # normal case: wait remaining time
35
- await asyncio.sleep(delay)
36
- # advance to next slot
37
- self.next_tick += self.interval
38
- else:
39
- # work took longer than interval; reset baseline to now + interval
40
- self.next_tick = now + self.interval
41
- # don't sleep now (we are already late)
42
-
43
- def reset(self):
44
- """Reset baseline so next sleep schedules from current time."""
45
- self.next_tick = 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