- Created new logging infrastructure with per-component filtering - Added 6 log levels: DEBUG, INFO, API, WARNING, ERROR, CRITICAL - Implemented non-hierarchical level control (any combination can be enabled) - Migrated 917 print() statements across 31 files to structured logging - Created web UI (system.html) for runtime configuration with dark theme - Added global level controls to enable/disable levels across all components - Added timestamp format control (off/time/date/datetime options) - Implemented log rotation (10MB per file, 5 backups) - Added API endpoints for dynamic log configuration - Configured HTTP request logging with filtering via api.requests component - Intercepted APScheduler logs with proper formatting - Fixed persistence paths to use /app/memory for Docker volume compatibility - Fixed checkbox display bug in web UI (enabled_levels now properly shown) - Changed System Settings button to open in same tab instead of new window Components: bot, api, api.requests, autonomous, persona, vision, llm, conversation, mood, dm, scheduled, gpu, media, server, commands, sentiment, core, apscheduler All settings persist across container restarts via JSON config.
658 lines
29 KiB
Python
658 lines
29 KiB
Python
# server_manager.py
|
|
|
|
import json
|
|
import os
|
|
import asyncio
|
|
from typing import Dict, List, Optional, Set
|
|
from dataclasses import dataclass, asdict
|
|
from datetime import datetime, timedelta
|
|
import discord
|
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
from apscheduler.triggers.interval import IntervalTrigger
|
|
from apscheduler.triggers.cron import CronTrigger
|
|
from apscheduler.triggers.date import DateTrigger
|
|
import random
|
|
from datetime import datetime, timedelta
|
|
from utils.logger import get_logger
|
|
|
|
logger = get_logger('server')
|
|
|
|
@dataclass
|
|
class ServerConfig:
|
|
"""Configuration for a single Discord server"""
|
|
guild_id: int
|
|
guild_name: str
|
|
autonomous_channel_id: int
|
|
autonomous_channel_name: str
|
|
bedtime_channel_ids: List[int]
|
|
enabled_features: Set[str] # autonomous, bedtime, monday_video, etc.
|
|
autonomous_interval_minutes: int = 15
|
|
conversation_detection_interval_minutes: int = 3
|
|
bedtime_hour: int = 21
|
|
bedtime_minute: int = 0
|
|
bedtime_hour_end: int = 21 # End of bedtime range (default 11PM)
|
|
bedtime_minute_end: int = 59 # End of bedtime range (default 11:59PM)
|
|
monday_video_hour: int = 4
|
|
monday_video_minute: int = 30
|
|
# Per-server mood tracking
|
|
current_mood_name: str = "neutral"
|
|
current_mood_description: str = ""
|
|
previous_mood_name: str = "neutral"
|
|
is_sleeping: bool = False
|
|
sleepy_responses_left: int = None
|
|
angry_wakeup_timer = None
|
|
forced_angry_until = None
|
|
just_woken_up: bool = False
|
|
|
|
def to_dict(self):
|
|
return asdict(self)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict):
|
|
# Convert set back from list, or handle old string format
|
|
if 'enabled_features' in data:
|
|
if isinstance(data['enabled_features'], list):
|
|
data['enabled_features'] = set(data['enabled_features'])
|
|
elif isinstance(data['enabled_features'], str):
|
|
# Handle old string format like "{'bedtime', 'monday_video', 'autonomous'}"
|
|
try:
|
|
# Remove the outer braces and split by comma
|
|
features_str = data['enabled_features'].strip('{}')
|
|
features_list = [f.strip().strip("'\"") for f in features_str.split(',') if f.strip()]
|
|
data['enabled_features'] = set(features_list)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to parse enabled_features string '{data['enabled_features']}': {e}")
|
|
# Fallback to default features
|
|
data['enabled_features'] = {"autonomous", "bedtime", "monday_video"}
|
|
return cls(**data)
|
|
|
|
class ServerManager:
|
|
"""Manages multiple Discord servers with independent configurations"""
|
|
|
|
def __init__(self, config_file: str = "memory/servers_config.json"):
|
|
self.config_file = config_file
|
|
self.servers: Dict[int, ServerConfig] = {}
|
|
self.schedulers: Dict[int, AsyncIOScheduler] = {}
|
|
self.server_memories: Dict[int, Dict] = {}
|
|
self.load_config()
|
|
|
|
def load_config(self):
|
|
"""Load server configurations from file"""
|
|
if os.path.exists(self.config_file):
|
|
try:
|
|
with open(self.config_file, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
for guild_id_str, server_data in data.items():
|
|
guild_id = int(guild_id_str)
|
|
self.servers[guild_id] = ServerConfig.from_dict(server_data)
|
|
self.server_memories[guild_id] = {}
|
|
logger.info(f"Loaded config for server: {server_data['guild_name']} (ID: {guild_id})")
|
|
|
|
# After loading, check if we need to repair the config
|
|
self.repair_config()
|
|
except Exception as e:
|
|
logger.error(f"Failed to load server config: {e}")
|
|
self._create_default_config()
|
|
else:
|
|
self._create_default_config()
|
|
|
|
def repair_config(self):
|
|
"""Repair corrupted configuration data and save it back"""
|
|
try:
|
|
needs_repair = False
|
|
for server in self.servers.values():
|
|
# Check if enabled_features is a string (corrupted)
|
|
if isinstance(server.enabled_features, str):
|
|
needs_repair = True
|
|
logger.info(f"Repairing corrupted enabled_features for server: {server.guild_name}")
|
|
# Re-parse the features
|
|
try:
|
|
features_str = server.enabled_features.strip('{}')
|
|
features_list = [f.strip().strip("'\"") for f in features_str.split(',') if f.strip()]
|
|
server.enabled_features = set(features_list)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to repair enabled_features for {server.guild_name}: {e}")
|
|
server.enabled_features = {"autonomous", "bedtime", "monday_video"}
|
|
|
|
if needs_repair:
|
|
logger.info("Saving repaired configuration...")
|
|
self.save_config()
|
|
except Exception as e:
|
|
logger.error(f"Failed to repair config: {e}")
|
|
|
|
def _create_default_config(self):
|
|
"""Create default configuration for backward compatibility"""
|
|
default_server = ServerConfig(
|
|
guild_id=759889672804630530,
|
|
guild_name="Default Server",
|
|
autonomous_channel_id=761014220707332107,
|
|
autonomous_channel_name="miku-chat",
|
|
bedtime_channel_ids=[761014220707332107],
|
|
enabled_features={"autonomous", "bedtime", "monday_video"},
|
|
autonomous_interval_minutes=10,
|
|
conversation_detection_interval_minutes=3
|
|
)
|
|
self.servers[default_server.guild_id] = default_server
|
|
self.server_memories[default_server.guild_id] = {}
|
|
self.save_config()
|
|
logger.info("Created default server configuration")
|
|
|
|
def save_config(self):
|
|
"""Save server configurations to file"""
|
|
try:
|
|
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
|
|
config_data = {}
|
|
for guild_id, server in self.servers.items():
|
|
# Convert the server config to dict, but handle sets properly
|
|
server_dict = server.to_dict()
|
|
# Convert set to list for JSON serialization
|
|
if 'enabled_features' in server_dict and isinstance(server_dict['enabled_features'], set):
|
|
server_dict['enabled_features'] = list(server_dict['enabled_features'])
|
|
config_data[str(guild_id)] = server_dict
|
|
|
|
with open(self.config_file, "w", encoding="utf-8") as f:
|
|
json.dump(config_data, f, indent=2)
|
|
except Exception as e:
|
|
logger.error(f"Failed to save server config: {e}")
|
|
|
|
def add_server(self, guild_id: int, guild_name: str, autonomous_channel_id: int,
|
|
autonomous_channel_name: str, bedtime_channel_ids: List[int] = None,
|
|
enabled_features: Set[str] = None) -> bool:
|
|
"""Add a new server configuration"""
|
|
if guild_id in self.servers:
|
|
logger.info(f"Server {guild_id} already exists")
|
|
return False
|
|
|
|
if bedtime_channel_ids is None:
|
|
bedtime_channel_ids = [autonomous_channel_id]
|
|
|
|
if enabled_features is None:
|
|
enabled_features = {"autonomous", "bedtime", "monday_video"}
|
|
|
|
server = ServerConfig(
|
|
guild_id=guild_id,
|
|
guild_name=guild_name,
|
|
autonomous_channel_id=autonomous_channel_id,
|
|
autonomous_channel_name=autonomous_channel_name,
|
|
bedtime_channel_ids=bedtime_channel_ids,
|
|
enabled_features=enabled_features
|
|
)
|
|
|
|
self.servers[guild_id] = server
|
|
self.server_memories[guild_id] = {}
|
|
self.save_config()
|
|
logger.info(f"Added new server: {guild_name} (ID: {guild_id})")
|
|
return True
|
|
|
|
def remove_server(self, guild_id: int) -> bool:
|
|
"""Remove a server configuration"""
|
|
if guild_id not in self.servers:
|
|
return False
|
|
|
|
server_name = self.servers[guild_id].guild_name
|
|
del self.servers[guild_id]
|
|
|
|
# Stop and remove scheduler
|
|
if guild_id in self.schedulers:
|
|
self.schedulers[guild_id].shutdown()
|
|
del self.schedulers[guild_id]
|
|
|
|
# Remove memory
|
|
if guild_id in self.server_memories:
|
|
del self.server_memories[guild_id]
|
|
|
|
self.save_config()
|
|
logger.info(f"Removed server: {server_name} (ID: {guild_id})")
|
|
return True
|
|
|
|
def get_server_config(self, guild_id: int) -> Optional[ServerConfig]:
|
|
"""Get configuration for a specific server"""
|
|
return self.servers.get(guild_id)
|
|
|
|
def get_all_servers(self) -> List[ServerConfig]:
|
|
"""Get all server configurations"""
|
|
return list(self.servers.values())
|
|
|
|
def update_server_config(self, guild_id: int, **kwargs) -> bool:
|
|
"""Update configuration for a specific server"""
|
|
if guild_id not in self.servers:
|
|
return False
|
|
|
|
server = self.servers[guild_id]
|
|
for key, value in kwargs.items():
|
|
if hasattr(server, key):
|
|
setattr(server, key, value)
|
|
|
|
self.save_config()
|
|
logger.info(f"Updated config for server: {server.guild_name}")
|
|
return True
|
|
|
|
def get_server_memory(self, guild_id: int, key: str = None):
|
|
"""Get or set server-specific memory"""
|
|
if guild_id not in self.server_memories:
|
|
self.server_memories[guild_id] = {}
|
|
|
|
if key is None:
|
|
return self.server_memories[guild_id]
|
|
|
|
return self.server_memories[guild_id].get(key)
|
|
|
|
def set_server_memory(self, guild_id: int, key: str, value):
|
|
"""Set server-specific memory"""
|
|
if guild_id not in self.server_memories:
|
|
self.server_memories[guild_id] = {}
|
|
|
|
self.server_memories[guild_id][key] = value
|
|
|
|
# ========== Mood Management Methods ==========
|
|
def get_server_mood(self, guild_id: int) -> tuple[str, str]:
|
|
"""Get current mood name and description for a server"""
|
|
if guild_id not in self.servers:
|
|
return "neutral", ""
|
|
|
|
server = self.servers[guild_id]
|
|
return server.current_mood_name, server.current_mood_description
|
|
|
|
def set_server_mood(self, guild_id: int, mood_name: str, mood_description: str = None):
|
|
"""Set mood for a specific server"""
|
|
if guild_id not in self.servers:
|
|
return False
|
|
|
|
server = self.servers[guild_id]
|
|
server.previous_mood_name = server.current_mood_name
|
|
server.current_mood_name = mood_name
|
|
|
|
if mood_description:
|
|
server.current_mood_description = mood_description
|
|
else:
|
|
# Load mood description if not provided
|
|
try:
|
|
from utils.moods import load_mood_description
|
|
server.current_mood_description = load_mood_description(mood_name)
|
|
except Exception as e:
|
|
logger.error(f"Failed to load mood description for {mood_name}: {e}")
|
|
server.current_mood_description = f"I'm feeling {mood_name} today."
|
|
|
|
self.save_config()
|
|
logger.info(f"Server {server.guild_name} mood changed to: {mood_name}")
|
|
logger.debug(f"Mood description: {server.current_mood_description[:100]}{'...' if len(server.current_mood_description) > 100 else ''}")
|
|
return True
|
|
|
|
def get_server_sleep_state(self, guild_id: int) -> bool:
|
|
"""Get sleep state for a specific server"""
|
|
if guild_id not in self.servers:
|
|
return False
|
|
return self.servers[guild_id].is_sleeping
|
|
|
|
def set_server_sleep_state(self, guild_id: int, sleeping: bool):
|
|
"""Set sleep state for a specific server"""
|
|
if guild_id not in self.servers:
|
|
return False
|
|
|
|
server = self.servers[guild_id]
|
|
server.is_sleeping = sleeping
|
|
self.save_config()
|
|
return True
|
|
|
|
def get_server_mood_state(self, guild_id: int) -> dict:
|
|
"""Get complete mood state for a specific server"""
|
|
if guild_id not in self.servers:
|
|
return {}
|
|
|
|
server = self.servers[guild_id]
|
|
return {
|
|
"current_mood_name": server.current_mood_name,
|
|
"current_mood_description": server.current_mood_description,
|
|
"previous_mood_name": server.previous_mood_name,
|
|
"is_sleeping": server.is_sleeping,
|
|
"sleepy_responses_left": server.sleepy_responses_left,
|
|
"forced_angry_until": server.forced_angry_until,
|
|
"just_woken_up": server.just_woken_up
|
|
}
|
|
|
|
def set_server_mood_state(self, guild_id: int, **kwargs):
|
|
"""Set multiple mood state properties for a server"""
|
|
if guild_id not in self.servers:
|
|
return False
|
|
|
|
server = self.servers[guild_id]
|
|
for key, value in kwargs.items():
|
|
if hasattr(server, key):
|
|
setattr(server, key, value)
|
|
|
|
self.save_config()
|
|
return True
|
|
|
|
def setup_server_scheduler(self, guild_id: int, client: discord.Client):
|
|
"""Setup independent scheduler for a specific server"""
|
|
if guild_id not in self.servers:
|
|
logger.warning(f"Cannot setup scheduler for unknown server: {guild_id}")
|
|
return
|
|
|
|
server_config = self.servers[guild_id]
|
|
|
|
# Create new scheduler for this server
|
|
scheduler = AsyncIOScheduler()
|
|
|
|
# Add autonomous speaking job
|
|
if "autonomous" in server_config.enabled_features:
|
|
scheduler.add_job(
|
|
self._run_autonomous_for_server,
|
|
IntervalTrigger(minutes=server_config.autonomous_interval_minutes),
|
|
args=[guild_id, client],
|
|
id=f"autonomous_{guild_id}"
|
|
)
|
|
|
|
# Add autonomous reaction job (parallel to speaking, runs every 20 minutes)
|
|
if "autonomous" in server_config.enabled_features:
|
|
scheduler.add_job(
|
|
self._run_autonomous_reaction_for_server,
|
|
IntervalTrigger(minutes=20),
|
|
args=[guild_id, client],
|
|
id=f"autonomous_reaction_{guild_id}"
|
|
)
|
|
|
|
# Note: Conversation detection is now handled by V2 system via message events
|
|
# No need for separate scheduler job
|
|
|
|
# Add Monday video job
|
|
if "monday_video" in server_config.enabled_features:
|
|
scheduler.add_job(
|
|
self._send_monday_video_for_server,
|
|
CronTrigger(day_of_week='mon', hour=server_config.monday_video_hour, minute=server_config.monday_video_minute),
|
|
args=[guild_id, client],
|
|
id=f"monday_video_{guild_id}"
|
|
)
|
|
|
|
# Add bedtime reminder job
|
|
if "bedtime" in server_config.enabled_features:
|
|
logger.info(f"Setting up bedtime scheduler for server {server_config.guild_name}")
|
|
logger.debug(f" Random time range: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} - {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d}")
|
|
scheduler.add_job(
|
|
self._schedule_random_bedtime_for_server,
|
|
CronTrigger(hour=server_config.bedtime_hour, minute=server_config.bedtime_minute),
|
|
args=[guild_id, client],
|
|
id=f"bedtime_schedule_{guild_id}"
|
|
)
|
|
|
|
# Add mood rotation job (every hour)
|
|
scheduler.add_job(
|
|
self._rotate_server_mood,
|
|
IntervalTrigger(hours=1),
|
|
args=[guild_id, client],
|
|
id=f"mood_rotation_{guild_id}"
|
|
)
|
|
|
|
self.schedulers[guild_id] = scheduler
|
|
scheduler.start()
|
|
logger.info(f"Started scheduler for server: {server_config.guild_name}")
|
|
|
|
def start_all_schedulers(self, client: discord.Client):
|
|
"""Start schedulers for all servers"""
|
|
logger.info("Starting all server schedulers...")
|
|
|
|
for guild_id in self.servers:
|
|
self.setup_server_scheduler(guild_id, client)
|
|
|
|
# Start DM mood rotation scheduler
|
|
self.setup_dm_mood_scheduler(client)
|
|
# Start Figurine DM scheduler
|
|
self.setup_figurine_updates_scheduler(client)
|
|
|
|
logger.info(f"Started {len(self.servers)} server schedulers + DM mood scheduler")
|
|
|
|
def update_server_bedtime_job(self, guild_id: int, client: discord.Client):
|
|
"""Update just the bedtime job for a specific server without restarting all schedulers"""
|
|
server_config = self.servers.get(guild_id)
|
|
if not server_config:
|
|
logger.warning(f"No server config found for guild {guild_id}")
|
|
return False
|
|
|
|
scheduler = self.schedulers.get(guild_id)
|
|
if not scheduler:
|
|
logger.warning(f"No scheduler found for guild {guild_id}")
|
|
return False
|
|
|
|
# Remove existing bedtime job if it exists
|
|
bedtime_job_id = f"bedtime_schedule_{guild_id}"
|
|
try:
|
|
scheduler.remove_job(bedtime_job_id)
|
|
logger.info(f"Removed old bedtime job for server {guild_id}")
|
|
except Exception as e:
|
|
logger.debug(f"No existing bedtime job to remove for server {guild_id}: {e}")
|
|
|
|
# Add new bedtime job with updated configuration
|
|
if "bedtime" in server_config.enabled_features:
|
|
logger.info(f"Updating bedtime scheduler for server {server_config.guild_name}")
|
|
logger.debug(f" New random time range: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} - {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d}")
|
|
scheduler.add_job(
|
|
self._schedule_random_bedtime_for_server,
|
|
CronTrigger(hour=server_config.bedtime_hour, minute=server_config.bedtime_minute),
|
|
args=[guild_id, client],
|
|
id=bedtime_job_id
|
|
)
|
|
logger.info(f"Updated bedtime job for server {server_config.guild_name}")
|
|
return True
|
|
else:
|
|
logger.info(f"Bedtime feature not enabled for server {guild_id}")
|
|
return True
|
|
|
|
def setup_dm_mood_scheduler(self, client: discord.Client):
|
|
"""Setup DM mood rotation scheduler"""
|
|
try:
|
|
from utils.moods import rotate_dm_mood
|
|
|
|
# Create DM mood rotation job (every 2 hours)
|
|
scheduler = AsyncIOScheduler()
|
|
scheduler.add_job(
|
|
rotate_dm_mood,
|
|
IntervalTrigger(hours=2),
|
|
id="dm_mood_rotation"
|
|
)
|
|
|
|
scheduler.start()
|
|
self.schedulers["dm_mood"] = scheduler
|
|
logger.info("DM mood rotation scheduler started (every 2 hours)")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to setup DM mood scheduler: {e}")
|
|
|
|
def _enqueue_figurine_send(self, client: discord.Client):
|
|
"""Enqueue the figurine DM send task in the client's loop."""
|
|
try:
|
|
from utils.figurine_notifier import send_figurine_dm_to_all_subscribers
|
|
if client.loop and client.loop.is_running():
|
|
client.loop.create_task(send_figurine_dm_to_all_subscribers(client))
|
|
logger.debug("Figurine DM send task queued")
|
|
else:
|
|
logger.warning("Client loop not available for figurine DM send")
|
|
except Exception as e:
|
|
logger.error(f"Error enqueuing figurine DM: {e}")
|
|
|
|
def _schedule_one_figurine_send_today(self, scheduler: AsyncIOScheduler, client: discord.Client):
|
|
"""Schedule one figurine DM send at a random non-evening time today (or tomorrow if time passed)."""
|
|
now = datetime.now()
|
|
# Define non-evening hours: 08:00-17:59
|
|
random_hour = random.randint(8, 17)
|
|
random_minute = random.randint(0, 59)
|
|
target_time = now.replace(hour=random_hour, minute=random_minute, second=0, microsecond=0)
|
|
if target_time <= now:
|
|
target_time = target_time + timedelta(days=1)
|
|
logger.info(f"Scheduling figurine DM at {target_time.strftime('%Y-%m-%d %H:%M')} (random non-evening)")
|
|
scheduler.add_job(
|
|
self._enqueue_figurine_send,
|
|
DateTrigger(run_date=target_time),
|
|
args=[client],
|
|
id=f"figurine_dm_{int(target_time.timestamp())}",
|
|
replace_existing=False
|
|
)
|
|
|
|
def setup_figurine_updates_scheduler(self, client: discord.Client):
|
|
"""Create a daily scheduler that schedules one random non-evening figurine DM send per day."""
|
|
try:
|
|
scheduler = AsyncIOScheduler()
|
|
# Every day at 07:30, schedule today's random send (will roll to tomorrow if time passed)
|
|
scheduler.add_job(
|
|
self._schedule_one_figurine_send_today,
|
|
CronTrigger(hour=7, minute=30),
|
|
args=[scheduler, client],
|
|
id="figurine_daily_scheduler"
|
|
)
|
|
# Also schedule one immediately on startup for today/tomorrow
|
|
self._schedule_one_figurine_send_today(scheduler, client)
|
|
scheduler.start()
|
|
self.schedulers["figurine_dm"] = scheduler
|
|
logger.info("Figurine updates scheduler started")
|
|
except Exception as e:
|
|
logger.error(f"Failed to setup figurine updates scheduler: {e}")
|
|
|
|
def stop_all_schedulers(self):
|
|
"""Stop all schedulers"""
|
|
logger.info("Stopping all schedulers...")
|
|
|
|
for scheduler in self.schedulers.values():
|
|
try:
|
|
scheduler.shutdown()
|
|
except Exception as e:
|
|
logger.warning(f"Error stopping scheduler: {e}")
|
|
|
|
self.schedulers.clear()
|
|
logger.info("All schedulers stopped")
|
|
|
|
# Implementation of autonomous functions - these integrate with the autonomous system
|
|
def _run_autonomous_for_server(self, guild_id: int, client: discord.Client):
|
|
"""Run autonomous behavior for a specific server - called by APScheduler"""
|
|
try:
|
|
# V2: Use the new context-aware autonomous system
|
|
from utils.autonomous import autonomous_tick
|
|
# Create an async task in the client's event loop
|
|
if client.loop and client.loop.is_running():
|
|
client.loop.create_task(autonomous_tick(guild_id))
|
|
logger.debug(f"[V2] Autonomous tick queued for server {guild_id}")
|
|
else:
|
|
logger.warning(f"Client loop not available for autonomous tick in server {guild_id}")
|
|
except Exception as e:
|
|
logger.error(f"Error in autonomous tick for server {guild_id}: {e}")
|
|
|
|
def _run_autonomous_reaction_for_server(self, guild_id: int, client: discord.Client):
|
|
"""Run autonomous reaction for a specific server - called by APScheduler"""
|
|
try:
|
|
# V2: Use the new context-aware reaction system
|
|
from utils.autonomous import autonomous_reaction_tick
|
|
# Create an async task in the client's event loop
|
|
if client.loop and client.loop.is_running():
|
|
client.loop.create_task(autonomous_reaction_tick(guild_id))
|
|
logger.debug(f"[V2] Autonomous reaction queued for server {guild_id}")
|
|
else:
|
|
logger.warning(f"Client loop not available for autonomous reaction in server {guild_id}")
|
|
except Exception as e:
|
|
logger.error(f"Error in autonomous reaction for server {guild_id}: {e}")
|
|
|
|
def _run_conversation_detection_for_server(self, guild_id: int, client: discord.Client):
|
|
"""Run conversation detection for a specific server - called by APScheduler"""
|
|
try:
|
|
from utils.autonomous import miku_detect_and_join_conversation_for_server
|
|
# Create an async task in the client's event loop
|
|
if client.loop and client.loop.is_running():
|
|
client.loop.create_task(miku_detect_and_join_conversation_for_server(guild_id))
|
|
logger.debug(f"Conversation detection queued for server {guild_id}")
|
|
else:
|
|
logger.warning(f"Client loop not available for conversation detection in server {guild_id}")
|
|
except Exception as e:
|
|
logger.error(f"Error in conversation detection for server {guild_id}: {e}")
|
|
|
|
def _send_monday_video_for_server(self, guild_id: int, client: discord.Client):
|
|
"""Send Monday video for a specific server - called by APScheduler"""
|
|
try:
|
|
from utils.scheduled import send_monday_video_for_server
|
|
# Create an async task in the client's event loop
|
|
if client.loop and client.loop.is_running():
|
|
client.loop.create_task(send_monday_video_for_server(guild_id))
|
|
logger.debug(f"Monday video queued for server {guild_id}")
|
|
else:
|
|
logger.warning(f"Client loop not available for Monday video in server {guild_id}")
|
|
except Exception as e:
|
|
logger.error(f"Error in Monday video for server {guild_id}: {e}")
|
|
|
|
def _schedule_random_bedtime_for_server(self, guild_id: int, client: discord.Client):
|
|
"""Schedule bedtime reminder for a specific server at a random time within the configured range"""
|
|
logger.info(f"Bedtime scheduler triggered for server {guild_id} at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
|
|
# Get server config to determine the random time range
|
|
server_config = self.servers.get(guild_id)
|
|
if not server_config:
|
|
logger.warning(f"No server config found for guild {guild_id}")
|
|
return
|
|
|
|
# Calculate random time within the bedtime range
|
|
start_minutes = server_config.bedtime_hour * 60 + server_config.bedtime_minute
|
|
end_minutes = server_config.bedtime_hour_end * 60 + server_config.bedtime_minute_end
|
|
|
|
logger.debug(f"Bedtime range calculation: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} ({start_minutes} min) to {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d} ({end_minutes} min)")
|
|
|
|
# Handle case where end time is next day (e.g., 23:30 to 00:30)
|
|
if end_minutes <= start_minutes:
|
|
end_minutes += 24 * 60 # Add 24 hours
|
|
logger.debug(f"Cross-midnight range detected, adjusted end to {end_minutes} minutes")
|
|
|
|
random_minutes = random.randint(start_minutes, end_minutes)
|
|
logger.debug(f"Random time selected: {random_minutes} minutes from midnight")
|
|
|
|
# Convert back to hours and minutes
|
|
random_hour = (random_minutes // 60) % 24
|
|
random_minute = random_minutes % 60
|
|
|
|
# Calculate delay until the random time
|
|
now = datetime.now()
|
|
target_time = now.replace(hour=random_hour, minute=random_minute, second=0, microsecond=0)
|
|
|
|
# If the target time has already passed today, schedule for tomorrow
|
|
if target_time <= now:
|
|
target_time += timedelta(days=1)
|
|
|
|
delay_seconds = (target_time - now).total_seconds()
|
|
|
|
logger.info(f"Random bedtime for server {server_config.guild_name}: {random_hour:02d}:{random_minute:02d} (in {delay_seconds/60:.1f} minutes)")
|
|
|
|
# Schedule the actual bedtime reminder
|
|
try:
|
|
from utils.scheduled import send_bedtime_reminder_for_server
|
|
|
|
def send_bedtime_delayed():
|
|
if client.loop and client.loop.is_running():
|
|
client.loop.create_task(send_bedtime_reminder_for_server(guild_id, client))
|
|
logger.info(f"Random bedtime reminder sent for server {guild_id}")
|
|
else:
|
|
logger.warning(f"Client loop not available for bedtime reminder in server {guild_id}")
|
|
|
|
# Use the scheduler to schedule the delayed bedtime reminder
|
|
scheduler = self.schedulers.get(guild_id)
|
|
if scheduler:
|
|
scheduler.add_job(
|
|
send_bedtime_delayed,
|
|
DateTrigger(run_date=target_time),
|
|
id=f"bedtime_reminder_{guild_id}_{int(target_time.timestamp())}"
|
|
)
|
|
logger.info(f"Bedtime reminder scheduled for server {guild_id} at {target_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
else:
|
|
logger.warning(f"No scheduler found for server {guild_id}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error scheduling bedtime reminder for server {guild_id}: {e}")
|
|
|
|
def _rotate_server_mood(self, guild_id: int, client: discord.Client):
|
|
"""Rotate mood for a specific server - called by APScheduler"""
|
|
try:
|
|
from utils.moods import rotate_server_mood
|
|
# Create an async task in the client's event loop
|
|
if client.loop and client.loop.is_running():
|
|
client.loop.create_task(rotate_server_mood(guild_id))
|
|
logger.debug(f"Mood rotation queued for server {guild_id}")
|
|
else:
|
|
logger.warning(f"Client loop not available for mood rotation in server {guild_id}")
|
|
except Exception as e:
|
|
logger.error(f"Error in mood rotation for server {guild_id}: {e}")
|
|
|
|
# Global instance
|
|
server_manager = ServerManager()
|