2025-12-07 17:15:09 +02:00
|
|
|
|
import discord
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import uvicorn
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import random
|
|
|
|
|
|
import string
|
|
|
|
|
|
import signal
|
|
|
|
|
|
import atexit
|
|
|
|
|
|
from api import app
|
|
|
|
|
|
|
|
|
|
|
|
from server_manager import server_manager
|
|
|
|
|
|
from utils.scheduled import (
|
|
|
|
|
|
send_monday_video
|
|
|
|
|
|
)
|
|
|
|
|
|
from utils.image_handling import (
|
|
|
|
|
|
download_and_encode_image,
|
|
|
|
|
|
download_and_encode_media,
|
|
|
|
|
|
extract_video_frames,
|
|
|
|
|
|
analyze_image_with_qwen,
|
|
|
|
|
|
analyze_video_with_vision,
|
|
|
|
|
|
rephrase_as_miku,
|
|
|
|
|
|
extract_tenor_gif_url,
|
|
|
|
|
|
convert_gif_to_mp4,
|
|
|
|
|
|
extract_embed_content
|
|
|
|
|
|
)
|
|
|
|
|
|
from utils.core import (
|
|
|
|
|
|
is_miku_addressed,
|
|
|
|
|
|
)
|
|
|
|
|
|
from utils.moods import (
|
|
|
|
|
|
detect_mood_shift
|
|
|
|
|
|
)
|
|
|
|
|
|
from utils.media import(
|
|
|
|
|
|
overlay_username_with_ffmpeg
|
|
|
|
|
|
)
|
2025-12-07 17:50:08 +02:00
|
|
|
|
from utils.llm import query_llama
|
2025-12-07 17:15:09 +02:00
|
|
|
|
from utils.autonomous import (
|
|
|
|
|
|
setup_autonomous_speaking,
|
|
|
|
|
|
load_last_sent_tweets,
|
|
|
|
|
|
# V2 imports
|
|
|
|
|
|
on_message_event,
|
|
|
|
|
|
on_presence_update as autonomous_presence_update,
|
|
|
|
|
|
on_member_join as autonomous_member_join,
|
|
|
|
|
|
initialize_v2_system
|
|
|
|
|
|
)
|
|
|
|
|
|
from utils.dm_logger import dm_logger
|
|
|
|
|
|
from utils.dm_interaction_analyzer import init_dm_analyzer
|
|
|
|
|
|
|
|
|
|
|
|
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}')
|
|
|
|
|
|
print(f'💬 DM support enabled - users can message Miku directly!')
|
|
|
|
|
|
|
|
|
|
|
|
globals.BOT_USER = globals.client.user
|
|
|
|
|
|
|
|
|
|
|
|
# Initialize DM interaction analyzer
|
|
|
|
|
|
if globals.OWNER_USER_ID and globals.OWNER_USER_ID != 0:
|
|
|
|
|
|
init_dm_analyzer(globals.OWNER_USER_ID)
|
|
|
|
|
|
print(f"📊 DM Interaction Analyzer initialized for owner ID: {globals.OWNER_USER_ID}")
|
|
|
|
|
|
|
|
|
|
|
|
# Schedule daily DM analysis (runs at 2 AM every day)
|
|
|
|
|
|
from utils.scheduled import run_daily_dm_analysis
|
|
|
|
|
|
globals.scheduler.add_job(
|
|
|
|
|
|
run_daily_dm_analysis,
|
|
|
|
|
|
'cron',
|
|
|
|
|
|
hour=2,
|
|
|
|
|
|
minute=0,
|
|
|
|
|
|
id='daily_dm_analysis'
|
|
|
|
|
|
)
|
|
|
|
|
|
print("⏰ Scheduled daily DM analysis at 2:00 AM")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("⚠️ OWNER_USER_ID not set, DM analysis feature disabled")
|
|
|
|
|
|
|
|
|
|
|
|
# Setup autonomous speaking (now handled by server manager)
|
|
|
|
|
|
setup_autonomous_speaking()
|
|
|
|
|
|
load_last_sent_tweets()
|
|
|
|
|
|
|
|
|
|
|
|
# Initialize the V2 autonomous system
|
|
|
|
|
|
initialize_v2_system(globals.client)
|
|
|
|
|
|
|
|
|
|
|
|
# Initialize profile picture manager
|
|
|
|
|
|
from utils.profile_picture_manager import profile_picture_manager
|
|
|
|
|
|
await profile_picture_manager.initialize()
|
|
|
|
|
|
|
|
|
|
|
|
# Save current avatar as fallback
|
|
|
|
|
|
await profile_picture_manager.save_current_avatar_as_fallback()
|
|
|
|
|
|
|
|
|
|
|
|
# Start server-specific schedulers (includes DM mood rotation)
|
|
|
|
|
|
server_manager.start_all_schedulers(globals.client)
|
2025-12-07 18:02:22 +02:00
|
|
|
|
|
2025-12-07 17:15:09 +02:00
|
|
|
|
# Start the global scheduler for other tasks
|
|
|
|
|
|
globals.scheduler.start()
|
|
|
|
|
|
|
|
|
|
|
|
@globals.client.event
|
|
|
|
|
|
async def on_message(message):
|
|
|
|
|
|
if message.author == globals.client.user:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# V2: Track message for autonomous engine (non-blocking, no LLM calls)
|
|
|
|
|
|
on_message_event(message)
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
# Check if this is a DM
|
|
|
|
|
|
is_dm = message.guild is None
|
|
|
|
|
|
|
|
|
|
|
|
if is_dm:
|
|
|
|
|
|
print(f"💌 DM from {message.author.display_name}: {message.content[:50]}{'...' if len(message.content) > 50 else ''}")
|
|
|
|
|
|
|
|
|
|
|
|
# Check if user is blocked
|
|
|
|
|
|
if dm_logger.is_user_blocked(message.author.id):
|
|
|
|
|
|
print(f"🚫 Blocked user {message.author.display_name} ({message.author.id}) tried to send DM - ignoring")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Log the user's DM message
|
|
|
|
|
|
dm_logger.log_user_message(message.author, message, is_bot_message=False)
|
|
|
|
|
|
|
|
|
|
|
|
if await is_miku_addressed(message):
|
|
|
|
|
|
|
|
|
|
|
|
prompt = text # No cleanup — keep it raw
|
|
|
|
|
|
user_id = str(message.author.id)
|
|
|
|
|
|
|
|
|
|
|
|
# If user is replying to a specific message, add context marker
|
|
|
|
|
|
if message.reference:
|
|
|
|
|
|
try:
|
|
|
|
|
|
replied_msg = await message.channel.fetch_message(message.reference.message_id)
|
|
|
|
|
|
# Only add context if replying to Miku's message
|
|
|
|
|
|
if replied_msg.author == globals.client.user:
|
|
|
|
|
|
# Truncate the replied message to keep prompt manageable
|
|
|
|
|
|
replied_content = replied_msg.content[:200] + "..." if len(replied_msg.content) > 200 else replied_msg.content
|
|
|
|
|
|
# Add reply context marker to the prompt
|
|
|
|
|
|
prompt = f'[Replying to your message: "{replied_content}"] {prompt}'
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"⚠️ Failed to fetch replied message for context: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
async with message.channel.typing():
|
|
|
|
|
|
# If message has an image, video, or GIF attachment
|
|
|
|
|
|
if message.attachments:
|
|
|
|
|
|
for attachment in message.attachments:
|
|
|
|
|
|
# Handle images
|
|
|
|
|
|
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)
|
|
|
|
|
|
# For DMs, pass None as guild_id to use DM mood
|
|
|
|
|
|
guild_id = message.guild.id if message.guild else None
|
|
|
|
|
|
miku_reply = await rephrase_as_miku(
|
|
|
|
|
|
qwen_description,
|
|
|
|
|
|
prompt,
|
|
|
|
|
|
guild_id=guild_id,
|
|
|
|
|
|
user_id=str(message.author.id),
|
|
|
|
|
|
author_name=message.author.display_name,
|
|
|
|
|
|
media_type="image"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if is_dm:
|
|
|
|
|
|
print(f"💌 DM image response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"💬 Server image response to {message.author.display_name} in {message.guild.name} (using server mood)")
|
|
|
|
|
|
|
|
|
|
|
|
response_message = await message.channel.send(miku_reply)
|
|
|
|
|
|
|
|
|
|
|
|
# Log the bot's DM response
|
|
|
|
|
|
if is_dm:
|
|
|
|
|
|
dm_logger.log_user_message(message.author, response_message, is_bot_message=True)
|
|
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Handle videos and GIFs
|
|
|
|
|
|
elif any(attachment.filename.lower().endswith(ext) for ext in [".gif", ".mp4", ".webm", ".mov"]):
|
|
|
|
|
|
# Determine media type
|
|
|
|
|
|
is_gif = attachment.filename.lower().endswith('.gif')
|
|
|
|
|
|
media_type = "gif" if is_gif else "video"
|
|
|
|
|
|
|
|
|
|
|
|
print(f"🎬 Processing {media_type}: {attachment.filename}")
|
|
|
|
|
|
|
|
|
|
|
|
# Download the media
|
|
|
|
|
|
media_bytes_b64 = await download_and_encode_media(attachment.url)
|
|
|
|
|
|
if not media_bytes_b64:
|
|
|
|
|
|
await message.channel.send(f"I couldn't load the {media_type}, sorry!")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Decode back to bytes for frame extraction
|
|
|
|
|
|
import base64
|
|
|
|
|
|
media_bytes = base64.b64decode(media_bytes_b64)
|
|
|
|
|
|
|
|
|
|
|
|
# If it's a GIF, convert to MP4 for better processing
|
|
|
|
|
|
if is_gif:
|
|
|
|
|
|
print(f"🔄 Converting GIF to MP4 for processing...")
|
|
|
|
|
|
mp4_bytes = await convert_gif_to_mp4(media_bytes)
|
|
|
|
|
|
if mp4_bytes:
|
|
|
|
|
|
media_bytes = mp4_bytes
|
|
|
|
|
|
print(f"✅ GIF converted to MP4")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"⚠️ GIF conversion failed, trying direct processing")
|
|
|
|
|
|
|
|
|
|
|
|
# Extract frames
|
|
|
|
|
|
frames = await extract_video_frames(media_bytes, num_frames=6)
|
|
|
|
|
|
|
|
|
|
|
|
if not frames:
|
|
|
|
|
|
await message.channel.send(f"I couldn't extract frames from that {media_type}, sorry!")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
print(f"📹 Extracted {len(frames)} frames from {attachment.filename}")
|
|
|
|
|
|
|
|
|
|
|
|
# Analyze the video/GIF with appropriate media type
|
|
|
|
|
|
video_description = await analyze_video_with_vision(frames, media_type=media_type)
|
|
|
|
|
|
# For DMs, pass None as guild_id to use DM mood
|
|
|
|
|
|
guild_id = message.guild.id if message.guild else None
|
|
|
|
|
|
miku_reply = await rephrase_as_miku(
|
|
|
|
|
|
video_description,
|
|
|
|
|
|
prompt,
|
|
|
|
|
|
guild_id=guild_id,
|
|
|
|
|
|
user_id=str(message.author.id),
|
|
|
|
|
|
author_name=message.author.display_name,
|
|
|
|
|
|
media_type=media_type
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if is_dm:
|
|
|
|
|
|
print(f"💌 DM {media_type} response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"💬 Server video response to {message.author.display_name} in {message.guild.name} (using server mood)")
|
|
|
|
|
|
|
|
|
|
|
|
response_message = await message.channel.send(miku_reply)
|
|
|
|
|
|
|
|
|
|
|
|
# Log the bot's DM response
|
|
|
|
|
|
if is_dm:
|
|
|
|
|
|
dm_logger.log_user_message(message.author, response_message, is_bot_message=True)
|
|
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Check for embeds (articles, images, videos, GIFs, etc.)
|
|
|
|
|
|
if message.embeds:
|
|
|
|
|
|
for embed in message.embeds:
|
|
|
|
|
|
# Handle Tenor GIF embeds specially (Discord uses these for /gif command)
|
|
|
|
|
|
if embed.type == 'gifv' and embed.url and 'tenor.com' in embed.url:
|
|
|
|
|
|
print(f"🎭 Processing Tenor GIF from embed: {embed.url}")
|
|
|
|
|
|
|
|
|
|
|
|
# Extract the actual GIF URL from Tenor
|
|
|
|
|
|
gif_url = await extract_tenor_gif_url(embed.url)
|
|
|
|
|
|
if not gif_url:
|
|
|
|
|
|
# Try using the embed's video or image URL as fallback
|
|
|
|
|
|
if hasattr(embed, 'video') and embed.video:
|
|
|
|
|
|
gif_url = embed.video.url
|
|
|
|
|
|
elif hasattr(embed, 'thumbnail') and embed.thumbnail:
|
|
|
|
|
|
gif_url = embed.thumbnail.url
|
|
|
|
|
|
|
|
|
|
|
|
if not gif_url:
|
|
|
|
|
|
print(f"⚠️ Could not extract GIF URL from Tenor embed")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# Download the GIF
|
|
|
|
|
|
media_bytes_b64 = await download_and_encode_media(gif_url)
|
|
|
|
|
|
if not media_bytes_b64:
|
|
|
|
|
|
await message.channel.send("I couldn't load that Tenor GIF, sorry!")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Decode to bytes
|
|
|
|
|
|
import base64
|
|
|
|
|
|
media_bytes = base64.b64decode(media_bytes_b64)
|
|
|
|
|
|
|
|
|
|
|
|
# Convert GIF to MP4
|
|
|
|
|
|
print(f"🔄 Converting Tenor GIF to MP4 for processing...")
|
|
|
|
|
|
mp4_bytes = await convert_gif_to_mp4(media_bytes)
|
|
|
|
|
|
if not mp4_bytes:
|
|
|
|
|
|
print(f"⚠️ GIF conversion failed, trying direct frame extraction")
|
|
|
|
|
|
mp4_bytes = media_bytes
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"✅ Tenor GIF converted to MP4")
|
|
|
|
|
|
|
|
|
|
|
|
# Extract frames
|
|
|
|
|
|
frames = await extract_video_frames(mp4_bytes, num_frames=6)
|
|
|
|
|
|
|
|
|
|
|
|
if not frames:
|
|
|
|
|
|
await message.channel.send("I couldn't extract frames from that GIF, sorry!")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
print(f"📹 Extracted {len(frames)} frames from Tenor GIF")
|
|
|
|
|
|
|
|
|
|
|
|
# Analyze the GIF with tenor_gif media type
|
|
|
|
|
|
video_description = await analyze_video_with_vision(frames, media_type="tenor_gif")
|
|
|
|
|
|
guild_id = message.guild.id if message.guild else None
|
|
|
|
|
|
miku_reply = await rephrase_as_miku(
|
|
|
|
|
|
video_description,
|
|
|
|
|
|
prompt,
|
|
|
|
|
|
guild_id=guild_id,
|
|
|
|
|
|
user_id=str(message.author.id),
|
|
|
|
|
|
author_name=message.author.display_name,
|
|
|
|
|
|
media_type="tenor_gif"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if is_dm:
|
|
|
|
|
|
print(f"💌 DM Tenor GIF response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"💬 Server Tenor GIF response to {message.author.display_name} in {message.guild.name} (using server mood)")
|
|
|
|
|
|
|
|
|
|
|
|
response_message = await message.channel.send(miku_reply)
|
|
|
|
|
|
|
|
|
|
|
|
# Log the bot's DM response
|
|
|
|
|
|
if is_dm:
|
|
|
|
|
|
dm_logger.log_user_message(message.author, response_message, is_bot_message=True)
|
|
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Handle other types of embeds (rich, article, image, video, link)
|
|
|
|
|
|
elif embed.type in ['rich', 'article', 'image', 'video', 'link']:
|
|
|
|
|
|
print(f"📰 Processing {embed.type} embed")
|
|
|
|
|
|
|
|
|
|
|
|
# Extract content from embed
|
|
|
|
|
|
embed_content = await extract_embed_content(embed)
|
|
|
|
|
|
|
|
|
|
|
|
if not embed_content['has_content']:
|
|
|
|
|
|
print(f"⚠️ Embed has no extractable content, skipping")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# Build context string with embed text
|
|
|
|
|
|
embed_context_parts = []
|
|
|
|
|
|
if embed_content['text']:
|
|
|
|
|
|
embed_context_parts.append(f"[Embedded content: {embed_content['text'][:500]}{'...' if len(embed_content['text']) > 500 else ''}]")
|
|
|
|
|
|
|
|
|
|
|
|
# Process images from embed
|
|
|
|
|
|
if embed_content['images']:
|
|
|
|
|
|
for img_url in embed_content['images']:
|
|
|
|
|
|
print(f"🖼️ Processing image from embed: {img_url}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
base64_img = await download_and_encode_image(img_url)
|
|
|
|
|
|
if base64_img:
|
|
|
|
|
|
print(f"✅ Image downloaded, analyzing with vision model...")
|
|
|
|
|
|
# Analyze image
|
|
|
|
|
|
qwen_description = await analyze_image_with_qwen(base64_img)
|
2025-12-07 18:03:36 +02:00
|
|
|
|
truncated = (qwen_description[:50] + "...") if len(qwen_description) > 50 else qwen_description
|
2025-12-07 17:15:09 +02:00
|
|
|
|
print(f"📝 Vision analysis result: {truncated}")
|
|
|
|
|
|
if qwen_description and qwen_description.strip():
|
|
|
|
|
|
embed_context_parts.append(f"[Embedded image shows: {qwen_description}]")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"❌ Failed to download image from embed")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"⚠️ Error processing embedded image: {e}")
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
|
|
|
|
# Process videos from embed
|
|
|
|
|
|
if embed_content['videos']:
|
|
|
|
|
|
for video_url in embed_content['videos']:
|
|
|
|
|
|
print(f"🎬 Processing video from embed: {video_url}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
media_bytes_b64 = await download_and_encode_media(video_url)
|
|
|
|
|
|
if media_bytes_b64:
|
|
|
|
|
|
import base64
|
|
|
|
|
|
media_bytes = base64.b64decode(media_bytes_b64)
|
|
|
|
|
|
frames = await extract_video_frames(media_bytes, num_frames=6)
|
|
|
|
|
|
if frames:
|
|
|
|
|
|
print(f"📹 Extracted {len(frames)} frames, analyzing with vision model...")
|
|
|
|
|
|
video_description = await analyze_video_with_vision(frames, media_type="video")
|
|
|
|
|
|
print(f"📝 Video analysis result: {video_description[:100]}...")
|
|
|
|
|
|
if video_description and video_description.strip():
|
|
|
|
|
|
embed_context_parts.append(f"[Embedded video shows: {video_description}]")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"❌ Failed to extract frames from video")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"❌ Failed to download video from embed")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"⚠️ Error processing embedded video: {e}")
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
|
|
|
|
# Combine embed context with user prompt
|
|
|
|
|
|
if embed_context_parts:
|
|
|
|
|
|
full_context = '\n'.join(embed_context_parts)
|
|
|
|
|
|
enhanced_prompt = f"{full_context}\n\nUser message: {prompt}" if prompt else full_context
|
|
|
|
|
|
|
|
|
|
|
|
# Get Miku's response
|
|
|
|
|
|
guild_id = message.guild.id if message.guild else None
|
|
|
|
|
|
response_type = "dm_response" if is_dm else "server_response"
|
|
|
|
|
|
author_name = message.author.display_name
|
|
|
|
|
|
|
2025-12-07 17:50:08 +02:00
|
|
|
|
response = await query_llama(
|
2025-12-07 17:15:09 +02:00
|
|
|
|
enhanced_prompt,
|
|
|
|
|
|
user_id=str(message.author.id),
|
|
|
|
|
|
guild_id=guild_id,
|
|
|
|
|
|
response_type=response_type,
|
|
|
|
|
|
author_name=author_name
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if is_dm:
|
|
|
|
|
|
print(f"💌 DM embed response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"💬 Server embed response to {message.author.display_name} in {message.guild.name}")
|
|
|
|
|
|
|
|
|
|
|
|
response_message = await message.channel.send(response)
|
|
|
|
|
|
|
|
|
|
|
|
# Log the bot's DM response
|
|
|
|
|
|
if is_dm:
|
|
|
|
|
|
dm_logger.log_user_message(message.author, response_message, is_bot_message=True)
|
|
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Check if this is an image generation request
|
|
|
|
|
|
from utils.image_generation import detect_image_request, handle_image_generation_request
|
|
|
|
|
|
is_image_request, image_prompt = await detect_image_request(prompt)
|
|
|
|
|
|
|
|
|
|
|
|
if is_image_request and image_prompt:
|
|
|
|
|
|
print(f"🎨 Image generation request detected: '{image_prompt}' from {message.author.display_name}")
|
|
|
|
|
|
|
|
|
|
|
|
# Handle the image generation workflow
|
|
|
|
|
|
success = await handle_image_generation_request(message, image_prompt)
|
|
|
|
|
|
if success:
|
|
|
|
|
|
return # Image generation completed successfully
|
|
|
|
|
|
|
|
|
|
|
|
# If image generation failed, fall back to normal response
|
|
|
|
|
|
print(f"⚠️ Image generation failed, falling back to normal response")
|
|
|
|
|
|
|
|
|
|
|
|
# If message is just a prompt, no image
|
|
|
|
|
|
# For DMs, pass None as guild_id to use DM mood
|
|
|
|
|
|
guild_id = message.guild.id if message.guild else None
|
|
|
|
|
|
response_type = "dm_response" if is_dm else "server_response"
|
|
|
|
|
|
author_name = message.author.display_name
|
2025-12-07 17:50:08 +02:00
|
|
|
|
response = await query_llama(
|
2025-12-07 17:15:09 +02:00
|
|
|
|
prompt,
|
|
|
|
|
|
user_id=str(message.author.id),
|
|
|
|
|
|
guild_id=guild_id,
|
|
|
|
|
|
response_type=response_type,
|
|
|
|
|
|
author_name=author_name
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if is_dm:
|
|
|
|
|
|
print(f"💌 DM response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"💬 Server response to {message.author.display_name} in {message.guild.name} (using server mood)")
|
|
|
|
|
|
|
|
|
|
|
|
response_message = await message.channel.send(response)
|
|
|
|
|
|
|
|
|
|
|
|
# Log the bot's DM response
|
|
|
|
|
|
if is_dm:
|
|
|
|
|
|
dm_logger.log_user_message(message.author, response_message, is_bot_message=True)
|
|
|
|
|
|
|
|
|
|
|
|
# For server messages, do server-specific mood detection
|
|
|
|
|
|
if not is_dm and message.guild:
|
|
|
|
|
|
try:
|
|
|
|
|
|
from server_manager import server_manager
|
|
|
|
|
|
server_config = server_manager.get_server_config(message.guild.id)
|
|
|
|
|
|
if server_config:
|
|
|
|
|
|
# Create server context for mood detection
|
|
|
|
|
|
server_context = {
|
|
|
|
|
|
'current_mood_name': server_config.current_mood_name,
|
|
|
|
|
|
'current_mood_description': server_config.current_mood_description,
|
|
|
|
|
|
'is_sleeping': server_config.is_sleeping
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
detected = detect_mood_shift(response, server_context)
|
|
|
|
|
|
if detected and detected != server_config.current_mood_name:
|
|
|
|
|
|
print(f"🔄 Auto mood detection for server {message.guild.name}: {server_config.current_mood_name} -> {detected}")
|
|
|
|
|
|
|
|
|
|
|
|
# Block direct transitions to asleep unless from sleepy
|
|
|
|
|
|
if detected == "asleep" and server_config.current_mood_name != "sleepy":
|
|
|
|
|
|
print("❌ Ignoring asleep mood; server wasn't sleepy before.")
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Update server mood
|
|
|
|
|
|
server_manager.set_server_mood(message.guild.id, detected)
|
|
|
|
|
|
|
|
|
|
|
|
# Update nickname for this server
|
|
|
|
|
|
from utils.moods import update_server_nickname
|
|
|
|
|
|
globals.client.loop.create_task(update_server_nickname(message.guild.id))
|
|
|
|
|
|
|
|
|
|
|
|
print(f"🔄 Server mood auto-updated to: {detected}")
|
|
|
|
|
|
|
|
|
|
|
|
if detected == "asleep":
|
|
|
|
|
|
server_manager.set_server_sleep_state(message.guild.id, True)
|
|
|
|
|
|
# Schedule wake-up after 1 hour
|
|
|
|
|
|
async def delayed_wakeup():
|
|
|
|
|
|
await asyncio.sleep(3600) # 1 hour
|
|
|
|
|
|
server_manager.set_server_sleep_state(message.guild.id, False)
|
|
|
|
|
|
server_manager.set_server_mood(message.guild.id, "neutral")
|
|
|
|
|
|
await update_server_nickname(message.guild.id)
|
|
|
|
|
|
print(f"🌅 Server {message.guild.name} woke up from auto-sleep")
|
|
|
|
|
|
|
|
|
|
|
|
globals.client.loop.create_task(delayed_wakeup())
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"⚠️ No server config found for guild {message.guild.id}, skipping mood detection")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"⚠️ Error in server mood detection: {e}")
|
|
|
|
|
|
elif is_dm:
|
|
|
|
|
|
print("💌 DM message - no mood detection (DM mood only changes via auto-rotation)")
|
|
|
|
|
|
|
|
|
|
|
|
# Note: Autonomous reactions are now handled by V2 system via on_message_event()
|
|
|
|
|
|
|
|
|
|
|
|
# Manual Monday test command (only for server messages)
|
|
|
|
|
|
if not is_dm and message.content.lower().strip() == "!monday":
|
|
|
|
|
|
await send_monday_video()
|
|
|
|
|
|
#await message.channel.send("✅ Monday message sent (or attempted). Check logs.")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
@globals.client.event
|
|
|
|
|
|
async def on_raw_reaction_add(payload):
|
|
|
|
|
|
"""Handle reactions added to messages (including bot's own reactions and uncached messages)"""
|
|
|
|
|
|
# Check if this is a DM
|
|
|
|
|
|
if payload.guild_id is not None:
|
|
|
|
|
|
return # Only handle DM reactions
|
|
|
|
|
|
|
|
|
|
|
|
# Get the channel
|
|
|
|
|
|
channel = await globals.client.fetch_channel(payload.channel_id)
|
|
|
|
|
|
if not isinstance(channel, discord.DMChannel):
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Get the user who reacted
|
|
|
|
|
|
user = await globals.client.fetch_user(payload.user_id)
|
|
|
|
|
|
|
|
|
|
|
|
# Get the DM partner (the person DMing the bot, not the bot itself)
|
|
|
|
|
|
# For DMs, we want to log under the user's ID, not the bot's
|
|
|
|
|
|
if user.id == globals.client.user.id:
|
|
|
|
|
|
# Bot reacted - find the other user in the DM
|
|
|
|
|
|
message = await channel.fetch_message(payload.message_id)
|
|
|
|
|
|
dm_user_id = message.author.id if message.author.id != globals.client.user.id else channel.recipient.id
|
|
|
|
|
|
is_bot_reactor = True
|
|
|
|
|
|
else:
|
|
|
|
|
|
# User reacted
|
|
|
|
|
|
dm_user_id = user.id
|
|
|
|
|
|
is_bot_reactor = False
|
|
|
|
|
|
|
|
|
|
|
|
# Get emoji string
|
|
|
|
|
|
emoji_str = str(payload.emoji)
|
|
|
|
|
|
|
|
|
|
|
|
# Log the reaction
|
|
|
|
|
|
await dm_logger.log_reaction_add(
|
|
|
|
|
|
user_id=dm_user_id,
|
|
|
|
|
|
message_id=payload.message_id,
|
|
|
|
|
|
emoji=emoji_str,
|
|
|
|
|
|
reactor_id=user.id,
|
|
|
|
|
|
reactor_name=user.display_name or user.name,
|
|
|
|
|
|
is_bot_reactor=is_bot_reactor
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
reactor_type = "🤖 Miku" if is_bot_reactor else f"👤 {user.display_name}"
|
|
|
|
|
|
print(f"➕ DM reaction added: {emoji_str} by {reactor_type} on message {payload.message_id}")
|
|
|
|
|
|
|
|
|
|
|
|
@globals.client.event
|
|
|
|
|
|
async def on_raw_reaction_remove(payload):
|
|
|
|
|
|
"""Handle reactions removed from messages (including bot's own reactions and uncached messages)"""
|
|
|
|
|
|
# Check if this is a DM
|
|
|
|
|
|
if payload.guild_id is not None:
|
|
|
|
|
|
return # Only handle DM reactions
|
|
|
|
|
|
|
|
|
|
|
|
# Get the channel
|
|
|
|
|
|
channel = await globals.client.fetch_channel(payload.channel_id)
|
|
|
|
|
|
if not isinstance(channel, discord.DMChannel):
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Get the user who removed the reaction
|
|
|
|
|
|
user = await globals.client.fetch_user(payload.user_id)
|
|
|
|
|
|
|
|
|
|
|
|
# Get the DM partner (the person DMing the bot, not the bot itself)
|
|
|
|
|
|
if user.id == globals.client.user.id:
|
|
|
|
|
|
# Bot removed reaction - find the other user in the DM
|
|
|
|
|
|
message = await channel.fetch_message(payload.message_id)
|
|
|
|
|
|
dm_user_id = message.author.id if message.author.id != globals.client.user.id else channel.recipient.id
|
|
|
|
|
|
else:
|
|
|
|
|
|
# User removed reaction
|
|
|
|
|
|
dm_user_id = user.id
|
|
|
|
|
|
|
|
|
|
|
|
# Get emoji string
|
|
|
|
|
|
emoji_str = str(payload.emoji)
|
|
|
|
|
|
|
|
|
|
|
|
# Log the reaction removal
|
|
|
|
|
|
await dm_logger.log_reaction_remove(
|
|
|
|
|
|
user_id=dm_user_id,
|
|
|
|
|
|
message_id=payload.message_id,
|
|
|
|
|
|
emoji=emoji_str,
|
|
|
|
|
|
reactor_id=user.id
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
reactor_type = "🤖 Miku" if user.id == globals.client.user.id else f"👤 {user.display_name}"
|
|
|
|
|
|
print(f"➖ DM reaction removed: {emoji_str} by {reactor_type} from message {payload.message_id}")
|
|
|
|
|
|
|
|
|
|
|
|
@globals.client.event
|
|
|
|
|
|
async def on_presence_update(before, after):
|
|
|
|
|
|
"""Track user presence changes for autonomous V2 system"""
|
|
|
|
|
|
# Discord.py passes before/after Member objects with different states
|
|
|
|
|
|
# We pass the 'after' member and both states for comparison
|
|
|
|
|
|
autonomous_presence_update(after, before, after)
|
|
|
|
|
|
|
|
|
|
|
|
@globals.client.event
|
|
|
|
|
|
async def on_member_join(member):
|
|
|
|
|
|
"""Track member joins for autonomous V2 system"""
|
|
|
|
|
|
autonomous_member_join(member)
|
|
|
|
|
|
|
|
|
|
|
|
def start_api():
|
|
|
|
|
|
uvicorn.run(app, host="0.0.0.0", port=3939, log_level="info")
|
|
|
|
|
|
|
|
|
|
|
|
def save_autonomous_state():
|
|
|
|
|
|
"""Save autonomous context on shutdown"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from utils.autonomous import autonomous_engine
|
|
|
|
|
|
autonomous_engine.save_context()
|
|
|
|
|
|
print("💾 Saved autonomous context on shutdown")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"⚠️ Failed to save autonomous context on shutdown: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
# Register shutdown handlers
|
|
|
|
|
|
atexit.register(save_autonomous_state)
|
|
|
|
|
|
signal.signal(signal.SIGTERM, lambda s, f: save_autonomous_state())
|
|
|
|
|
|
signal.signal(signal.SIGINT, lambda s, f: save_autonomous_state())
|
|
|
|
|
|
|
|
|
|
|
|
threading.Thread(target=start_api, daemon=True).start()
|
|
|
|
|
|
globals.client.run(globals.DISCORD_BOT_TOKEN)
|