2026-06-08 02:41:29 +07:00
import asyncio
2026-08-02 23:38:50 +07:00
import logging
2026-06-10 10:54:56 +07:00
import random
2026-06-08 02:41:29 +07:00
import signal
import threading
2026-08-02 23:38:50 +07:00
import traceback
2026-06-08 02:41:29 +07:00
from datetime import datetime
from slixmpp import ClientXMPP
from services . session_manager import SessionManager
2026-06-23 11:23:04 +07:00
from lib . agent_loop import run_agent_loop
2026-06-08 02:41:29 +07:00
2026-06-10 10:54:56 +07:00
import config
2026-06-10 17:15:25 +07:00
from tools . roleplayer import should_respond
2026-06-23 11:23:04 +07:00
from lib import personality
2026-07-07 17:17:50 +07:00
from lib import ragroleplay
2026-06-10 10:54:56 +07:00
# Anti-ban: delay constants for MUC rejoin behavior
2026-08-02 23:38:50 +07:00
MUC_REJOIN_INITIAL_DELAY = 30.0 # detik, delay awal sebelum rejoin (lebih gentle)
MUC_REJOIN_BACKOFF_MULT = 1.5 # multiplier exponential backoff (lebih gentle)
MUC_REJOIN_MAX_DELAY = 600.0 # detik, batas max backoff (10 menit)
MUC_REJOIN_COOLDOWN = 60.0 # detik, cooldown minimum antar rejoin attempt
2026-06-10 11:11:27 +07:00
MUC_NICK_SUFFIX_MAX = 3 # max coba nick alternatif (anti-ban: jangan terlalu banyak)
2026-08-02 23:38:50 +07:00
MUC_JOIN_JITTER = 0.3 # jitter 30% untuk randomize delay
# Anti-ban: Message queue rate limiting
MSG_QUEUE_DELAY_MIN = 3.0 # detik, delay minimum antar pesan keluar
MSG_QUEUE_DELAY_MAX = 8.0 # detik, delay maksimum antar pesan keluar
MSG_QUEUE_JITTER = 0.25 # jitter 25% untuk randomize delay
MSG_TYPING_SPEED_MIN = 8.0 # chars/sec, minimum typing speed
MSG_TYPING_SPEED_MAX = 20.0 # chars/sec, maksimum typing speed
MSG_READ_DELAY_MIN = 2.0 # detik, minimum read delay
MSG_READ_DELAY_MAX = 5.0 # detik, maksimum read delay
# Anti-ban: Adaptive rate limiting (deteksi throttle dari server)
ADAPTIVE_WINDOW_SIZE = 20 # jumlah pesan terakhir untuk tracking
ADAPTIVE_ERROR_THRESHOLD = 0.15 # 15% error rate = server mulai throttle
ADAPTIVE_SLOW_THRESHOLD = 5.0 # detik, response time > ini = server lambat
ADAPTIVE_BACKOFF_MULT = 2.0 # multiplier delay saat throttle detected
ADAPTIVE_RECOVERY_TIME = 300 # detik, waktu tunggu sebelum recovery ke normal
# Anti-ban: Connection behavior (sangat penting untuk conversations.im)
CONNECTION_KEEPALIVE = True # enable keepalive
CONNECTION_KEEPALIVE_INTERVAL = 300 # detik, ping interval (5 menit, jangan terlalu sering)
AUTO_RECONNECT_ENABLED = False # JANGAN auto reconnect instant (bot-like)
RECONNECT_DELAY_MIN = 30.0 # detik, delay minimum sebelum reconnect manual
RECONNECT_DELAY_MAX = 120.0 # detik, delay maksimum sebelum reconnect
2026-06-10 10:54:56 +07:00
2026-06-08 02:41:29 +07:00
def _ts ( ) :
return datetime . now ( ) . strftime ( ' % H: % M: % S ' )
2026-08-02 23:38:50 +07:00
def _setup_debug_logging ( ) :
""" Debug: aktifkan logging DEBUG slixmpp untuk menampilkan SEMUA stanza XML keluar/masuk.
Sumber utama diagnosa :
- [ slixmpp . xmlstream . xmlstream ] SEND : < xml > - > stanza yang benar2 dikirim ke server
- [ slixmpp . xmlstream . xmlstream ] RECV : < xml > - > stanza yang diterima dari server
"""
if not config . XMPP_DEBUG :
return
logging . basicConfig (
level = logging . DEBUG ,
format = ' [ %(asctime)s ][ %(name)s ] %(message)s ' ,
datefmt = ' % H: % M: % S ' ,
force = True ,
)
logging . getLogger ( ' slixmpp ' ) . setLevel ( logging . DEBUG )
print ( f ' [ { _ts ( ) } ] DEBUG: XMPP debug logging ENABLED (semua stanza XML akan dicetak) ' , flush = True )
def _dbg ( msg ) :
""" Debug: cetak pesan debug hanya jika config.XMPP_DEBUG aktif. """
if config . XMPP_DEBUG :
print ( f ' [ { _ts ( ) } ] DEBUG: { msg } ' , flush = True )
def _add_jitter ( delay : float , jitter_factor : float = MSG_QUEUE_JITTER ) - > float :
""" Tambahkan jitter acak ke delay untuk menghindari pola terdeteksi. """
jitter = delay * jitter_factor
return delay + random . uniform ( - jitter , jitter )
2026-06-10 10:54:56 +07:00
def _typing_delay ( text : str ) - > float :
""" Hitung delay mengetik (detik) proporsional dengan panjang teks. """
char_count = len ( text ) if text else 0
2026-08-02 23:38:50 +07:00
# Random typing speed untuk lebih human-like
typing_speed = random . uniform ( MSG_TYPING_SPEED_MIN , MSG_TYPING_SPEED_MAX )
delay = char_count / typing_speed
# Clamp antara 1-15 detik, tambah jitter
return _add_jitter ( max ( 1.0 , min ( delay , 15.0 ) ) )
2026-06-10 10:54:56 +07:00
async def _read_delay ( ) :
""" Delay simulasi membaca pesan user. """
2026-08-02 23:38:50 +07:00
delay = random . uniform ( MSG_READ_DELAY_MIN , MSG_READ_DELAY_MAX )
2026-06-10 10:54:56 +07:00
await asyncio . sleep ( delay )
2026-08-02 23:38:50 +07:00
class AdaptiveRateLimiter :
""" Anti-ban: adaptive rate limiting yang mendeteksi throttle dari server. """
def __init__ ( self ) :
self . _send_times : list [ float ] = [ ] # timestamp pengiriman pesan
self . _errors : list [ bool ] = [ ] # apakah pengiriman error
self . _response_times : list [ float ] = [ ] # waktu response server (jika ada)
self . _throttle_detected = False
self . _throttle_start : float | None = None
self . _backoff_multiplier = 1.0
def record_send ( self , success : bool , response_time : float | None = None ) :
""" Catat hasil pengiriman pesan. """
now = asyncio . get_event_loop ( ) . time ( )
self . _send_times . append ( now )
self . _errors . append ( not success )
if response_time is not None :
self . _response_times . append ( response_time )
# Keep only recent window
if len ( self . _send_times ) > ADAPTIVE_WINDOW_SIZE :
self . _send_times . pop ( 0 )
self . _errors . pop ( 0 )
if len ( self . _response_times ) > ADAPTIVE_WINDOW_SIZE :
self . _response_times . pop ( 0 )
self . _check_throttle ( )
def _check_throttle ( self ) :
""" Cek apakah server mulai throttle. """
if len ( self . _errors ) < 5 : # butuh minimal sample
return
# Hitung error rate
error_rate = sum ( self . _errors ) / len ( self . _errors )
# Hitung avg response time
avg_response = 0
if self . _response_times :
avg_response = sum ( self . _response_times ) / len ( self . _response_times )
# Deteksi throttle: error rate tinggi ATAU response time lambat
was_throttled = self . _throttle_detected
if error_rate > = ADAPTIVE_ERROR_THRESHOLD or avg_response > = ADAPTIVE_SLOW_THRESHOLD :
if not self . _throttle_detected :
self . _throttle_detected = True
self . _throttle_start = asyncio . get_event_loop ( ) . time ( )
self . _backoff_multiplier = ADAPTIVE_BACKOFF_MULT
print ( f ' [ { _ts ( ) } ] ADAPTIVE: Throttle detected! '
f ' error_rate= { error_rate : .1% } , avg_response= { avg_response : .1f } s, '
f ' backing off x { self . _backoff_multiplier } ' , flush = True )
else :
# Recovery: cek apakah sudah cukup waktu untuk kembali normal
if self . _throttle_detected and self . _throttle_start :
elapsed = asyncio . get_event_loop ( ) . time ( ) - self . _throttle_start
if elapsed > = ADAPTIVE_RECOVERY_TIME :
self . _throttle_detected = False
self . _backoff_multiplier = 1.0
print ( f ' [ { _ts ( ) } ] ADAPTIVE: Recovered to normal rate ' , flush = True )
def get_delay_multiplier ( self ) - > float :
""" Dapatkan multiplier untuk delay saat ini. """
return self . _backoff_multiplier
def is_throttled ( self ) - > bool :
""" Apakah sedang dalam kondisi throttle. """
return self . _throttle_detected
2026-06-08 02:41:29 +07:00
class XMPPClient ( ClientXMPP ) :
def __init__ ( self , jid , password , llm_client , tools_definition , TOOLS ,
TOOL_HANDLERS , build_system_prompt , agent_max_iterations ,
muc_rooms = None ) :
super ( ) . __init__ ( jid , password )
self . _llm = llm_client
self . _tools_def = tools_definition
self . _TOOLS = TOOLS
self . _TOOL_HANDLERS = TOOL_HANDLERS
self . _build_system_prompt = build_system_prompt
self . _max_iterations = agent_max_iterations
2026-06-14 10:56:55 +07:00
self . _skill = config . AGENT_SKILL
2026-06-08 02:41:29 +07:00
self . _muc_rooms = muc_rooms or [ ]
2026-06-10 11:11:27 +07:00
# 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
2026-06-08 02:41:29 +07:00
self . _muc_ready : set [ str ] = set ( )
self . _session_mgr = SessionManager ( )
self . _loop = None
self . _stopped : asyncio . Event | None = None
2026-06-10 10:54:56 +07:00
# 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)
2026-08-02 23:38:50 +07:00
# Anti-ban: Message queue untuk rate limiting
self . _msg_queue : asyncio . Queue | None = None
self . _msg_worker_task : asyncio . Task | None = None
self . _msg_worker_running = False
# Anti-ban: Adaptive rate limiter
self . _rate_limiter = AdaptiveRateLimiter ( )
# Anti-ban: reconnect tracking (jangan instant reconnect)
self . _reconnect_scheduled = False
self . _last_disconnect : datetime | None = None
# Anti-ban: JANGAN auto reconnect instant (bot-like behavior)
self . auto_reconnect = AUTO_RECONNECT_ENABLED
# ── Anti-ban (conversations.im): ringkasan solusi yang membuat akun
# tidak dianggap spam/di-block (penyebab: klien default slixmpp
# mengiklankan identity `client/bot` + caps node slixmpp) ────────────
#
# 1. JANGAN kirim 'subscribe' balik saat ada yang subscribe (contact
# farming = red flag). auto_authorize=True utk tetap menerima,
# auto_subscribe=False utk tidak membalas subscribe.
# 2. Caps node netral (bukan http://slixmpp.com/ver/X.Y.Z) supaya klien
# tidak mudah dikenali sebagai slixmpp.
# 3. Disco identity `client/console` di-set di _on_session_start (setelah
# bind). CATATAN: slixmpp menyimpan identity per key `boundjid.full` —
# di __init__ boundjid masih bare-JID, lookup runtime (setelah bind)
# memakai JID ber-resource, sehingga identity di __init__ TIDAK pernah
# ditemukan dan server melihat fallback `client/bot`. Harus di session_start.
self . roster . auto_authorize = True # terima subscribe masuk (sopan)
self . roster . auto_subscribe = False # JANGAN kirim subscribe balik
# Anti-ban: register plugin dengan konfigurasi gentle
self . register_plugin ( ' xep_0030 ' ) # Service Discovery
self . register_plugin ( ' xep_0045 ' ) # MUC
self . register_plugin ( ' xep_0199 ' , { # XMPP Ping
' keepalive ' : CONNECTION_KEEPALIVE ,
' interval ' : CONNECTION_KEEPALIVE_INTERVAL ,
' timeout ' : 30 ,
} )
# Muat plugin sekarang agar bisa set caps node sebelum konek
self . init_plugins ( )
if self . plugin . get ( ' xep_0115 ' , None ) is not None :
self . plugin [ ' xep_0115 ' ] . caps_node = ' https://hendrik.local/caps '
2026-06-08 02:41:29 +07:00
self . add_event_handler ( ' session_start ' , self . _on_session_start )
self . add_event_handler ( ' message ' , self . _on_message )
self . add_event_handler ( ' groupchat_message ' , self . _on_groupchat_message )
self . add_event_handler ( ' disconnected ' , self . _on_disconnected )
self . add_event_handler ( ' connected ' , self . _on_connected )
self . add_event_handler ( ' groupchat_presence ' , self . _on_muc_presence )
2026-08-02 23:38:50 +07:00
# Anti-ban: handler untuk melihat error dari server (perlu selalu aktif)
self . add_event_handler ( ' message_error ' , self . _on_message_error )
self . add_event_handler ( ' stream_error ' , self . _on_stream_error )
# Anti-ban: kontrol manual atas subscription request (jangan auto-subscribe balik)
self . add_event_handler ( ' roster_subscription_request ' , self . _on_roster_subscription_request )
2026-06-08 02:41:29 +07:00
2026-06-10 11:11:27 +07:00
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 } "
2026-06-10 10:54:56 +07:00
def _calc_rejoin_delay ( self , room : str ) - > float :
2026-08-02 23:38:50 +07:00
""" Anti-ban: hitung delay rejoin dengan exponential backoff + jitter. """
2026-06-10 10:54:56 +07:00
attempts = self . _muc_rejoin_attempts . get ( room , 0 )
delay = MUC_REJOIN_INITIAL_DELAY * ( MUC_REJOIN_BACKOFF_MULT * * attempts )
2026-08-02 23:38:50 +07:00
delay = min ( delay , MUC_REJOIN_MAX_DELAY )
# Tambah jitter untuk menghindari pola terdeteksi
return _add_jitter ( delay , MUC_JOIN_JITTER )
2026-06-10 10:54:56 +07:00
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 ( )
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] Cancelled pending rejoin (new trigger) ' , flush = True )
2026-06-10 10:54:56 +07:00
# 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
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] Cooldown active ( { cooldown_left : .0f } s left), delaying rejoin ' , flush = True )
2026-06-10 10:54:56 +07:00
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
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] Rejoin scheduled in { delay : .0f } s (attempt # { attempts } ) ' , flush = True )
2026-06-10 10:54:56 +07:00
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 :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] Already ready, skip rejoin ' , flush = True )
2026-06-10 10:54:56 +07:00
return
2026-06-10 11:11:27 +07:00
nick = self . _get_muc_nick ( room )
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] Rejoining as { nick } ... ' , flush = True )
2026-06-10 10:54:56 +07:00
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 )
2026-06-10 11:11:27 +07:00
self . _muc_rejoin_attempts . pop ( " _nick_ " + room , None )
2026-08-02 23:38:50 +07:00
# Catat sukses untuk adaptive rate limiting
self . _rate_limiter . record_send ( success = True )
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] Rejoin successful as { nick } ' , flush = True )
2026-06-10 10:54:56 +07:00
except asyncio . CancelledError :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] Rejoin cancelled ' , flush = True )
2026-06-10 10:54:56 +07:00
except Exception as e :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] Rejoin failed: { e } ' , flush = True )
2026-08-02 23:38:50 +07:00
# Catat error untuk adaptive rate limiting
self . _rate_limiter . record_send ( success = False )
2026-06-10 11:11:27 +07:00
# 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 )
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] Nick conflict, trying alternative: { new_nick } ' , flush = True )
2026-06-10 11:11:27 +07:00
# 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
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] All nick variations exhausted, skipping room ' , flush = True )
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] Set XMPP_NICKNAME in .env to a unique nick ' , flush = True )
2026-06-10 11:11:27 +07:00
else :
# Anti-ban: error biasa (network, dll), retry with backoff
self . _schedule_muc_rejoin ( room )
2026-06-10 10:54:56 +07:00
2026-06-08 02:41:29 +07:00
async def _on_connected ( self , event ) :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] XMPP connected ' , flush = True )
2026-08-02 23:38:50 +07:00
_dbg ( f ' connected state: authenticated= { self . authenticated } , bound= { self . bound } , '
f ' sessionstarted= { self . sessionstarted } , '
f ' _session_started= { getattr ( self , " _session_started " , None ) } ' )
_dbg ( f ' connected via: { self . transport } ' )
2026-06-08 02:41:29 +07:00
async def _on_disconnected ( self , event ) :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] XMPP disconnected ' , flush = True )
2026-06-10 10:54:56 +07:00
# 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 ( )
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] Cancelled pending rejoin (disconnected) ' , flush = True )
2026-06-10 10:54:56 +07:00
self . _muc_rejoin_tasks . clear ( )
2026-08-02 23:38:50 +07:00
# Anti-ban: schedule manual reconnect dengan delay (jangan instant)
if not self . _stopped . is_set ( ) and not self . _reconnect_scheduled :
self . _reconnect_scheduled = True
self . _last_disconnect = datetime . now ( )
delay = random . uniform ( RECONNECT_DELAY_MIN , RECONNECT_DELAY_MAX )
print ( f ' [ { _ts ( ) } ] Will attempt reconnect in { delay : .0f } s... ' , flush = True )
asyncio . create_task ( self . _delayed_reconnect ( delay ) )
async def _delayed_reconnect ( self , delay : float ) :
""" Anti-ban: reconnect dengan delay, bukan instant. """
await asyncio . sleep ( delay )
if self . _stopped . is_set ( ) :
return
print ( f ' [ { _ts ( ) } ] Attempting manual reconnect... ' , flush = True )
try :
await self . connect ( )
except Exception as e :
print ( f ' [ { _ts ( ) } ] Reconnect failed: { e } ' , flush = True )
# Schedule another reconnect with longer delay
self . _reconnect_scheduled = False
self . _on_disconnected ( None )
else :
self . _reconnect_scheduled = False
2026-06-08 02:41:29 +07:00
async def _on_session_start ( self , event ) :
2026-08-02 23:38:50 +07:00
_dbg ( ' --- SESSION START --- ' )
_dbg ( f ' session state: authenticated= { self . authenticated } , bound= { self . bound } , '
f ' sessionstarted= { self . sessionstarted } , '
f ' _session_started= { getattr ( self , " _session_started " , None ) } ' )
_dbg ( f ' boundjid: full= { self . boundjid . full } , bare= { self . boundjid . bare } , '
f ' resource= { self . boundjid . resource } , host= { self . boundjid . host } ' )
print ( f ' [ { _ts ( ) } ] XMPP online as { self . boundjid . full } ' , flush = True )
# Anti-ban: set disco identity SEKARANG (setelah bind, boundjid ber-resource)
# supaya identity masuk ke caps ver (presence) DAN respon disco#info server.
# Sebelumnya di __init__ -> tersimpan di key bare-JID -> tak pernah ditemukan
# saat runtime -> server melihat identity fallback `client/bot`.
self . plugin [ ' xep_0030 ' ] . add_identity (
category = ' client ' , itype = ' console ' , name = config . AGENT_CHARACTER . title ( ) or ' Hendrik ' ,
)
try :
local_info = await self . plugin [ ' xep_0030 ' ] . get_info ( local = True )
if ' disco_info ' in local_info :
identities = local_info [ ' disco_info ' ] [ ' identities ' ]
else :
identities = local_info [ ' identities ' ]
_dbg ( f ' disco identity (local) after set: { identities } ' )
except Exception as e :
_dbg ( f ' disco identity check failed: { e } ' )
# Anti-ban: send presence + request roster (wajib sebelum kirim pesan DM)
print ( f ' [ { _ts ( ) } ] Sending initial presence... ' , flush = True )
2026-06-08 02:41:29 +07:00
self . send_presence ( )
2026-08-02 23:38:50 +07:00
print ( f ' [ { _ts ( ) } ] Requesting roster... ' , flush = True )
2026-06-08 02:41:29 +07:00
self . get_roster ( )
2026-08-02 23:38:50 +07:00
# Anti-ban: delay sebelum join MUC pertama agar startup tidak terlihat bot
if self . _muc_rooms :
pre_delay = random . uniform ( 5.0 , 15.0 )
print ( f ' [ { _ts ( ) } ] MUC pre-join delay { pre_delay : .1f } s (anti-ban)... ' , flush = True )
await asyncio . sleep ( pre_delay )
# Anti-ban: delay sebelum join MUC untuk menghindari koneksi yang terlalu agresif
for i , room in enumerate ( self . _muc_rooms ) :
# Delay antar room join (3-8 detik per room)
if i > 0 :
join_delay = random . uniform ( 3.0 , 8.0 )
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] Waiting { join_delay : .1f } s before join... ' , flush = True )
await asyncio . sleep ( join_delay )
2026-06-10 11:11:27 +07:00
# Anti-ban: retry join dengan incremental delay & nick fallback
2026-06-10 10:54:56 +07:00
success = False
for attempt in range ( 1 , 4 ) :
2026-06-10 11:11:27 +07:00
nick = self . _get_muc_nick ( room )
2026-06-10 10:54:56 +07:00
try :
2026-06-10 11:11:27 +07:00
await self . plugin [ ' xep_0045 ' ] . join_muc_wait ( room , nick , maxstanzas = 0 )
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] Joined MUC room: { room } as { nick } ' , flush = True )
2026-06-10 10:54:56 +07:00
self . _muc_last_join [ room ] = datetime . now ( )
self . _muc_rejoin_attempts . pop ( room , None )
2026-06-10 11:11:27 +07:00
self . _muc_rejoin_attempts . pop ( " _nick_ " + room , None )
2026-06-10 10:54:56 +07:00
success = True
break
except Exception as e :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC join attempt # { attempt } failed ( { room } ): { e } ' , flush = True )
2026-06-10 11:11:27 +07:00
# 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
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] Nick conflict, switching to: { self . _get_muc_nick ( room ) } ' , flush = True )
2026-06-10 11:11:27 +07:00
# Retry segera dengan nick baru (jangan wait)
continue
else :
# Anti-ban: semua nick alternatif habis
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] All nick variations exhausted ' , flush = True )
2026-06-10 11:11:27 +07:00
break
elif attempt < 3 :
2026-08-02 23:38:50 +07:00
# Anti-ban: error biasa, wait before retry (5s, 10s, 15s)
retry_delay = 5.0 * attempt
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] Retrying in { retry_delay : .0f } s... ' , flush = True )
2026-06-10 10:54:56 +07:00
await asyncio . sleep ( retry_delay )
if not success :
# Anti-ban: semua attempt gagal, schedule background rejoin
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] All join attempts failed, scheduling background rejoin ' , flush = True )
2026-06-10 10:54:56 +07:00
self . _schedule_muc_rejoin ( room )
2026-06-08 02:41:29 +07:00
def _on_message ( self , msg ) :
if msg [ ' type ' ] not in ( ' chat ' , ' normal ' ) :
return
jid = msg [ ' from ' ] . bare
body = msg [ ' body ' ] . strip ( )
if not body :
return
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] DM from { jid } : { body [ : 60 ] } ' , flush = True )
2026-08-02 23:38:50 +07:00
# Anti-ban: proses langsung di event loop, bukan thread baru
# Ini memastikan msg.send() dipanggil dari thread yang benar
asyncio . create_task ( self . _process_dm_async ( jid , body ) )
def _on_message_error ( self , msg ) :
""" Anti-ban: handle error message dari server. """
print ( f ' [ { _ts ( ) } ] MESSAGE ERROR from server: ' , flush = True )
print ( f ' [ { _ts ( ) } ] Type: { msg . get ( " type " , " unknown " ) } ' , flush = True )
print ( f ' [ { _ts ( ) } ] From: { msg . get ( " from " , " unknown " ) } ' , flush = True )
print ( f ' [ { _ts ( ) } ] To: { msg . get ( " to " , " unknown " ) } ' , flush = True )
print ( f ' [ { _ts ( ) } ] Error: { msg . get ( " error " , " unknown " ) } ' , flush = True )
print ( f ' [ { _ts ( ) } ] Full stanza: { msg } ' , flush = True )
# Catat error untuk adaptive rate limiting
self . _rate_limiter . record_send ( success = False )
def _on_stream_error ( self , error ) :
""" Anti-ban: handle stream error dari server. """
print ( f ' [ { _ts ( ) } ] STREAM ERROR from server: ' , flush = True )
print ( f ' [ { _ts ( ) } ] Condition: { error . get ( " condition " , " unknown " ) } ' , flush = True )
print ( f ' [ { _ts ( ) } ] Text: { error . get ( " text " , " unknown " ) } ' , flush = True )
print ( f ' [ { _ts ( ) } ] Full error: { error } ' , flush = True )
async def _on_roster_subscription_request ( self , presence ) :
print ( f ' [ { _ts ( ) } ] Subscription request from { presence [ " from " ] } ' , flush = True )
# Anti-ban: TIDAK merespons subscribe otomatis (auto_subscribe=False).
# Balasan 'subscribe' balik adalah red flag (contact farming) di conversations.im.
_dbg ( f ' NOT auto-responding subscribe from { presence [ " from " ] } '
f ' (auto_subscribe= { self . roster . auto_subscribe } , '
f ' auto_authorize= { self . roster . auto_authorize } ) ' )
2026-06-08 02:41:29 +07:00
def _on_groupchat_message ( self , msg ) :
if msg [ ' type ' ] != ' groupchat ' :
return
2026-06-10 11:11:27 +07:00
room = msg [ ' from ' ] . bare
2026-06-08 02:41:29 +07:00
nick = msg [ ' from ' ] . resource
2026-06-10 11:11:27 +07:00
if self . _is_my_nick ( room , nick ) :
2026-06-08 02:41:29 +07:00
return
room = msg [ ' from ' ] . bare
if room not in self . _muc_ready :
return
body = msg [ ' body ' ] . strip ( )
if not body :
return
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] < { nick } >: { body [ : 60 ] } ' , flush = True )
2026-08-02 23:38:50 +07:00
# Anti-ban: proses langsung di event loop, bukan thread baru
asyncio . create_task ( self . _process_muc_async ( room , nick , body ) )
2026-06-08 02:41:29 +07:00
2026-06-10 11:11:27 +07:00
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
2026-06-08 02:41:29 +07:00
def _on_muc_presence ( self , presence ) :
room = presence [ ' from ' ] . bare
nick = presence [ ' from ' ] . resource
ptype = presence [ ' type ' ]
2026-06-10 11:11:27 +07:00
if self . _is_my_nick ( room , nick ) and ptype not in ( ' unavailable ' , ' error ' ) :
2026-06-08 02:41:29 +07:00
self . _muc_ready . add ( room )
2026-06-10 10:54:56 +07:00
# Reset rejoin counter on successful join (anti-ban: avoid accumulating backoff)
self . _muc_rejoin_attempts . pop ( room , None )
2026-06-10 11:11:27 +07:00
self . _muc_rejoin_attempts . pop ( " _nick_ " + room , None )
2026-06-08 02:41:29 +07:00
if ptype == ' unavailable ' :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] < { nick } > left ' , flush = True )
2026-06-10 10:54:56 +07:00
# 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
2026-06-10 11:11:27 +07:00
if self . _is_my_nick ( room , nick ) :
2026-06-10 10:54:56 +07:00
self . _schedule_muc_rejoin ( room )
2026-06-08 02:41:29 +07:00
elif ptype == ' error ' :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] error: { presence } ' , flush = True )
2026-06-10 11:11:27 +07:00
# Anti-ban: also rejoin on error (e.g. temporary failure)
if self . _is_my_nick ( room , nick ) :
2026-06-10 10:54:56 +07:00
self . _muc_ready . discard ( room )
self . _schedule_muc_rejoin ( room )
2026-06-08 02:41:29 +07:00
else :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] MUC [ { room } ] < { nick } > joined (type= { ptype } ) ' , flush = True )
2026-06-08 02:41:29 +07:00
2026-08-02 23:38:50 +07:00
async def _process_dm_async ( self , jid , body ) :
""" Anti-ban: versi async dari _process_dm, dipanggil dari event loop. """
2026-06-08 02:41:29 +07:00
session = self . _session_mgr . get_or_create (
2026-06-12 14:10:06 +07:00
jid , self . _build_system_prompt (
tools_definition = self . _tools_def ,
character = config . AGENT_CHARACTER or None ,
skills = config . AGENT_SKILLS . split ( " , " ) if config . AGENT_SKILLS else None ,
)
2026-06-08 02:41:29 +07:00
)
session . cancel_timer ( )
2026-08-02 23:38:50 +07:00
# Anti-ban: JANGAN kirim presence subscription otomatis
# Ini adalah red flag untuk server (contact farming behavior)
# Hanya kirim jika user explicitly request
# self.send_presence_subscription(pto=jid, ptype='subscribed')
2026-06-08 02:41:29 +07:00
if body == ' :new ' :
self . _session_mgr . reset ( jid )
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] Session reset for { jid } ' , flush = True )
2026-06-08 02:41:29 +07:00
self . _schedule_send ( jid , ' Memulai sesi baru. Ada yang bisa di bantu? ' )
return
2026-07-15 09:47:48 +07:00
if ' roleplayer ' in self . _skill :
2026-07-07 17:17:50 +07:00
try :
2026-07-14 05:43:42 +07:00
results = ragroleplay . user_load ( config . ragroleplay_db_path , unique_id = jid , character = personality . PERSONALITY . name )
2026-07-07 17:17:50 +07:00
if results :
u = results [ 0 ]
context = (
f ' [User Context] \n '
2026-07-12 13:39:39 +07:00
f ' ID: { u [ " id " ] } \n '
2026-07-07 17:17:50 +07:00
f ' Nama: { u [ " fullname " ] } ( { u [ " nickname " ] } ) \n '
2026-07-13 17:07:55 +07:00
f ' Family: { u . get ( " familyname " , " - " ) or " - " } \n '
f ' Character: { u . get ( " character " , " - " ) or " - " } \n '
2026-07-07 17:17:50 +07:00
f ' Alias: { u . get ( " alias " , " - " ) or " - " } \n '
2026-07-12 13:38:02 +07:00
f ' Salutation: { u . get ( " salutation " , " - " ) or " - " } \n '
2026-07-07 17:17:50 +07:00
f ' Persona: { u . get ( " persona " , " - " ) or " - " } \n '
f ' Telegram: { u . get ( " telegram_id " , " - " ) or " - " } / @ { u . get ( " telegram_username " , " - " ) or " - " } \n '
2026-08-01 14:29:32 +07:00
f ' XMPP: { u . get ( " xmpp_username " , " - " ) or " - " } \n '
2026-07-21 21:12:36 +07:00
f ' [/User Context] \n '
f ' [PENTING: Kamu SUDAH mengenal user ini. WAJIB panggil memories_latest(character= " { personality . PERSONALITY . name } " , user_id= " { u [ " id " ] } " , limit=10) untuk mengambil riwayat percakapan terbaru. GUNAKAN data kondisi emotional/physical dari memori terbaru untuk melanjutkan state character secara natural sebelum merespon.] '
2026-07-07 17:17:50 +07:00
)
session . add_message ( ' system ' , context )
else :
session . add_message ( ' system ' ,
f ' [User Context: Pengguna baru — belum ada di database] \n '
2026-07-14 06:21:54 +07:00
f ' [PENTING: Kamu BELUM mengenal user ini. WAJIB tanya nama sebagai pembuka. '
f ' Setelah nama diketahui, simpan via users_store. '
f ' Lanjutkan percakapan natural dan proaktif melengkapi data user lainnya di pesan berikutnya.] \n '
2026-07-07 17:17:50 +07:00
f ' [Platform: XMPP ( { jid } )] ' )
except Exception :
pass
2026-06-08 02:41:29 +07:00
session . add_message ( ' user ' , body )
2026-07-15 09:47:48 +07:00
is_roleplay = ' roleplayer ' in self . _skill
2026-06-14 10:56:55 +07:00
if not is_roleplay :
2026-06-10 15:12:50 +07:00
self . _schedule_send ( jid , f ' > { body } \n Thinking... ' )
2026-06-10 10:54:56 +07:00
# Delay 1: simulasi membaca pesan user
2026-08-02 23:38:50 +07:00
await _read_delay ( )
2026-06-10 10:54:56 +07:00
2026-06-23 11:23:04 +07:00
my_name = personality . PERSONALITY . name
2026-06-16 22:49:00 +07:00
quote = body
2026-07-18 02:14:07 +07:00
def on_tool_calls ( content ) :
if content and content . strip ( ) :
self . _schedule_send ( jid , content , ' chat ' )
2026-06-16 22:49:00 +07:00
2026-07-16 10:46:35 +07:00
tool_reminder = None
2026-07-03 10:45:00 +07:00
2026-07-13 16:30:24 +07:00
final_content , should_close = run_agent_loop (
2026-06-16 22:49:00 +07:00
session , self . _llm , self . _TOOLS , self . _TOOL_HANDLERS ,
2026-07-03 10:45:00 +07:00
self . _max_iterations , on_tool_calls = on_tool_calls , tool_reminder = tool_reminder
2026-06-16 22:49:00 +07:00
)
if final_content is not None :
if is_roleplay :
if config . XMPP_SELECTIVE_RESPONSE :
recent_msgs = [ ]
for msg in session . messages [ - 6 : ] :
if msg . get ( ' role ' ) == ' user ' :
recent_msgs . append ( f " User: { msg . get ( ' content ' , ' ' ) } " )
elif msg . get ( ' role ' ) == ' assistant ' and msg . get ( ' content ' ) :
recent_msgs . append ( f " { my_name } : { msg . get ( ' content ' , ' ' ) } " )
recent_history = " \n " . join ( recent_msgs )
if should_respond (
message = quote ,
sender_nickname = jid ,
recent_history = recent_history ,
my_name = my_name ,
) :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] need_response=True → sending response ' , flush = True )
2026-06-16 22:49:00 +07:00
self . _schedule_send ( jid , final_content , ' chat ' )
else :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] need_response=False → staying silent ' , flush = True )
2026-06-16 22:49:00 +07:00
else :
from tools . roleplayer import _name_mentioned
if _name_mentioned ( my_name , quote ) :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] Name mentioned → sending response ' , flush = True )
2026-06-16 22:49:00 +07:00
self . _schedule_send ( jid , final_content , ' chat ' )
else :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] Name not mentioned → staying silent ' , flush = True )
2026-06-16 22:49:00 +07:00
else :
self . _schedule_send ( jid , f ' > { quote } \n { final_content } ' , ' chat ' )
else :
msg = ' Max iterations reached without final answer. '
if is_roleplay :
self . _schedule_send ( jid , msg , ' chat ' )
else :
self . _schedule_send ( jid , f ' > { quote } \n { msg } ' , ' chat ' )
2026-06-08 02:41:29 +07:00
2026-07-13 16:30:24 +07:00
# Natural close: DM only, roleplayer only
2026-07-15 09:47:48 +07:00
if should_close and ' roleplayer ' in self . _skill :
2026-07-13 16:30:24 +07:00
print ( f ' [ { _ts ( ) } ] Natural close triggered for { jid } ' , flush = True )
self . _session_mgr . reset ( jid )
else :
# DM: timeout 24 jam (efektif tidak auto-close), MUC tetap 5 menit
session . start_timer ( 86400 , self . _timeout_session , jid , ' chat ' )
2026-06-08 02:41:29 +07:00
2026-08-02 23:38:50 +07:00
async def _process_muc_async ( self , room , nick , body ) :
""" Anti-ban: versi async dari _process_muc, dipanggil dari event loop. """
2026-06-08 02:41:29 +07:00
session = self . _session_mgr . get_or_create (
2026-06-12 14:10:06 +07:00
room , self . _build_system_prompt (
tools_definition = self . _tools_def ,
character = config . AGENT_CHARACTER or None ,
skills = config . AGENT_SKILLS . split ( " , " ) if config . AGENT_SKILLS else None ,
)
2026-06-08 02:41:29 +07:00
)
session . cancel_timer ( )
if body == ' :new ' :
self . _session_mgr . reset ( room )
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] Session reset for MUC room { room } ' , flush = True )
2026-06-08 02:41:29 +07:00
self . _schedule_send ( room , ' Memulai sesi baru. Ada yang bisa di bantu? ' , mtype = ' groupchat ' )
return
prefixed = f ' [ { nick } ] { body } '
session . add_message ( ' user ' , prefixed )
2026-06-14 10:56:55 +07:00
if self . _skill != ' roleplayer ' :
2026-06-10 15:12:50 +07:00
self . _schedule_send ( room , f ' > [ { nick } ] { body } \n Thinking... ' , mtype = ' groupchat ' )
2026-06-10 10:54:56 +07:00
# Delay 1: simulasi membaca pesan user
2026-08-02 23:38:50 +07:00
await _read_delay ( )
2026-06-10 10:54:56 +07:00
2026-06-23 11:23:04 +07:00
my_name = personality . PERSONALITY . name
2026-06-16 22:49:00 +07:00
quote = f ' [ { nick } ] { body } '
2026-06-10 15:12:50 +07:00
2026-07-15 09:47:48 +07:00
_is_roleplay = ' roleplayer ' in self . _skill
2026-07-03 10:45:00 +07:00
2026-07-18 02:14:07 +07:00
def on_tool_calls ( content ) :
if content and content . strip ( ) :
self . _schedule_send ( room , content , ' groupchat ' )
2026-06-08 02:41:29 +07:00
2026-07-16 10:46:35 +07:00
tool_reminder = None
2026-07-03 10:45:00 +07:00
2026-07-13 16:30:24 +07:00
final_content , _should_close = run_agent_loop (
2026-06-16 22:49:00 +07:00
session , self . _llm , self . _TOOLS , self . _TOOL_HANDLERS ,
2026-07-03 10:45:00 +07:00
self . _max_iterations , on_tool_calls = on_tool_calls , tool_reminder = tool_reminder
2026-06-16 22:49:00 +07:00
)
2026-06-10 15:12:50 +07:00
2026-06-16 22:49:00 +07:00
if final_content is not None :
2026-07-03 10:45:00 +07:00
if _is_roleplay :
2026-06-16 22:49:00 +07:00
if config . XMPP_SELECTIVE_RESPONSE :
recent_msgs = [ ]
for msg in session . messages [ - 6 : ] :
if msg . get ( ' role ' ) == ' user ' :
recent_msgs . append ( f " User: { msg . get ( ' content ' , ' ' ) } " )
elif msg . get ( ' role ' ) == ' assistant ' and msg . get ( ' content ' ) :
recent_msgs . append ( f " { my_name } : { msg . get ( ' content ' , ' ' ) } " )
recent_history = " \n " . join ( recent_msgs )
if should_respond (
message = quote ,
sender_nickname = nick ,
recent_history = recent_history ,
my_name = my_name ,
) :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] need_response=True → sending response ' , flush = True )
2026-06-16 22:49:00 +07:00
self . _schedule_send ( room , final_content , ' groupchat ' )
else :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] need_response=False → staying silent ' , flush = True )
2026-06-16 22:49:00 +07:00
else :
from tools . roleplayer import _name_mentioned
if _name_mentioned ( my_name , quote ) :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] Name mentioned → sending response ' , flush = True )
2026-06-16 22:49:00 +07:00
self . _schedule_send ( room , final_content , ' groupchat ' )
else :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] Name not mentioned → staying silent ' , flush = True )
2026-06-16 22:49:00 +07:00
else :
self . _schedule_send ( room , f ' > { quote } \n { final_content } ' , ' groupchat ' )
2026-06-10 15:12:50 +07:00
else :
2026-06-16 22:49:00 +07:00
msg = ' Max iterations reached without final answer. '
2026-07-03 10:45:00 +07:00
if _is_roleplay :
2026-06-16 22:49:00 +07:00
self . _schedule_send ( room , msg , ' groupchat ' )
else :
self . _schedule_send ( room , f ' > { quote } \n { msg } ' , ' groupchat ' )
session . start_timer ( 300 , self . _timeout_session , room , ' groupchat ' )
2026-06-08 02:41:29 +07:00
def _execute_tool ( self , tool_call ) :
2026-06-23 11:23:04 +07:00
from lib . agent_loop import execute_tool
2026-06-16 22:49:00 +07:00
return execute_tool ( tool_call , self . _TOOL_HANDLERS )
2026-06-08 02:41:29 +07:00
def _schedule_send ( self , to , body , mtype = ' chat ' ) :
2026-08-02 23:38:50 +07:00
""" Anti-ban: enqueue pesan ke queue, bukan langsung kirim. """
if self . _msg_queue and self . _loop and not self . _loop . is_closed ( ) :
try :
self . _msg_queue . put_nowait ( ( to , body , mtype ) )
print ( f ' [ { _ts ( ) } ] Queued message to { to } ( { len ( body ) } chars) ' , flush = True )
except asyncio . QueueFull :
print ( f ' [ { _ts ( ) } ] WARNING: Message queue full, dropping message to { to } ' , flush = True )
2026-06-08 02:41:29 +07:00
else :
2026-08-02 23:38:50 +07:00
print ( f ' [ { _ts ( ) } ] WARNING: cannot queue message to { to } — queue unavailable ' , flush = True )
async def _msg_worker ( self ) :
""" Anti-ban: worker coroutine yang mengirim pesan dari queue dengan rate limiting. """
print ( f ' [ { _ts ( ) } ] Message queue worker started ' , flush = True )
self . _msg_worker_running = True
while self . _msg_worker_running :
try :
# Ambil pesan dari queue dengan timeout
try :
to , body , mtype = await asyncio . wait_for (
self . _msg_queue . get ( ) , timeout = 1.0
)
except asyncio . TimeoutError :
continue
# Adaptive: apply backoff multiplier jika throttle detected
base_delay = random . uniform ( MSG_QUEUE_DELAY_MIN , MSG_QUEUE_DELAY_MAX )
multiplier = self . _rate_limiter . get_delay_multiplier ( )
delay = _add_jitter ( base_delay ) * multiplier
if multiplier > 1.0 :
print ( f ' [ { _ts ( ) } ] ADAPTIVE: Throttled, delay { delay : .1f } s (x { multiplier } ) to { to } ' , flush = True )
else :
print ( f ' [ { _ts ( ) } ] Pre-send delay: { delay : .1f } s to { to } ' , flush = True )
await asyncio . sleep ( delay )
# Kirim pesan dan catat hasilnya
send_start = asyncio . get_event_loop ( ) . time ( )
send_success = False
try :
# Anti-ban: gunakan full JID untuk mfrom (server strict tentang ini)
mfrom = self . boundjid . full if self . boundjid else None
msg = self . make_message ( mto = to , mbody = body , mtype = mtype , mfrom = mfrom )
# Debug: snapshot state koneksi + stanza XML lengkap sebelum kirim.
_dbg ( f ' SEND STATE: to= { to } , mtype= { mtype } , '
f ' _session_started= { getattr ( self , " _session_started " , None ) } , '
f ' authenticated= { self . authenticated } , bound= { self . bound } ' )
_dbg ( f ' Stanza to send (full): { msg } ' )
msg . send ( )
send_success = True
print ( f ' [ { _ts ( ) } ] Sent to { to } ( { len ( body ) } chars) ' , flush = True )
except Exception as e :
print ( f ' [ { _ts ( ) } ] SEND ERROR to { to } : { e } ' , flush = True )
print ( traceback . format_exc ( ) , flush = True )
# Catat error untuk adaptive rate limiting
self . _rate_limiter . record_send ( success = False )
if send_success :
# Catat sukses (tanpa response time karena XMPP async)
self . _rate_limiter . record_send ( success = True )
# Mark task done
self . _msg_queue . task_done ( )
# Small delay antara pesan untuk menghindari burst
# Adaptive: lebih lama jika throttled
inter_delay = random . uniform ( 0.5 , 1.5 ) * multiplier
await asyncio . sleep ( inter_delay )
except asyncio . CancelledError :
print ( f ' [ { _ts ( ) } ] Message queue worker cancelled ' , flush = True )
break
except Exception as e :
print ( f ' [ { _ts ( ) } ] Message queue worker error: { e } ' , flush = True )
await asyncio . sleep ( 1 )
self . _msg_worker_running = False
print ( f ' [ { _ts ( ) } ] Message queue worker stopped ' , flush = True )
2026-06-08 02:41:29 +07:00
async def _send_coro ( self , to , body , mtype ) :
try :
2026-06-10 10:54:56 +07:00
# Delay 2: simulasi mengetik (proporsional dengan panjang pesan)
delay = _typing_delay ( body )
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] Typing delay: { delay : .1f } s ( { len ( body ) } chars) ' , flush = True )
2026-06-10 10:54:56 +07:00
await asyncio . sleep ( delay )
2026-08-02 23:38:50 +07:00
# Anti-ban: gunakan full JID untuk mfrom (server strict tentang ini)
mfrom = self . boundjid . full if self . boundjid else None
msg = self . make_message ( mto = to , mbody = body , mtype = mtype , mfrom = mfrom )
2026-06-08 02:41:29 +07:00
msg . send ( )
except Exception as e :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] SEND ERROR: { e } ' , flush = True )
2026-06-08 02:41:29 +07:00
def _timeout_session ( self , session_id , mtype ) :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] Session timeout: { session_id } ' , flush = True )
2026-06-08 02:41:29 +07:00
self . _schedule_send ( session_id , ' Sesi ditutup. Sampai jumpa ' , mtype )
self . _session_mgr . reset ( session_id )
def start ( self ) :
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] Starting XMPP service... ' , flush = True )
2026-08-02 23:38:50 +07:00
_setup_debug_logging ( )
2026-06-08 02:41:29 +07:00
asyncio . run ( self . _run ( ) )
async def _run ( self ) :
self . _stopped = asyncio . Event ( )
self . _loop = asyncio . get_running_loop ( )
2026-06-10 21:13:15 +07:00
2026-08-02 23:38:50 +07:00
# Anti-ban: inisialisasi message queue dan worker
self . _msg_queue = asyncio . Queue ( maxsize = 100 )
self . _msg_worker_task = asyncio . create_task ( self . _msg_worker ( ) )
2026-06-10 21:13:15 +07:00
# Hanya tangani SIGTERM untuk shutdown.
# SENGATKAN SIGHUP: nohup kirim SIGHUP saat terminal close,
# dan kita tidak mau proses mati karena itu.
try :
self . _loop . add_signal_handler ( signal . SIGTERM , self . _stopped . set )
2026-06-16 22:49:00 +07:00
except ( NotImplementedError , RuntimeError ) :
2026-06-10 21:13:15 +07:00
pass
2026-08-02 23:38:50 +07:00
print ( f ' [ { _ts ( ) } ] Connecting to server (jid= { self . jid } )... ' , flush = True )
try :
await self . connect ( )
except Exception as e :
print ( f ' [ { _ts ( ) } ] CONNECT ERROR: { e } ' , flush = True )
print ( traceback . format_exc ( ) , flush = True )
raise
print ( f ' [ { _ts ( ) } ] Connected, waiting for stream events... ' , flush = True )
2026-06-08 02:41:29 +07:00
try :
await self . _stopped . wait ( )
except ( asyncio . CancelledError , KeyboardInterrupt ) :
pass
2026-06-18 08:12:59 +07:00
print ( f ' [ { _ts ( ) } ] Shutting down... ' , flush = True )
2026-06-08 02:41:29 +07:00
await self . disconnect ( )
def stop ( self ) :
if self . _loop and not self . _loop . is_closed ( ) :
asyncio . run_coroutine_threadsafe ( self . _async_stop ( ) , self . _loop )
async def _async_stop ( self ) :
2026-08-02 23:38:50 +07:00
# Anti-ban: stop message queue worker dulu
if self . _msg_worker_task and not self . _msg_worker_task . done ( ) :
self . _msg_worker_running = False
self . _msg_worker_task . cancel ( )
try :
await self . _msg_worker_task
except asyncio . CancelledError :
pass
# Cancel semua pending rejoin tasks
for room , task in list ( self . _muc_rejoin_tasks . items ( ) ) :
if not task . done ( ) :
task . cancel ( )
self . _muc_rejoin_tasks . clear ( )
2026-06-08 02:41:29 +07:00
self . _stopped . set ( )