655 lines
29 KiB
Python
655 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
|
||
|
||
@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:
|
||
print(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] = {}
|
||
print(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:
|
||
print(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
|
||
print(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:
|
||
print(f"⚠️ Failed to repair enabled_features for {server.guild_name}: {e}")
|
||
server.enabled_features = {"autonomous", "bedtime", "monday_video"}
|
||
|
||
if needs_repair:
|
||
print("🔧 Saving repaired configuration...")
|
||
self.save_config()
|
||
except Exception as e:
|
||
print(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()
|
||
print("📋 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:
|
||
print(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:
|
||
print(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()
|
||
print(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()
|
||
print(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()
|
||
print(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:
|
||
print(f"⚠️ Failed to load mood description for {mood_name}: {e}")
|
||
server.current_mood_description = f"I'm feeling {mood_name} today."
|
||
|
||
self.save_config()
|
||
print(f"😊 Server {server.guild_name} mood changed to: {mood_name}")
|
||
print(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:
|
||
print(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:
|
||
print(f"⏰ Setting up bedtime scheduler for server {server_config.guild_name}")
|
||
print(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()
|
||
print(f"⏰ Started scheduler for server: {server_config.guild_name}")
|
||
|
||
def start_all_schedulers(self, client: discord.Client):
|
||
"""Start schedulers for all servers"""
|
||
print("🚀 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)
|
||
|
||
print(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:
|
||
print(f"⚠️ No server config found for guild {guild_id}")
|
||
return False
|
||
|
||
scheduler = self.schedulers.get(guild_id)
|
||
if not scheduler:
|
||
print(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)
|
||
print(f"🗑️ Removed old bedtime job for server {guild_id}")
|
||
except Exception as e:
|
||
print(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:
|
||
print(f"⏰ Updating bedtime scheduler for server {server_config.guild_name}")
|
||
print(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
|
||
)
|
||
print(f"✅ Updated bedtime job for server {server_config.guild_name}")
|
||
return True
|
||
else:
|
||
print(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
|
||
print("🔄 DM mood rotation scheduler started (every 2 hours)")
|
||
|
||
except Exception as e:
|
||
print(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))
|
||
print("✅ Figurine DM send task queued")
|
||
else:
|
||
print("⚠️ Client loop not available for figurine DM send")
|
||
except Exception as e:
|
||
print(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)
|
||
print(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
|
||
print("🗓️ Figurine updates scheduler started")
|
||
except Exception as e:
|
||
print(f"❌ Failed to setup figurine updates scheduler: {e}")
|
||
|
||
def stop_all_schedulers(self):
|
||
"""Stop all schedulers"""
|
||
print("🛑 Stopping all schedulers...")
|
||
|
||
for scheduler in self.schedulers.values():
|
||
try:
|
||
scheduler.shutdown()
|
||
except Exception as e:
|
||
print(f"⚠️ Error stopping scheduler: {e}")
|
||
|
||
self.schedulers.clear()
|
||
print("✅ 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))
|
||
print(f"✅ [V2] Autonomous tick queued for server {guild_id}")
|
||
else:
|
||
print(f"⚠️ Client loop not available for autonomous tick in server {guild_id}")
|
||
except Exception as e:
|
||
print(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))
|
||
print(f"✅ [V2] Autonomous reaction queued for server {guild_id}")
|
||
else:
|
||
print(f"⚠️ Client loop not available for autonomous reaction in server {guild_id}")
|
||
except Exception as e:
|
||
print(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))
|
||
print(f"✅ Conversation detection queued for server {guild_id}")
|
||
else:
|
||
print(f"⚠️ Client loop not available for conversation detection in server {guild_id}")
|
||
except Exception as e:
|
||
print(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))
|
||
print(f"✅ Monday video queued for server {guild_id}")
|
||
else:
|
||
print(f"⚠️ Client loop not available for Monday video in server {guild_id}")
|
||
except Exception as e:
|
||
print(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"""
|
||
print(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:
|
||
print(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
|
||
|
||
print(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
|
||
print(f"🌙 Cross-midnight range detected, adjusted end to {end_minutes} minutes")
|
||
|
||
random_minutes = random.randint(start_minutes, end_minutes)
|
||
print(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()
|
||
|
||
print(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))
|
||
print(f"✅ Random bedtime reminder sent for server {guild_id}")
|
||
else:
|
||
print(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())}"
|
||
)
|
||
print(f"✅ Bedtime reminder scheduled for server {guild_id} at {target_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||
else:
|
||
print(f"⚠️ No scheduler found for server {guild_id}")
|
||
|
||
except Exception as e:
|
||
print(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))
|
||
print(f"✅ Mood rotation queued for server {guild_id}")
|
||
else:
|
||
print(f"⚠️ Client loop not available for mood rotation in server {guild_id}")
|
||
except Exception as e:
|
||
print(f"⚠️ Error in mood rotation for server {guild_id}: {e}")
|
||
|
||
# Global instance
|
||
server_manager = ServerManager()
|