feat: add custom MUC nickname & handle 409 Conflict error

- Add XMPP_NICKNAME config in .env for custom MUC nick (fallback to JID username)
- Add _get_muc_nick() helper: resolve nick with suffix fallback on conflict
- Add _is_my_nick() helper: compare presence nick with expected nick per room
- Handle 409 Conflict in _on_session_start: try nick alternatives (lily_, lily__)
- Handle 409 Conflict in _muc_rejoin_coro: try nick alternatives with max 3 attempts
- Stop retry when all nick variations exhausted (anti-ban: avoid infinite retry)
- Reset nick suffix counter on successful join
- Update _on_groupchat_message filter to use _is_my_nick()
This commit is contained in:
Dita Aji Pratama 2026-06-10 11:11:27 +07:00
parent 93dd74d1b4
commit 41ec8287f7
2 changed files with 68 additions and 15 deletions

View File

@ -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)

View File

@ -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: