Unbuffered output
This commit is contained in:
parent
19b128618d
commit
dd4c6c674d
@ -150,7 +150,7 @@ if _persona_yaml_path and _persona_yaml_path.is_file():
|
|||||||
if not isinstance(_character_env, dict):
|
if not isinstance(_character_env, dict):
|
||||||
_character_env = {}
|
_character_env = {}
|
||||||
except Exception as _e:
|
except Exception as _e:
|
||||||
print(f"[config] Warning: gagal load persona.yaml untuk '{AGENT_CHARACTER}': {_e}")
|
print(f"[config] Warning: gagal load persona.yaml untuk '{AGENT_CHARACTER}': {_e}", flush=True)
|
||||||
_character_env = {}
|
_character_env = {}
|
||||||
elif ENV_CHARACTER_CONFIG_PATH and ENV_CHARACTER_CONFIG_PATH.is_file():
|
elif ENV_CHARACTER_CONFIG_PATH and ENV_CHARACTER_CONFIG_PATH.is_file():
|
||||||
# Fallback: baca character.md (format lama)
|
# Fallback: baca character.md (format lama)
|
||||||
|
|||||||
@ -50,7 +50,7 @@ def main():
|
|||||||
if workspace:
|
if workspace:
|
||||||
resolved = os.path.abspath(workspace)
|
resolved = os.path.abspath(workspace)
|
||||||
if not os.path.isdir(resolved):
|
if not os.path.isdir(resolved):
|
||||||
print(f"Error: '{resolved}' is not a valid directory")
|
print(f"Error: '{resolved}' is not a valid directory", flush=True)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
os.chdir(resolved)
|
os.chdir(resolved)
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ def main():
|
|||||||
|
|
||||||
def _handle_sig(signum, frame):
|
def _handle_sig(signum, frame):
|
||||||
nonlocal _shutdown
|
nonlocal _shutdown
|
||||||
print("\nShutting down...")
|
print("\nShutting down...", flush=True)
|
||||||
for svc in services:
|
for svc in services:
|
||||||
svc.stop()
|
svc.stop()
|
||||||
_shutdown = True
|
_shutdown = True
|
||||||
@ -116,7 +116,7 @@ def main():
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
_shutdown = True
|
_shutdown = True
|
||||||
print("Exiting.")
|
print("Exiting.", flush=True)
|
||||||
else:
|
else:
|
||||||
from tui import HendrikTUI
|
from tui import HendrikTUI
|
||||||
HendrikTUI(
|
HendrikTUI(
|
||||||
|
|||||||
@ -293,7 +293,7 @@ def build_system_prompt(
|
|||||||
else:
|
else:
|
||||||
selected_skill = ""
|
selected_skill = ""
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[persona] Warning: gagal load persona.yaml untuk '{character_name}': {e}")
|
print(f"[persona] Warning: gagal load persona.yaml untuk '{character_name}': {e}", flush=True)
|
||||||
|
|
||||||
# Resolve skills list
|
# Resolve skills list
|
||||||
# Priority: explicit skills param > persona.yaml skill > AGENT_SKILL env > AGENT_SKILLS env > selected_skill
|
# Priority: explicit skills param > persona.yaml skill > AGENT_SKILL env > AGENT_SKILLS env > selected_skill
|
||||||
|
|||||||
@ -29,7 +29,7 @@ def execute_tool(tool_call, TOOL_HANDLERS):
|
|||||||
|
|
||||||
def run_agent_loop(session, llm_client, TOOLS, TOOL_HANDLERS, max_iterations, on_tool_calls=None):
|
def run_agent_loop(session, llm_client, TOOLS, TOOL_HANDLERS, max_iterations, on_tool_calls=None):
|
||||||
for step in range(max_iterations):
|
for step in range(max_iterations):
|
||||||
print(f'[{_ts()}] Step {step + 1} — calling LLM...')
|
print(f'[{_ts()}] Step {step + 1} — calling LLM...', flush=True)
|
||||||
response = llm_client.chat(session.messages, tools=TOOLS)
|
response = llm_client.chat(session.messages, tools=TOOLS)
|
||||||
|
|
||||||
if response.tool_calls:
|
if response.tool_calls:
|
||||||
@ -41,7 +41,7 @@ def run_agent_loop(session, llm_client, TOOLS, TOOL_HANDLERS, max_iterations, on
|
|||||||
session.messages.append(amsg)
|
session.messages.append(amsg)
|
||||||
|
|
||||||
tnames = [tc['function']['name'] for tc in response.tool_calls]
|
tnames = [tc['function']['name'] for tc in response.tool_calls]
|
||||||
print(f'[{_ts()}] Using tools: {", ".join(tnames)}')
|
print(f'[{_ts()}] Using tools: {", ".join(tnames)}', flush=True)
|
||||||
|
|
||||||
if on_tool_calls:
|
if on_tool_calls:
|
||||||
on_tool_calls(tnames)
|
on_tool_calls(tnames)
|
||||||
@ -55,12 +55,12 @@ def run_agent_loop(session, llm_client, TOOLS, TOOL_HANDLERS, max_iterations, on
|
|||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
if response.content:
|
if response.content:
|
||||||
print(f'[{_ts()}] Response generated ({len(response.content)} chars)')
|
print(f'[{_ts()}] Response generated ({len(response.content)} chars)', flush=True)
|
||||||
session.messages.append({'role': 'assistant', 'content': response.content})
|
session.messages.append({'role': 'assistant', 'content': response.content})
|
||||||
return response.content
|
return response.content
|
||||||
return None
|
return None
|
||||||
|
|
||||||
print(f'[{_ts()}] Max iterations ({max_iterations}) reached')
|
print(f'[{_ts()}] Max iterations ({max_iterations}) reached', flush=True)
|
||||||
session.messages.append({
|
session.messages.append({
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'content': 'Max iterations reached without final answer.',
|
'content': 'Max iterations reached without final answer.',
|
||||||
|
|||||||
@ -46,7 +46,7 @@ class TelegramClient:
|
|||||||
self._app = Application.builder().token(token).build()
|
self._app = Application.builder().token(token).build()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
print(f'[{_ts()}] Starting Telegram service...')
|
print(f'[{_ts()}] Starting Telegram service...', flush=True)
|
||||||
asyncio.run(self._async_run())
|
asyncio.run(self._async_run())
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@ -62,14 +62,14 @@ class TelegramClient:
|
|||||||
|
|
||||||
bot_user = await self._app.bot.get_me()
|
bot_user = await self._app.bot.get_me()
|
||||||
self._bot_username = bot_user.username or ""
|
self._bot_username = bot_user.username or ""
|
||||||
print(f'[{_ts()}] Telegram bot: @{self._bot_username}')
|
print(f'[{_ts()}] Telegram bot: @{self._bot_username}', flush=True)
|
||||||
|
|
||||||
self._register_handlers()
|
self._register_handlers()
|
||||||
|
|
||||||
await self._app.initialize()
|
await self._app.initialize()
|
||||||
await self._app.start()
|
await self._app.start()
|
||||||
await self._app.updater.start_polling()
|
await self._app.updater.start_polling()
|
||||||
print(f'[{_ts()}] Telegram bot is polling')
|
print(f'[{_ts()}] Telegram bot is polling', flush=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self._stopped.wait()
|
await self._stopped.wait()
|
||||||
@ -79,7 +79,7 @@ class TelegramClient:
|
|||||||
await self._app.updater.stop()
|
await self._app.updater.stop()
|
||||||
await self._app.stop()
|
await self._app.stop()
|
||||||
await self._app.shutdown()
|
await self._app.shutdown()
|
||||||
print(f'[{_ts()}] Telegram service stopped')
|
print(f'[{_ts()}] Telegram service stopped', flush=True)
|
||||||
|
|
||||||
def _register_handlers(self):
|
def _register_handlers(self):
|
||||||
from telegram.ext import MessageHandler, filters, CommandHandler
|
from telegram.ext import MessageHandler, filters, CommandHandler
|
||||||
@ -115,7 +115,7 @@ class TelegramClient:
|
|||||||
if not text:
|
if not text:
|
||||||
return
|
return
|
||||||
msg_id = update.message.message_id
|
msg_id = update.message.message_id
|
||||||
print(f'[{_ts()}] Telegram DM from {chat_id}: {text[:60]}')
|
print(f'[{_ts()}] Telegram DM from {chat_id}: {text[:60]}', flush=True)
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=self._process_message,
|
target=self._process_message,
|
||||||
args=(chat_id, text, 'private', '', msg_id),
|
args=(chat_id, text, 'private', '', msg_id),
|
||||||
@ -156,7 +156,7 @@ class TelegramClient:
|
|||||||
name_mentioned = _name_mentioned(PERSONALITY.name, text)
|
name_mentioned = _name_mentioned(PERSONALITY.name, text)
|
||||||
|
|
||||||
if not replied_to_bot and not has_mention and not name_mentioned:
|
if not replied_to_bot and not has_mention and not name_mentioned:
|
||||||
print(f'[{_ts()}] Telegram Group [{chat_id}] NO-REPLY: {text[:60]}')
|
print(f'[{_ts()}] Telegram Group [{chat_id}] NO-REPLY: {text[:60]}', flush=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
if has_mention and self._bot_username:
|
if has_mention and self._bot_username:
|
||||||
@ -167,7 +167,7 @@ class TelegramClient:
|
|||||||
msg_id = update.message.message_id
|
msg_id = update.message.message_id
|
||||||
sender = update.effective_user
|
sender = update.effective_user
|
||||||
sender_name = sender.full_name or sender.username or str(sender.id) if sender else str(chat_id)
|
sender_name = sender.full_name or sender.username or str(sender.id) if sender else str(chat_id)
|
||||||
print(f'[{_ts()}] Telegram Group [{chat_id}] <{sender_name}>: {text[:60]}')
|
print(f'[{_ts()}] Telegram Group [{chat_id}] <{sender_name}>: {text[:60]}', flush=True)
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=self._process_message,
|
target=self._process_message,
|
||||||
args=(chat_id, text, 'group', sender_name, msg_id),
|
args=(chat_id, text, 'group', sender_name, msg_id),
|
||||||
@ -186,7 +186,7 @@ class TelegramClient:
|
|||||||
|
|
||||||
if body in (':new', '/new'):
|
if body in (':new', '/new'):
|
||||||
self._session_mgr.reset(str(chat_id))
|
self._session_mgr.reset(str(chat_id))
|
||||||
print(f'[{_ts()}] Session reset for {chat_id}')
|
print(f'[{_ts()}] Session reset for {chat_id}', flush=True)
|
||||||
self._schedule_send(chat_id, 'Memulai sesi baru. Ada yang bisa dibantu?', reply_to_msg_id)
|
self._schedule_send(chat_id, 'Memulai sesi baru. Ada yang bisa dibantu?', reply_to_msg_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -203,7 +203,7 @@ class TelegramClient:
|
|||||||
|
|
||||||
def on_tool_calls(tnames):
|
def on_tool_calls(tnames):
|
||||||
info = f'Using: {", ".join(tnames)}'
|
info = f'Using: {", ".join(tnames)}'
|
||||||
print(f'[{_ts()}] {info}')
|
print(f'[{_ts()}] {info}', flush=True)
|
||||||
if not is_roleplay:
|
if not is_roleplay:
|
||||||
self._schedule_send(chat_id, info, reply_to_msg_id)
|
self._schedule_send(chat_id, info, reply_to_msg_id)
|
||||||
|
|
||||||
@ -231,17 +231,17 @@ class TelegramClient:
|
|||||||
recent_history=recent_history,
|
recent_history=recent_history,
|
||||||
my_name=my_name,
|
my_name=my_name,
|
||||||
):
|
):
|
||||||
print(f'[{_ts()}] need_response=True → sending response')
|
print(f'[{_ts()}] need_response=True → sending response', flush=True)
|
||||||
self._schedule_send(chat_id, final_content, reply_to_msg_id)
|
self._schedule_send(chat_id, final_content, reply_to_msg_id)
|
||||||
else:
|
else:
|
||||||
print(f'[{_ts()}] need_response=False → staying silent')
|
print(f'[{_ts()}] need_response=False → staying silent', flush=True)
|
||||||
else:
|
else:
|
||||||
from tools.roleplayer import _name_mentioned
|
from tools.roleplayer import _name_mentioned
|
||||||
if _name_mentioned(my_name, body):
|
if _name_mentioned(my_name, body):
|
||||||
print(f'[{_ts()}] Name mentioned → sending response')
|
print(f'[{_ts()}] Name mentioned → sending response', flush=True)
|
||||||
self._schedule_send(chat_id, final_content, reply_to_msg_id)
|
self._schedule_send(chat_id, final_content, reply_to_msg_id)
|
||||||
else:
|
else:
|
||||||
print(f'[{_ts()}] Name not mentioned → staying silent')
|
print(f'[{_ts()}] Name not mentioned → staying silent', flush=True)
|
||||||
else:
|
else:
|
||||||
self._schedule_send(chat_id, final_content, reply_to_msg_id)
|
self._schedule_send(chat_id, final_content, reply_to_msg_id)
|
||||||
else:
|
else:
|
||||||
@ -255,7 +255,7 @@ class TelegramClient:
|
|||||||
if self._loop and not self._loop.is_closed():
|
if self._loop and not self._loop.is_closed():
|
||||||
char_count = len(text) if text else 0
|
char_count = len(text) if text else 0
|
||||||
sleep_delay = max(1.0, min(char_count / config.TYPING_SPEED, config.TYPING_MAX))
|
sleep_delay = max(1.0, min(char_count / config.TYPING_SPEED, config.TYPING_MAX))
|
||||||
print(f'[{_ts()}] Typing delay: {sleep_delay:.1f}s ({char_count} chars)')
|
print(f'[{_ts()}] Typing delay: {sleep_delay:.1f}s ({char_count} chars)', flush=True)
|
||||||
time.sleep(sleep_delay)
|
time.sleep(sleep_delay)
|
||||||
|
|
||||||
asyncio.run_coroutine_threadsafe(
|
asyncio.run_coroutine_threadsafe(
|
||||||
@ -267,9 +267,9 @@ class TelegramClient:
|
|||||||
self._loop
|
self._loop
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(f'[{_ts()}] WARNING: cannot send to {chat_id} — loop unavailable')
|
print(f'[{_ts()}] WARNING: cannot send to {chat_id} — loop unavailable', flush=True)
|
||||||
|
|
||||||
def _timeout_session(self, chat_id):
|
def _timeout_session(self, chat_id):
|
||||||
print(f'[{_ts()}] Session timeout: {chat_id}')
|
print(f'[{_ts()}] Session timeout: {chat_id}', flush=True)
|
||||||
self._schedule_send(chat_id, 'Sesi ditutup. Sampai jumpa')
|
self._schedule_send(chat_id, 'Sesi ditutup. Sampai jumpa')
|
||||||
self._session_mgr.reset(str(chat_id))
|
self._session_mgr.reset(str(chat_id))
|
||||||
|
|||||||
@ -99,7 +99,7 @@ class XMPPClient(ClientXMPP):
|
|||||||
pending = self._muc_rejoin_tasks.get(room)
|
pending = self._muc_rejoin_tasks.get(room)
|
||||||
if pending and not pending.done():
|
if pending and not pending.done():
|
||||||
pending.cancel()
|
pending.cancel()
|
||||||
print(f'[{_ts()}] MUC [{room}] Cancelled pending rejoin (new trigger)')
|
print(f'[{_ts()}] MUC [{room}] Cancelled pending rejoin (new trigger)', flush=True)
|
||||||
|
|
||||||
# Check cooldown: jangan rejoin terlalu cepat berturut-turut
|
# Check cooldown: jangan rejoin terlalu cepat berturut-turut
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
@ -109,7 +109,7 @@ class XMPPClient(ClientXMPP):
|
|||||||
if elapsed < MUC_REJOIN_COOLDOWN:
|
if elapsed < MUC_REJOIN_COOLDOWN:
|
||||||
# Anti-ban: too soon, schedule delayed rejoin instead of immediate
|
# Anti-ban: too soon, schedule delayed rejoin instead of immediate
|
||||||
cooldown_left = MUC_REJOIN_COOLDOWN - elapsed
|
cooldown_left = MUC_REJOIN_COOLDOWN - elapsed
|
||||||
print(f'[{_ts()}] MUC [{room}] Cooldown active ({cooldown_left:.0f}s left), delaying rejoin')
|
print(f'[{_ts()}] MUC [{room}] Cooldown active ({cooldown_left:.0f}s left), delaying rejoin', flush=True)
|
||||||
delay = cooldown_left + self._calc_rejoin_delay(room)
|
delay = cooldown_left + self._calc_rejoin_delay(room)
|
||||||
else:
|
else:
|
||||||
delay = self._calc_rejoin_delay(room)
|
delay = self._calc_rejoin_delay(room)
|
||||||
@ -120,7 +120,7 @@ class XMPPClient(ClientXMPP):
|
|||||||
attempts = self._muc_rejoin_attempts.get(room, 0) + 1
|
attempts = self._muc_rejoin_attempts.get(room, 0) + 1
|
||||||
self._muc_rejoin_attempts[room] = attempts
|
self._muc_rejoin_attempts[room] = attempts
|
||||||
|
|
||||||
print(f'[{_ts()}] MUC [{room}] Rejoin scheduled in {delay:.0f}s (attempt #{attempts})')
|
print(f'[{_ts()}] MUC [{room}] Rejoin scheduled in {delay:.0f}s (attempt #{attempts})', flush=True)
|
||||||
|
|
||||||
if self._loop and not self._loop.is_closed():
|
if self._loop and not self._loop.is_closed():
|
||||||
task = asyncio.run_coroutine_threadsafe(
|
task = asyncio.run_coroutine_threadsafe(
|
||||||
@ -134,20 +134,20 @@ class XMPPClient(ClientXMPP):
|
|||||||
await asyncio.sleep(delay)
|
await asyncio.sleep(delay)
|
||||||
# Double-check: jangan rejoin kalau sudah di _muc_ready
|
# Double-check: jangan rejoin kalau sudah di _muc_ready
|
||||||
if room in self._muc_ready:
|
if room in self._muc_ready:
|
||||||
print(f'[{_ts()}] MUC [{room}] Already ready, skip rejoin')
|
print(f'[{_ts()}] MUC [{room}] Already ready, skip rejoin', flush=True)
|
||||||
return
|
return
|
||||||
nick = self._get_muc_nick(room)
|
nick = self._get_muc_nick(room)
|
||||||
print(f'[{_ts()}] MUC [{room}] Rejoining as {nick}...')
|
print(f'[{_ts()}] MUC [{room}] Rejoining as {nick}...', flush=True)
|
||||||
await self.plugin['xep_0045'].join_muc_wait(room, nick, maxstanzas=0)
|
await self.plugin['xep_0045'].join_muc_wait(room, nick, maxstanzas=0)
|
||||||
self._muc_last_join[room] = datetime.now()
|
self._muc_last_join[room] = datetime.now()
|
||||||
# _muc_ready akan di-set oleh _on_muc_presence saat join berhasil
|
# _muc_ready akan di-set oleh _on_muc_presence saat join berhasil
|
||||||
self._muc_rejoin_attempts.pop(room, None)
|
self._muc_rejoin_attempts.pop(room, None)
|
||||||
self._muc_rejoin_attempts.pop("_nick_" + room, None)
|
self._muc_rejoin_attempts.pop("_nick_" + room, None)
|
||||||
print(f'[{_ts()}] MUC [{room}] Rejoin successful as {nick}')
|
print(f'[{_ts()}] MUC [{room}] Rejoin successful as {nick}', flush=True)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
print(f'[{_ts()}] MUC [{room}] Rejoin cancelled')
|
print(f'[{_ts()}] MUC [{room}] Rejoin cancelled', flush=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'[{_ts()}] MUC [{room}] Rejoin failed: {e}')
|
print(f'[{_ts()}] MUC [{room}] Rejoin failed: {e}', flush=True)
|
||||||
# Anti-ban: handle 409 Conflict - nick sudah dipakai orang lain
|
# Anti-ban: handle 409 Conflict - nick sudah dipakai orang lain
|
||||||
if '409' in str(e) or 'conflict' in str(e).lower():
|
if '409' in str(e) or 'conflict' in str(e).lower():
|
||||||
nick_attempts = self._muc_rejoin_attempts.get("_nick_" + room, 0)
|
nick_attempts = self._muc_rejoin_attempts.get("_nick_" + room, 0)
|
||||||
@ -155,33 +155,33 @@ class XMPPClient(ClientXMPP):
|
|||||||
# Anti-ban: coba nick alternatif (lily_, lily__)
|
# Anti-ban: coba nick alternatif (lily_, lily__)
|
||||||
self._muc_rejoin_attempts["_nick_" + room] = nick_attempts + 1
|
self._muc_rejoin_attempts["_nick_" + room] = nick_attempts + 1
|
||||||
new_nick = self._get_muc_nick(room)
|
new_nick = self._get_muc_nick(room)
|
||||||
print(f'[{_ts()}] MUC [{room}] Nick conflict, trying alternative: {new_nick}')
|
print(f'[{_ts()}] MUC [{room}] Nick conflict, trying alternative: {new_nick}', flush=True)
|
||||||
# Retry segera dengan nick baru (tanpa backoff rejoin, tapi tetap ada delay biasa)
|
# Retry segera dengan nick baru (tanpa backoff rejoin, tapi tetap ada delay biasa)
|
||||||
self._schedule_muc_rejoin(room)
|
self._schedule_muc_rejoin(room)
|
||||||
else:
|
else:
|
||||||
# Anti-ban: semua nick alternativehabis, stop retry untuk avoid ban
|
# 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}] All nick variations exhausted, skipping room', flush=True)
|
||||||
print(f'[{_ts()}] MUC [{room}] Set XMPP_NICKNAME in .env to a unique nick')
|
print(f'[{_ts()}] MUC [{room}] Set XMPP_NICKNAME in .env to a unique nick', flush=True)
|
||||||
else:
|
else:
|
||||||
# Anti-ban: error biasa (network, dll), retry with backoff
|
# Anti-ban: error biasa (network, dll), retry with backoff
|
||||||
self._schedule_muc_rejoin(room)
|
self._schedule_muc_rejoin(room)
|
||||||
|
|
||||||
async def _on_connected(self, event):
|
async def _on_connected(self, event):
|
||||||
print(f'[{_ts()}] XMPP connected')
|
print(f'[{_ts()}] XMPP connected', flush=True)
|
||||||
|
|
||||||
async def _on_disconnected(self, event):
|
async def _on_disconnected(self, event):
|
||||||
print(f'[{_ts()}] XMPP disconnected')
|
print(f'[{_ts()}] XMPP disconnected', flush=True)
|
||||||
# Anti-ban: cancel all pending rejoin tasks on disconnect
|
# Anti-ban: cancel all pending rejoin tasks on disconnect
|
||||||
for room, task in list(self._muc_rejoin_tasks.items()):
|
for room, task in list(self._muc_rejoin_tasks.items()):
|
||||||
if not task.done():
|
if not task.done():
|
||||||
task.cancel()
|
task.cancel()
|
||||||
print(f'[{_ts()}] MUC [{room}] Cancelled pending rejoin (disconnected)')
|
print(f'[{_ts()}] MUC [{room}] Cancelled pending rejoin (disconnected)', flush=True)
|
||||||
self._muc_rejoin_tasks.clear()
|
self._muc_rejoin_tasks.clear()
|
||||||
|
|
||||||
async def _on_session_start(self, event):
|
async def _on_session_start(self, event):
|
||||||
self.send_presence()
|
self.send_presence()
|
||||||
self.get_roster()
|
self.get_roster()
|
||||||
print(f'[{_ts()}] XMPP online as {self.boundjid.full}')
|
print(f'[{_ts()}] XMPP online as {self.boundjid.full}', flush=True)
|
||||||
for room in self._muc_rooms:
|
for room in self._muc_rooms:
|
||||||
# Anti-ban: retry join dengan incremental delay & nick fallback
|
# Anti-ban: retry join dengan incremental delay & nick fallback
|
||||||
success = False
|
success = False
|
||||||
@ -189,35 +189,35 @@ class XMPPClient(ClientXMPP):
|
|||||||
nick = self._get_muc_nick(room)
|
nick = self._get_muc_nick(room)
|
||||||
try:
|
try:
|
||||||
await self.plugin['xep_0045'].join_muc_wait(room, nick, maxstanzas=0)
|
await self.plugin['xep_0045'].join_muc_wait(room, nick, maxstanzas=0)
|
||||||
print(f'[{_ts()}] Joined MUC room: {room} as {nick}')
|
print(f'[{_ts()}] Joined MUC room: {room} as {nick}', flush=True)
|
||||||
self._muc_last_join[room] = datetime.now()
|
self._muc_last_join[room] = datetime.now()
|
||||||
self._muc_rejoin_attempts.pop(room, None)
|
self._muc_rejoin_attempts.pop(room, None)
|
||||||
self._muc_rejoin_attempts.pop("_nick_" + room, None)
|
self._muc_rejoin_attempts.pop("_nick_" + room, None)
|
||||||
success = True
|
success = True
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'[{_ts()}] MUC join attempt #{attempt} failed ({room}): {e}')
|
print(f'[{_ts()}] MUC join attempt #{attempt} failed ({room}): {e}', flush=True)
|
||||||
# Anti-ban: handle 409 Conflict - coba nick alternatif
|
# Anti-ban: handle 409 Conflict - coba nick alternatif
|
||||||
if '409' in str(e) or 'conflict' in str(e).lower():
|
if '409' in str(e) or 'conflict' in str(e).lower():
|
||||||
nick_attempts = self._muc_rejoin_attempts.get("_nick_" + room, 0)
|
nick_attempts = self._muc_rejoin_attempts.get("_nick_" + room, 0)
|
||||||
if nick_attempts < MUC_NICK_SUFFIX_MAX:
|
if nick_attempts < MUC_NICK_SUFFIX_MAX:
|
||||||
nick_attempts += 1
|
nick_attempts += 1
|
||||||
self._muc_rejoin_attempts["_nick_" + room] = nick_attempts
|
self._muc_rejoin_attempts["_nick_" + room] = nick_attempts
|
||||||
print(f'[{_ts()}] MUC [{room}] Nick conflict, switching to: {self._get_muc_nick(room)}')
|
print(f'[{_ts()}] MUC [{room}] Nick conflict, switching to: {self._get_muc_nick(room)}', flush=True)
|
||||||
# Retry segera dengan nick baru (jangan wait)
|
# Retry segera dengan nick baru (jangan wait)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
# Anti-ban: semua nick alternatif habis
|
# Anti-ban: semua nick alternatif habis
|
||||||
print(f'[{_ts()}] MUC [{room}] All nick variations exhausted')
|
print(f'[{_ts()}] MUC [{room}] All nick variations exhausted', flush=True)
|
||||||
break
|
break
|
||||||
elif attempt < 3:
|
elif attempt < 3:
|
||||||
# Anti-ban: error biasa, wait before retry (2s, 4s)
|
# Anti-ban: error biasa, wait before retry (2s, 4s)
|
||||||
retry_delay = 2.0 * attempt
|
retry_delay = 2.0 * attempt
|
||||||
print(f'[{_ts()}] MUC [{room}] Retrying in {retry_delay:.0f}s...')
|
print(f'[{_ts()}] MUC [{room}] Retrying in {retry_delay:.0f}s...', flush=True)
|
||||||
await asyncio.sleep(retry_delay)
|
await asyncio.sleep(retry_delay)
|
||||||
if not success:
|
if not success:
|
||||||
# Anti-ban: semua attempt gagal, schedule background rejoin
|
# Anti-ban: semua attempt gagal, schedule background rejoin
|
||||||
print(f'[{_ts()}] MUC [{room}] All join attempts failed, scheduling background rejoin')
|
print(f'[{_ts()}] MUC [{room}] All join attempts failed, scheduling background rejoin', flush=True)
|
||||||
self._schedule_muc_rejoin(room)
|
self._schedule_muc_rejoin(room)
|
||||||
|
|
||||||
def _on_message(self, msg):
|
def _on_message(self, msg):
|
||||||
@ -227,7 +227,7 @@ class XMPPClient(ClientXMPP):
|
|||||||
body = msg['body'].strip()
|
body = msg['body'].strip()
|
||||||
if not body:
|
if not body:
|
||||||
return
|
return
|
||||||
print(f'[{_ts()}] DM from {jid}: {body[:60]}')
|
print(f'[{_ts()}] DM from {jid}: {body[:60]}', flush=True)
|
||||||
threading.Thread(target=self._process_dm, args=(jid, body), daemon=True).start()
|
threading.Thread(target=self._process_dm, args=(jid, body), daemon=True).start()
|
||||||
|
|
||||||
def _on_groupchat_message(self, msg):
|
def _on_groupchat_message(self, msg):
|
||||||
@ -243,7 +243,7 @@ class XMPPClient(ClientXMPP):
|
|||||||
body = msg['body'].strip()
|
body = msg['body'].strip()
|
||||||
if not body:
|
if not body:
|
||||||
return
|
return
|
||||||
print(f'[{_ts()}] MUC [{room}] <{nick}>: {body[:60]}')
|
print(f'[{_ts()}] MUC [{room}] <{nick}>: {body[:60]}', flush=True)
|
||||||
threading.Thread(target=self._process_muc, args=(room, nick, body), daemon=True).start()
|
threading.Thread(target=self._process_muc, args=(room, nick, body), daemon=True).start()
|
||||||
|
|
||||||
def _is_my_nick(self, room: str, nick: str) -> bool:
|
def _is_my_nick(self, room: str, nick: str) -> bool:
|
||||||
@ -263,20 +263,20 @@ class XMPPClient(ClientXMPP):
|
|||||||
self._muc_rejoin_attempts.pop(room, None)
|
self._muc_rejoin_attempts.pop(room, None)
|
||||||
self._muc_rejoin_attempts.pop("_nick_" + room, None)
|
self._muc_rejoin_attempts.pop("_nick_" + room, None)
|
||||||
if ptype == 'unavailable':
|
if ptype == 'unavailable':
|
||||||
print(f'[{_ts()}] MUC [{room}] <{nick}> left')
|
print(f'[{_ts()}] MUC [{room}] <{nick}> left', flush=True)
|
||||||
# Anti-ban: remove from ready set on unavailable to keep state consistent
|
# Anti-ban: remove from ready set on unavailable to keep state consistent
|
||||||
self._muc_ready.discard(room)
|
self._muc_ready.discard(room)
|
||||||
# Anti-ban: trigger auto-rejoin with exponential backoff
|
# Anti-ban: trigger auto-rejoin with exponential backoff
|
||||||
if self._is_my_nick(room, nick):
|
if self._is_my_nick(room, nick):
|
||||||
self._schedule_muc_rejoin(room)
|
self._schedule_muc_rejoin(room)
|
||||||
elif ptype == 'error':
|
elif ptype == 'error':
|
||||||
print(f'[{_ts()}] MUC [{room}] error: {presence}')
|
print(f'[{_ts()}] MUC [{room}] error: {presence}', flush=True)
|
||||||
# Anti-ban: also rejoin on error (e.g. temporary failure)
|
# Anti-ban: also rejoin on error (e.g. temporary failure)
|
||||||
if self._is_my_nick(room, nick):
|
if self._is_my_nick(room, nick):
|
||||||
self._muc_ready.discard(room)
|
self._muc_ready.discard(room)
|
||||||
self._schedule_muc_rejoin(room)
|
self._schedule_muc_rejoin(room)
|
||||||
else:
|
else:
|
||||||
print(f'[{_ts()}] MUC [{room}] <{nick}> joined (type={ptype})')
|
print(f'[{_ts()}] MUC [{room}] <{nick}> joined (type={ptype})', flush=True)
|
||||||
|
|
||||||
def _process_dm(self, jid, body):
|
def _process_dm(self, jid, body):
|
||||||
session = self._session_mgr.get_or_create(
|
session = self._session_mgr.get_or_create(
|
||||||
@ -292,7 +292,7 @@ class XMPPClient(ClientXMPP):
|
|||||||
|
|
||||||
if body == ':new':
|
if body == ':new':
|
||||||
self._session_mgr.reset(jid)
|
self._session_mgr.reset(jid)
|
||||||
print(f'[{_ts()}] Session reset for {jid}')
|
print(f'[{_ts()}] Session reset for {jid}', flush=True)
|
||||||
self._schedule_send(jid, 'Memulai sesi baru. Ada yang bisa di bantu?')
|
self._schedule_send(jid, 'Memulai sesi baru. Ada yang bisa di bantu?')
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -335,17 +335,17 @@ class XMPPClient(ClientXMPP):
|
|||||||
recent_history=recent_history,
|
recent_history=recent_history,
|
||||||
my_name=my_name,
|
my_name=my_name,
|
||||||
):
|
):
|
||||||
print(f'[{_ts()}] need_response=True → sending response')
|
print(f'[{_ts()}] need_response=True → sending response', flush=True)
|
||||||
self._schedule_send(jid, final_content, 'chat')
|
self._schedule_send(jid, final_content, 'chat')
|
||||||
else:
|
else:
|
||||||
print(f'[{_ts()}] need_response=False → staying silent')
|
print(f'[{_ts()}] need_response=False → staying silent', flush=True)
|
||||||
else:
|
else:
|
||||||
from tools.roleplayer import _name_mentioned
|
from tools.roleplayer import _name_mentioned
|
||||||
if _name_mentioned(my_name, quote):
|
if _name_mentioned(my_name, quote):
|
||||||
print(f'[{_ts()}] Name mentioned → sending response')
|
print(f'[{_ts()}] Name mentioned → sending response', flush=True)
|
||||||
self._schedule_send(jid, final_content, 'chat')
|
self._schedule_send(jid, final_content, 'chat')
|
||||||
else:
|
else:
|
||||||
print(f'[{_ts()}] Name not mentioned → staying silent')
|
print(f'[{_ts()}] Name not mentioned → staying silent', flush=True)
|
||||||
else:
|
else:
|
||||||
self._schedule_send(jid, f'> {quote}\n{final_content}', 'chat')
|
self._schedule_send(jid, f'> {quote}\n{final_content}', 'chat')
|
||||||
else:
|
else:
|
||||||
@ -370,7 +370,7 @@ class XMPPClient(ClientXMPP):
|
|||||||
|
|
||||||
if body == ':new':
|
if body == ':new':
|
||||||
self._session_mgr.reset(room)
|
self._session_mgr.reset(room)
|
||||||
print(f'[{_ts()}] Session reset for MUC room {room}')
|
print(f'[{_ts()}] Session reset for MUC room {room}', flush=True)
|
||||||
self._schedule_send(room, 'Memulai sesi baru. Ada yang bisa di bantu?', mtype='groupchat')
|
self._schedule_send(room, 'Memulai sesi baru. Ada yang bisa di bantu?', mtype='groupchat')
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -413,17 +413,17 @@ class XMPPClient(ClientXMPP):
|
|||||||
recent_history=recent_history,
|
recent_history=recent_history,
|
||||||
my_name=my_name,
|
my_name=my_name,
|
||||||
):
|
):
|
||||||
print(f'[{_ts()}] need_response=True → sending response')
|
print(f'[{_ts()}] need_response=True → sending response', flush=True)
|
||||||
self._schedule_send(room, final_content, 'groupchat')
|
self._schedule_send(room, final_content, 'groupchat')
|
||||||
else:
|
else:
|
||||||
print(f'[{_ts()}] need_response=False → staying silent')
|
print(f'[{_ts()}] need_response=False → staying silent', flush=True)
|
||||||
else:
|
else:
|
||||||
from tools.roleplayer import _name_mentioned
|
from tools.roleplayer import _name_mentioned
|
||||||
if _name_mentioned(my_name, quote):
|
if _name_mentioned(my_name, quote):
|
||||||
print(f'[{_ts()}] Name mentioned → sending response')
|
print(f'[{_ts()}] Name mentioned → sending response', flush=True)
|
||||||
self._schedule_send(room, final_content, 'groupchat')
|
self._schedule_send(room, final_content, 'groupchat')
|
||||||
else:
|
else:
|
||||||
print(f'[{_ts()}] Name not mentioned → staying silent')
|
print(f'[{_ts()}] Name not mentioned → staying silent', flush=True)
|
||||||
else:
|
else:
|
||||||
self._schedule_send(room, f'> {quote}\n{final_content}', 'groupchat')
|
self._schedule_send(room, f'> {quote}\n{final_content}', 'groupchat')
|
||||||
else:
|
else:
|
||||||
@ -445,27 +445,27 @@ class XMPPClient(ClientXMPP):
|
|||||||
self._send_coro(to, body, mtype), self._loop
|
self._send_coro(to, body, mtype), self._loop
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(f'[{_ts()}] WARNING: cannot send to {to} — loop unavailable')
|
print(f'[{_ts()}] WARNING: cannot send to {to} — loop unavailable', flush=True)
|
||||||
|
|
||||||
async def _send_coro(self, to, body, mtype):
|
async def _send_coro(self, to, body, mtype):
|
||||||
try:
|
try:
|
||||||
# Delay 2: simulasi mengetik (proporsional dengan panjang pesan)
|
# Delay 2: simulasi mengetik (proporsional dengan panjang pesan)
|
||||||
delay = _typing_delay(body)
|
delay = _typing_delay(body)
|
||||||
print(f'[{_ts()}] Typing delay: {delay:.1f}s ({len(body)} chars)')
|
print(f'[{_ts()}] Typing delay: {delay:.1f}s ({len(body)} chars)', flush=True)
|
||||||
await asyncio.sleep(delay)
|
await asyncio.sleep(delay)
|
||||||
|
|
||||||
msg = self.make_message(mto=to, mbody=body, mtype=mtype)
|
msg = self.make_message(mto=to, mbody=body, mtype=mtype)
|
||||||
msg.send()
|
msg.send()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'[{_ts()}] SEND ERROR: {e}')
|
print(f'[{_ts()}] SEND ERROR: {e}', flush=True)
|
||||||
|
|
||||||
def _timeout_session(self, session_id, mtype):
|
def _timeout_session(self, session_id, mtype):
|
||||||
print(f'[{_ts()}] Session timeout: {session_id}')
|
print(f'[{_ts()}] Session timeout: {session_id}', flush=True)
|
||||||
self._schedule_send(session_id, 'Sesi ditutup. Sampai jumpa', mtype)
|
self._schedule_send(session_id, 'Sesi ditutup. Sampai jumpa', mtype)
|
||||||
self._session_mgr.reset(session_id)
|
self._session_mgr.reset(session_id)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
print(f'[{_ts()}] Starting XMPP service...')
|
print(f'[{_ts()}] Starting XMPP service...', flush=True)
|
||||||
asyncio.run(self._run())
|
asyncio.run(self._run())
|
||||||
|
|
||||||
async def _run(self):
|
async def _run(self):
|
||||||
@ -485,7 +485,7 @@ class XMPPClient(ClientXMPP):
|
|||||||
await self._stopped.wait()
|
await self._stopped.wait()
|
||||||
except (asyncio.CancelledError, KeyboardInterrupt):
|
except (asyncio.CancelledError, KeyboardInterrupt):
|
||||||
pass
|
pass
|
||||||
print(f'[{_ts()}] Shutting down...')
|
print(f'[{_ts()}] Shutting down...', flush=True)
|
||||||
await self.disconnect()
|
await self.disconnect()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user