diff --git a/config.py b/config.py index 0ef0421..c5e3bc2 100644 --- a/config.py +++ b/config.py @@ -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) diff --git a/services/xmpp_client.py b/services/xmpp_client.py index 313a691..0d9ab06 100644 --- a/services/xmpp_client.py +++ b/services/xmpp_client.py @@ -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: - try: - await self.plugin['xep_0045'].join_muc_wait(room, self._muc_nick, maxstanzas=0) - print(f'[{_ts()}] Joined MUC room: {room}') - except Exception as e: - print(f'[{_ts()}] MUC join failed ({room}): {e}') + # 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 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: