feat: add humanize delays & anti-ban MUC rejoin mechanism
- Add READ_DELAY_MIN/MAX config for reading delay (1-2s random) - Add TYPING_SPEED/MAX config for proporsional typing delay - Add reading delay before processing DM & MUC messages - Add typing delay before sending any XMPP message (proporsional to msg length) - Add auto-rejoin MUC on unavailable/error with exponential backoff - Add retry join on session_start with incremental delay (3 attempts) - Add cooldown between rejoin attempts to prevent join-spam - Cancel pending rejoin tasks on disconnect - Reset rejoin counter on successful join
This commit is contained in:
parent
78387899ad
commit
93dd74d1b4
@ -20,3 +20,9 @@ XMPP_ENABLED = os.getenv("XMPP_ENABLED", default="False"
|
||||
XMPP_USERNAME = os.getenv("XMPP_USERNAME", default="" )
|
||||
XMPP_PASSWORD = os.getenv("XMPP_PASSWORD", default="" )
|
||||
XMPP_MUC_ROOMS = os.getenv("XMPP_MUC_ROOMS", default="" )
|
||||
|
||||
# Humanize Delay Configuration (anti-bot detection)
|
||||
READ_DELAY_MIN = float( os.getenv("READ_DELAY_MIN", default="1.0" ) ) # min reading delay (detik)
|
||||
READ_DELAY_MAX = float( os.getenv("READ_DELAY_MAX", default="2.0" ) ) # max reading delay (detik)
|
||||
TYPING_SPEED = float( os.getenv("TYPING_SPEED", default="15.0" ) ) # karakter per detik (kecepatan mengetik)
|
||||
TYPING_MAX = float( os.getenv("TYPING_MAX", default="10.0" ) ) # batas max typing delay (detik)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
import signal
|
||||
import threading
|
||||
from datetime import datetime
|
||||
@ -7,11 +8,32 @@ from datetime import datetime
|
||||
from slixmpp import ClientXMPP
|
||||
from services.session_manager import SessionManager
|
||||
|
||||
import config
|
||||
|
||||
# Anti-ban: delay constants for MUC rejoin behavior
|
||||
MUC_REJOIN_INITIAL_DELAY = 5.0 # detik, delay awal sebelum rejoin
|
||||
MUC_REJOIN_BACKOFF_MULT = 2.0 # multiplier exponential backoff
|
||||
MUC_REJOIN_MAX_DELAY = 300.0 # detik, batas max backoff (5 menit)
|
||||
MUC_REJOIN_COOLDOWN = 10.0 # detik, cooldown minimum antar rejoin attempt
|
||||
|
||||
|
||||
def _ts():
|
||||
return datetime.now().strftime('%H:%M:%S')
|
||||
|
||||
|
||||
def _typing_delay(text: str) -> float:
|
||||
"""Hitung delay mengetik (detik) proporsional dengan panjang teks."""
|
||||
char_count = len(text) if text else 0
|
||||
delay = char_count / config.TYPING_SPEED
|
||||
return max(1.0, min(delay, config.TYPING_MAX))
|
||||
|
||||
|
||||
async def _read_delay():
|
||||
"""Delay simulasi membaca pesan user."""
|
||||
delay = random.uniform(config.READ_DELAY_MIN, config.READ_DELAY_MAX)
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
|
||||
class XMPPClient(ClientXMPP):
|
||||
def __init__(self, jid, password, llm_client, tools_definition, TOOLS,
|
||||
TOOL_HANDLERS, build_system_prompt, agent_max_iterations,
|
||||
@ -32,6 +54,11 @@ class XMPPClient(ClientXMPP):
|
||||
self._loop = None
|
||||
self._stopped: asyncio.Event | None = None
|
||||
|
||||
# Anti-ban: MUC rejoin tracking per room
|
||||
self._muc_rejoin_attempts: dict[str, int] = {} # room -> jumlah attempt
|
||||
self._muc_rejoin_tasks: dict[str, asyncio.Task] = {} # room -> pending rejoin task
|
||||
self._muc_last_join: dict[str, datetime] = {} # room -> terakhir join (cooldown)
|
||||
|
||||
self.auto_reconnect = True
|
||||
|
||||
self.register_plugin('xep_0030')
|
||||
@ -45,22 +72,107 @@ class XMPPClient(ClientXMPP):
|
||||
self.add_event_handler('connected', self._on_connected)
|
||||
self.add_event_handler('groupchat_presence', self._on_muc_presence)
|
||||
|
||||
def _calc_rejoin_delay(self, room: str) -> float:
|
||||
"""Anti-ban: hitung delay rejoin dengan exponential backoff."""
|
||||
attempts = self._muc_rejoin_attempts.get(room, 0)
|
||||
delay = MUC_REJOIN_INITIAL_DELAY * (MUC_REJOIN_BACKOFF_MULT ** attempts)
|
||||
return min(delay, MUC_REJOIN_MAX_DELAY)
|
||||
|
||||
def _schedule_muc_rejoin(self, room: str):
|
||||
"""Anti-ban: schedule rejoin room dengan backoff & cooldown."""
|
||||
# Cancel pending rejoin task untuk room yang sama (anti-ban: avoid duplicate rejoin)
|
||||
pending = self._muc_rejoin_tasks.get(room)
|
||||
if pending and not pending.done():
|
||||
pending.cancel()
|
||||
print(f'[{_ts()}] MUC [{room}] Cancelled pending rejoin (new trigger)')
|
||||
|
||||
# Check cooldown: jangan rejoin terlalu cepat berturut-turut
|
||||
now = datetime.now()
|
||||
last_join = self._muc_last_join.get(room)
|
||||
if last_join:
|
||||
elapsed = (now - last_join).total_seconds()
|
||||
if elapsed < MUC_REJOIN_COOLDOWN:
|
||||
# Anti-ban: too soon, schedule delayed rejoin instead of immediate
|
||||
cooldown_left = MUC_REJOIN_COOLDOWN - elapsed
|
||||
print(f'[{_ts()}] MUC [{room}] Cooldown active ({cooldown_left:.0f}s left), delaying rejoin')
|
||||
delay = cooldown_left + self._calc_rejoin_delay(room)
|
||||
else:
|
||||
delay = self._calc_rejoin_delay(room)
|
||||
else:
|
||||
delay = self._calc_rejoin_delay(room)
|
||||
|
||||
# Increment attempt counter (anti-ban: track for exponential backoff)
|
||||
attempts = self._muc_rejoin_attempts.get(room, 0) + 1
|
||||
self._muc_rejoin_attempts[room] = attempts
|
||||
|
||||
print(f'[{_ts()}] MUC [{room}] Rejoin scheduled in {delay:.0f}s (attempt #{attempts})')
|
||||
|
||||
if self._loop and not self._loop.is_closed():
|
||||
task = asyncio.run_coroutine_threadsafe(
|
||||
self._muc_rejoin_coro(room, delay), self._loop
|
||||
)
|
||||
self._muc_rejoin_tasks[room] = task
|
||||
|
||||
async def _muc_rejoin_coro(self, room: str, delay: float):
|
||||
"""Anti-ban: coroutine untuk rejoin room setelah delay."""
|
||||
try:
|
||||
await asyncio.sleep(delay)
|
||||
# Double-check: jangan rejoin kalau sudah di _muc_ready
|
||||
if room in self._muc_ready:
|
||||
print(f'[{_ts()}] MUC [{room}] Already ready, skip rejoin')
|
||||
return
|
||||
nick = self._muc_nick
|
||||
print(f'[{_ts()}] MUC [{room}] Rejoining as {nick}...')
|
||||
await self.plugin['xep_0045'].join_muc_wait(room, nick, maxstanzas=0)
|
||||
self._muc_last_join[room] = datetime.now()
|
||||
# _muc_ready akan di-set oleh _on_muc_presence saat join berhasil
|
||||
self._muc_rejoin_attempts.pop(room, None)
|
||||
print(f'[{_ts()}] MUC [{room}] Rejoin successful')
|
||||
except asyncio.CancelledError:
|
||||
print(f'[{_ts()}] MUC [{room}] Rejoin cancelled')
|
||||
except Exception as e:
|
||||
print(f'[{_ts()}] MUC [{room}] Rejoin failed: {e}')
|
||||
# Anti-ban: retry with incremented backoff on failure
|
||||
self._schedule_muc_rejoin(room)
|
||||
|
||||
async def _on_connected(self, event):
|
||||
print(f'[{_ts()}] XMPP connected')
|
||||
|
||||
async def _on_disconnected(self, event):
|
||||
print(f'[{_ts()}] XMPP disconnected')
|
||||
# Anti-ban: cancel all pending rejoin tasks on disconnect
|
||||
for room, task in list(self._muc_rejoin_tasks.items()):
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
print(f'[{_ts()}] MUC [{room}] Cancelled pending rejoin (disconnected)')
|
||||
self._muc_rejoin_tasks.clear()
|
||||
|
||||
async def _on_session_start(self, event):
|
||||
self.send_presence()
|
||||
self.get_roster()
|
||||
print(f'[{_ts()}] XMPP online as {self.boundjid.full}')
|
||||
for room in self._muc_rooms:
|
||||
# Anti-ban: retry join dengan incremental delay (max 3 attempts)
|
||||
success = False
|
||||
for attempt in range(1, 4):
|
||||
try:
|
||||
await self.plugin['xep_0045'].join_muc_wait(room, self._muc_nick, maxstanzas=0)
|
||||
print(f'[{_ts()}] Joined MUC room: {room}')
|
||||
self._muc_last_join[room] = datetime.now()
|
||||
self._muc_rejoin_attempts.pop(room, None)
|
||||
success = True
|
||||
break
|
||||
except Exception as e:
|
||||
print(f'[{_ts()}] MUC join failed ({room}): {e}')
|
||||
print(f'[{_ts()}] MUC join attempt #{attempt} failed ({room}): {e}')
|
||||
if attempt < 3:
|
||||
# Anti-ban: wait before retry (2s, 4s) — tidak terlalu agresif
|
||||
retry_delay = 2.0 * attempt
|
||||
print(f'[{_ts()}] MUC [{room}] Retrying in {retry_delay:.0f}s...')
|
||||
await asyncio.sleep(retry_delay)
|
||||
if not success:
|
||||
# Anti-ban: semua attempt gagal, schedule background rejoin
|
||||
print(f'[{_ts()}] MUC [{room}] All join attempts failed, scheduling background rejoin')
|
||||
self._schedule_muc_rejoin(room)
|
||||
|
||||
def _on_message(self, msg):
|
||||
if msg['type'] not in ('chat', 'normal'):
|
||||
@ -93,10 +205,21 @@ class XMPPClient(ClientXMPP):
|
||||
ptype = presence['type']
|
||||
if nick == self._muc_nick and ptype not in ('unavailable', 'error'):
|
||||
self._muc_ready.add(room)
|
||||
# Reset rejoin counter on successful join (anti-ban: avoid accumulating backoff)
|
||||
self._muc_rejoin_attempts.pop(room, None)
|
||||
if ptype == 'unavailable':
|
||||
print(f'[{_ts()}] MUC [{room}] <{nick}> left')
|
||||
# Anti-ban: remove from ready set on unavailable to keep state consistent
|
||||
self._muc_ready.discard(room)
|
||||
# Anti-ban: trigger auto-rejoin with exponential backoff
|
||||
if nick == self._muc_nick:
|
||||
self._schedule_muc_rejoin(room)
|
||||
elif ptype == 'error':
|
||||
print(f'[{_ts()}] MUC [{room}] error: {presence}')
|
||||
# Anti-bban: also rejoin on error (e.g. temporary failure)
|
||||
if nick == self._muc_nick:
|
||||
self._muc_ready.discard(room)
|
||||
self._schedule_muc_rejoin(room)
|
||||
else:
|
||||
print(f'[{_ts()}] MUC [{room}] <{nick}> joined (type={ptype})')
|
||||
|
||||
@ -117,6 +240,11 @@ class XMPPClient(ClientXMPP):
|
||||
session.add_message('user', body)
|
||||
|
||||
self._schedule_send(jid, f'> {body}\nThinking...')
|
||||
|
||||
# Delay 1: simulasi membaca pesan user
|
||||
if self._loop and not self._loop.is_closed():
|
||||
asyncio.run_coroutine_threadsafe(_read_delay(), self._loop)
|
||||
|
||||
self._agent_loop(session, jid, body, 'chat')
|
||||
|
||||
session.start_timer(300, self._timeout_session, jid, 'chat')
|
||||
@ -137,6 +265,11 @@ class XMPPClient(ClientXMPP):
|
||||
session.add_message('user', prefixed)
|
||||
|
||||
self._schedule_send(room, f'> [{nick}] {body}\nThinking...', mtype='groupchat')
|
||||
|
||||
# Delay 1: simulasi membaca pesan user
|
||||
if self._loop and not self._loop.is_closed():
|
||||
asyncio.run_coroutine_threadsafe(_read_delay(), self._loop)
|
||||
|
||||
self._agent_loop(session, room, f'[{nick}] {body}', 'groupchat')
|
||||
|
||||
session.start_timer(300, self._timeout_session, room, 'groupchat')
|
||||
@ -209,6 +342,11 @@ class XMPPClient(ClientXMPP):
|
||||
|
||||
async def _send_coro(self, to, body, mtype):
|
||||
try:
|
||||
# Delay 2: simulasi mengetik (proporsional dengan panjang pesan)
|
||||
delay = _typing_delay(body)
|
||||
print(f'[{_ts()}] Typing delay: {delay:.1f}s ({len(body)} chars)')
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
msg = self.make_message(mto=to, mbody=body, mtype=mtype)
|
||||
msg.send()
|
||||
except Exception as e:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user