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:
parent
93dd74d1b4
commit
41ec8287f7
@ -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)
|
||||
|
||||
@ -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,18 +133,34 @@ 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
|
||||
# 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):
|
||||
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user