import discord import aiohttp import asyncio import random import string import datetime import os import threading import uvicorn import logging import sys from api import app from command_router import handle_command from utils.scheduled import ( schedule_random_bedtime, send_bedtime_reminder, send_monday_video ) from utils.image_handling import ( download_and_encode_image, analyze_image_with_qwen, rephrase_as_miku ) from utils.core import ( is_miku_addressed, ) from utils.moods import ( detect_mood_shift, set_sleep_state, nickname_mood_emoji, rotate_mood, load_mood_description, clear_angry_mood_after_delay ) from utils.media import overlay_username_with_ffmpeg from utils.kindness import detect_and_react_to_kindness from utils.llm import query_ollama from utils.autonomous import setup_autonomous_speaking, load_last_sent_tweets import globals logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s", handlers=[ logging.FileHandler("bot.log", mode='a', encoding='utf-8'), logging.StreamHandler(sys.stdout) # Optional: see logs in stdout too ], force=True # Override previous configs ) @globals.client.event async def on_ready(): print(f'🎤 MikuBot connected as {globals.client.user}') globals.BOT_USER = globals.client.user # Change mood every 1 hour rotate_mood.start() # Schedule the weekly task (Monday 07:30) globals.scheduler.add_job(send_monday_video, 'cron', day_of_week='mon', hour=4, minute=30) # Schedule first bedtime reminder schedule_random_bedtime() # Reschedule every midnight globals.scheduler.add_job(schedule_random_bedtime, 'cron', hour=21, minute=0) #scheduler.add_job(send_bedtime_reminder, 'cron', hour=12, minute=22)i # Schedule autonomous speaking setup_autonomous_speaking() load_last_sent_tweets() globals.scheduler.start() @globals.client.event async def on_message(message): if message.author == globals.client.user: return handled, globals.CURRENT_MOOD_NAME, globals.CURRENT_MOOD, globals.PREVIOUS_MOOD_NAME, globals.IS_SLEEPING = await handle_command( message, set_sleep_state ) if message.content.strip().lower() == "miku, rape this nigga balls" and message.reference: async with message.channel.typing(): # Get replied-to user try: replied_msg = await message.channel.fetch_message(message.reference.message_id) target_username = replied_msg.author.display_name # Prepare video base_video = "MikuMikuBeam.mp4" output_video = f"/tmp/video_{''.join(random.choices(string.ascii_letters, k=5))}.mp4" await overlay_username_with_ffmpeg(base_video, output_video, target_username) caption = f"Here you go, @{target_username}! 🌟" #await message.channel.send(content=caption, file=discord.File(output_video)) await replied_msg.reply(file=discord.File(output_video)) except Exception as e: print(f"⚠️ Error processing video: {e}") await message.channel.send("Sorry, something went wrong while generating the video.") return text = message.content.strip() if await is_miku_addressed(message): if globals.IS_SLEEPING: # Initialize sleepy response count if not set yet if globals.SLEEPY_RESPONSES_LEFT is None: globals.SLEEPY_RESPONSES_LEFT = random.randint(3, 5) print(f"🎲 Sleepy responses allowed: {globals.SLEEPY_RESPONSES_LEFT}") if globals.SLEEPY_RESPONSES_LEFT > 0: if random.random() < 1/3: # ⅓ chance sleep_talk_lines = [ "mnnn... five more minutes... zzz...", "nya... d-don't tickle me there... mm~", "zz... nyaa~ pancakes flying... eep...", "so warm... stay close... zzz...", "huh...? is it morning...? nooo... \*rolls over*", "\*mumbles* pink clouds... and pudding... heehee...", "\*softly snores* zzz... nyuu... mmh..." ] response = random.choice(sleep_talk_lines) await message.channel.typing() await asyncio.sleep(random.uniform(1.5, 3.0)) # random delay before replying await message.channel.send(response) globals.SLEEPY_RESPONSES_LEFT -= 1 print(f"💤 Sleepy responses left: {globals.SLEEPY_RESPONSES_LEFT}") else: # No response at all print("😴 Miku is asleep and didn't respond.") return # Skip any further message handling else: # Exceeded sleepy response count — wake up angry now! globals.IS_SLEEPING = False globals.CURRENT_MOOD_NAME = "angry" globals.CURRENT_MOOD = load_mood_description("angry") globals.SLEEPY_RESPONSES_LEFT = None # Set angry period end time 40 minutes from now globals.FORCED_ANGRY_UNTIL = datetime.datetime.utcnow() + datetime.timedelta(minutes=40) # Cancel any existing angry timer task first if globals.ANGRY_WAKEUP_TIMER and not globals.ANGRY_WAKEUP_TIMER.done(): globals.ANGRY_WAKEUP_TIMER.cancel() # Start cooldown task to clear angry mood after 40 mins globals.ANGRY_WAKEUP_TIMER = asyncio.create_task(clear_angry_mood_after_delay()) print("😡 Miku woke up angry and will stay angry for 40 minutes!") globals.JUST_WOKEN_UP = True # Set flag for next response await nickname_mood_emoji() await set_sleep_state(False) # Immediately get an angry response to send back try: async with message.channel.typing(): angry_response = await query_ollama("...", user_id=str(message.author.id)) await message.channel.send(angry_response) finally: # Reset the flag after sending the angry response globals.JUST_WOKEN_UP = False return prompt = text # No cleanup — keep it raw user_id = str(message.author.id) # 1st kindness check with just keywords if globals.CURRENT_MOOD not in ["angry", "irritated"]: await detect_and_react_to_kindness(message) # Add replied Miku message to conversation history as context if message.reference: try: replied_msg = await message.channel.fetch_message(message.reference.message_id) if replied_msg.author == globals.client.user: history = globals.conversation_history.get(user_id, []) if not history or (history and history[-1][1] != replied_msg.content): globals.conversation_history.setdefault(user_id, []).append(("", replied_msg.content)) except Exception as e: print(f"⚠️ Failed to fetch replied message for context: {e}") async with message.channel.typing(): # If message has an image attachment if message.attachments: for attachment in message.attachments: if any(attachment.filename.lower().endswith(ext) for ext in [".jpg", ".jpeg", ".png", ".webp"]): base64_img = await download_and_encode_image(attachment.url) if not base64_img: await message.channel.send("I couldn't load the image, sorry!") return # Analyze image (objective description) qwen_description = await analyze_image_with_qwen(base64_img) miku_reply = await rephrase_as_miku(qwen_description, prompt) await message.channel.send(miku_reply) return # If message is just a prompt, no image response = await query_ollama(prompt, user_id=str(message.author.id)) await message.channel.send(response) # 2nd kindness check (only if no keywords detected) if globals.CURRENT_MOOD not in ["angry", "irritated"]: await detect_and_react_to_kindness(message, after_reply=True) # Manual Monday test command if message.content.lower().strip() == "!monday": await send_monday_video() #await message.channel.send("✅ Monday message sent (or attempted). Check logs.") return if globals.AUTO_MOOD and 'response' in locals(): # Block auto mood updates if forced angry period is active now = datetime.datetime.utcnow() if globals.FORCED_ANGRY_UNTIL and now < globals.FORCED_ANGRY_UNTIL: print("🚫 Skipping auto mood detection — forced angry period active.") else: detected = detect_mood_shift(response) if detected and detected != globals.CURRENT_MOOD_NAME: # Block direct transitions to asleep unless from sleepy if detected == "asleep" and globals.CURRENT_MOOD_NAME != "sleepy": print("❌ Ignoring asleep mood; Miku wasn't sleepy before.") else: globals.PREVIOUS_MOOD_NAME = globals.CURRENT_MOOD_NAME globals.CURRENT_MOOD_NAME = detected globals.CURRENT_MOOD = load_mood_description(detected) await nickname_mood_emoji() print(f"🔄 Auto-updated mood to: {detected}") if detected == "asleep": globals.IS_SLEEPING = True await set_sleep_state(True) await asyncio.sleep(3600) # 1 hour globals.IS_SLEEPING = False await set_sleep_state(False) globals.CURRENT_MOOD_NAME = "neutral" globals.CURRENT_MOOD = load_mood_description("neutral") def start_api(): uvicorn.run(app, host="0.0.0.0", port=3939, log_level="info") threading.Thread(target=start_api, daemon=True).start() globals.client.run(globals.DISCORD_BOT_TOKEN)