diff --git a/config.py b/config.py index c5e3bc2..846050a 100644 --- a/config.py +++ b/config.py @@ -20,6 +20,7 @@ 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="" ) +XMPP_NICKNAME = os.getenv("XMPP_NICKNAME", default="" ) # custom nick MUC (kosong = pakai username) # Humanize Delay Configuration (anti-bot detection) READ_DELAY_MIN = float( os.getenv("READ_DELAY_MIN", default="1.0" ) ) # min reading delay (detik) diff --git a/services/xmpp_client.py b/services/xmpp_client.py index 0d9ab06..4e0f823 100644 --- a/services/xmpp_client.py +++ b/services/xmpp_client.py @@ -15,6 +15,7 @@ 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 +MUC_NICK_SUFFIX_MAX = 3 # max coba nick alternatif (anti-ban: jangan terlalu banyak) def _ts(): @@ -47,7 +48,9 @@ class XMPPClient(ClientXMPP): self._build_system_prompt = build_system_prompt self._max_iterations = agent_max_iterations self._muc_rooms = muc_rooms or [] - self._muc_nick = jid.split('@')[0] + # Custom nick dari config, fallback ke username JID + self._muc_nick = config.XMPP_NICKNAME.strip() or jid.split('@')[0] + self._muc_nick_suffix = 0 # counter untuk nick alternatif saat 409 self._muc_ready: set[str] = set() self._session_mgr = SessionManager() @@ -72,6 +75,15 @@ class XMPPClient(ClientXMPP): self.add_event_handler('connected', self._on_connected) self.add_event_handler('groupchat_presence', self._on_muc_presence) + def _get_muc_nick(self, room: str) -> str: + """Anti-ban: resolve nick untuk room, coba nick alternatif kalau conflict.""" + base = config.XMPP_NICKNAME.strip() or self._muc_nick + suffix = self._muc_rejoin_attempts.get("_nick_" + room, 0) + if suffix == 0: + return base + # Anti-ban: append suffix untuk menghindari 409 Conflict + return f"{base}_{suffix}" + def _calc_rejoin_delay(self, room: str) -> float: """Anti-ban: hitung delay rejoin dengan exponential backoff.""" attempts = self._muc_rejoin_attempts.get(room, 0) @@ -121,19 +133,35 @@ class XMPPClient(ClientXMPP): if room in self._muc_ready: print(f'[{_ts()}] MUC [{room}] Already ready, skip rejoin') return - nick = self._muc_nick + nick = self._get_muc_nick(room) 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') + self._muc_rejoin_attempts.pop("_nick_" + room, None) + print(f'[{_ts()}] MUC [{room}] Rejoin successful as {nick}') 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) + # Anti-ban: handle 409 Conflict - nick sudah dipakai orang lain + if '409' in str(e) or 'conflict' in str(e).lower(): + nick_attempts = self._muc_rejoin_attempts.get("_nick_" + room, 0) + if nick_attempts < MUC_NICK_SUFFIX_MAX: + # Anti-ban: coba nick alternatif (lily_, lily__) + self._muc_rejoin_attempts["_nick_" + room] = nick_attempts + 1 + new_nick = self._get_muc_nick(room) + print(f'[{_ts()}] MUC [{room}] Nick conflict, trying alternative: {new_nick}') + # Retry segera dengan nick baru (tanpa backoff rejoin, tapi tetap ada delay biasa) + self._schedule_muc_rejoin(room) + else: + # Anti-ban: semua nick alternativehabis, stop retry untuk avoid ban + print(f'[{_ts()}] MUC [{room}] All nick variations exhausted, skipping room') + print(f'[{_ts()}] MUC [{room}] Set XMPP_NICKNAME in .env to a unique nick') + else: + # Anti-ban: error biasa (network, dll), retry with backoff + self._schedule_muc_rejoin(room) async def _on_connected(self, event): print(f'[{_ts()}] XMPP connected') @@ -152,20 +180,35 @@ class XMPPClient(ClientXMPP): 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) + # Anti-ban: retry join dengan incremental delay & nick fallback success = False for attempt in range(1, 4): + nick = self._get_muc_nick(room) try: - await self.plugin['xep_0045'].join_muc_wait(room, self._muc_nick, maxstanzas=0) - print(f'[{_ts()}] Joined MUC room: {room}') + await self.plugin['xep_0045'].join_muc_wait(room, nick, maxstanzas=0) + print(f'[{_ts()}] Joined MUC room: {room} as {nick}') self._muc_last_join[room] = datetime.now() self._muc_rejoin_attempts.pop(room, None) + self._muc_rejoin_attempts.pop("_nick_" + 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 + # Anti-ban: handle 409 Conflict - coba nick alternatif + if '409' in str(e) or 'conflict' in str(e).lower(): + nick_attempts = self._muc_rejoin_attempts.get("_nick_" + room, 0) + if nick_attempts < MUC_NICK_SUFFIX_MAX: + nick_attempts += 1 + self._muc_rejoin_attempts["_nick_" + room] = nick_attempts + print(f'[{_ts()}] MUC [{room}] Nick conflict, switching to: {self._get_muc_nick(room)}') + # Retry segera dengan nick baru (jangan wait) + continue + else: + # Anti-ban: semua nick alternatif habis + print(f'[{_ts()}] MUC [{room}] All nick variations exhausted') + break + elif attempt < 3: + # Anti-ban: error biasa, wait before retry (2s, 4s) retry_delay = 2.0 * attempt print(f'[{_ts()}] MUC [{room}] Retrying in {retry_delay:.0f}s...') await asyncio.sleep(retry_delay) @@ -187,8 +230,9 @@ class XMPPClient(ClientXMPP): def _on_groupchat_message(self, msg): if msg['type'] != 'groupchat': return + room = msg['from'].bare nick = msg['from'].resource - if nick == self._muc_nick: + if self._is_my_nick(room, nick): return room = msg['from'].bare if room not in self._muc_ready: @@ -199,25 +243,33 @@ class XMPPClient(ClientXMPP): print(f'[{_ts()}] MUC [{room}] <{nick}>: {body[:60]}') threading.Thread(target=self._process_muc, args=(room, nick, body), daemon=True).start() + def _is_my_nick(self, room: str, nick: str) -> bool: + """Anti-ban: cek apakah nick yang dimasukan sesuai dengan nick bot di room.""" + expected = self._get_muc_nick(room) + # Bandingkan dengan nick yang diharapkan, plus base nick tanpa suffix + base = config.XMPP_NICKNAME.strip() or self._muc_nick + return nick == expected or nick == base + def _on_muc_presence(self, presence): room = presence['from'].bare nick = presence['from'].resource ptype = presence['type'] - if nick == self._muc_nick and ptype not in ('unavailable', 'error'): + if self._is_my_nick(room, 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) + self._muc_rejoin_attempts.pop("_nick_" + 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: + if self._is_my_nick(room, 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: + # Anti-ban: also rejoin on error (e.g. temporary failure) + if self._is_my_nick(room, nick): self._muc_ready.discard(room) self._schedule_muc_rejoin(room) else: