- Moved on_message_event() call to END of message processing in bot.py - Only track messages for autonomous when NOT addressed to Miku - Fixed autonomous_engine.py to convert all message-triggered actions to join_conversation - Prevent inappropriate autonomous actions (general, share_tweet, change_profile_picture) when triggered by user messages - Ensures Miku responds to user messages FIRST before any autonomous action fires This fixes the issue where autonomous actions would fire before Miku's response to user messages, and ensures the 'detect and join conversation' safeguard works properly.
650 lines
31 KiB
Python
650 lines
31 KiB
Python
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
|
||
)
|
||
from utils.llm import query_llama
|
||
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)
|
||
|
||
# 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
|
||
|
||
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
|
||
|
||
# Check if message is addressed to Miku (needed to decide whether to track for autonomous)
|
||
miku_addressed = await is_miku_addressed(message)
|
||
|
||
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 miku_addressed:
|
||
|
||
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)
|
||
truncated = (qwen_description[:50] + "...") if len(qwen_description) > 50 else qwen_description
|
||
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
|
||
|
||
response = await query_llama(
|
||
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
|
||
response = await query_llama(
|
||
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)")
|
||
|
||
# V2: Track message for autonomous engine (non-blocking, no LLM calls)
|
||
# IMPORTANT: Only call this if the message was NOT addressed to Miku
|
||
# This prevents autonomous actions from firing when the user is directly talking to Miku
|
||
if not miku_addressed:
|
||
on_message_event(message)
|
||
|
||
# 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)
|