import asyncio from typing import Optional, Dict from fastapi import FastAPI, HTTPException from pydantic import BaseModel import discord from discord.ext import commands from NoiseGatev2 import NoiseGate import op25_controller import pulse # Define FastAPI app app = FastAPI() # Discord Bot Setup intents = discord.Intents.default() intents.voice_states = True intents.guilds = True # Models for API requests class BotConfig(BaseModel): token: str # Discord Bot Token class VoiceChannelRequest(BaseModel): guild_id: int channel_id: int # Discord Bot Manager class DiscordBotManager: def __init__(self): self.bot: Optional[commands.Bot] = None self.bot_task: Optional[asyncio.Task] = None self.voice_clients: Dict[int, discord.VoiceClient] = {} self.token: Optional[str] = None self.loop = asyncio.get_event_loop() self.lock = asyncio.Lock() async def start_bot(self, token: str): async with self.lock: if self.bot and self.bot.is_closed(): raise RuntimeError("Bot is already running.") if self.bot_task and not self.bot_task.done(): raise RuntimeError("Bot is already running.") self.token = token self.bot = commands.Bot(command_prefix="!", intents=intents) @self.bot.event async def on_ready(): print(f'Logged in as {self.bot.user}') # Handle graceful shutdown when all voice connections are closed @self.bot.event async def on_voice_state_update(member, before, after): # Check if all voice clients are disconnected await asyncio.sleep(1) # Give time for the state to update if not self.voice_clients: await self.stop_bot() # Start the bot in the background self.bot_task = self.loop.create_task(self.bot.start(token)) async def stop_bot(self): async with self.lock: if self.bot: await self.bot.close() self.bot = None if self.bot_task: await self.bot_task self.bot_task = None self.voice_clients.clear() print("Bot has been stopped.") async def join_voice_channel(self, guild_id: int, channel_id: int, ng_threshold: int = 50, device_id: int = 4): if not self.bot: raise RuntimeError("Bot is not running.") guild = self.bot.get_guild(guild_id) if not guild: raise ValueError("Guild not found.") channel = guild.get_channel(channel_id) if not isinstance(channel, discord.VoiceChannel): raise ValueError("Channel is not a voice channel.") if guild_id in self.voice_clients: raise RuntimeError("Already connected to this guild's voice channel.") voice_client = await channel.connect() streamHandler = NoiseGate( _input_device_index=device_id, _voice_connection=voice_client, _noise_gate_threshold=ng_threshold) # Start the audio stream streamHandler.run() self.voice_clients[guild_id] = voice_client print(f"Joined guild {guild_id} voice channel {channel_id}.") async def leave_voice_channel(self, guild_id: int): if not self.bot: raise RuntimeError("Bot is not running.") voice_client = self.voice_clients.get(guild_id) if not voice_client: raise RuntimeError("Not connected to the specified guild's voice channel.") await voice_client.disconnect() del self.voice_clients[guild_id] print(f"Left guild {guild_id} voice channel.") # Initialize Discord Bot Manager bot_manager = DiscordBotManager() # API Endpoints @app.post("/start_bot") async def start_bot(config: BotConfig): try: await bot_manager.start_bot(config.token) return {"status": "Bot started successfully."} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/stop_bot") async def stop_bot(): try: await bot_manager.stop_bot() return {"status": "Bot stopped successfully."} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/join_voice") async def join_voice_channel(request: VoiceChannelRequest): try: await bot_manager.join_voice_channel(request.guild_id, request.channel_id) return {"status": f"Joined guild {request.guild_id} voice channel {request.channel_id}."} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/leave_voice") async def leave_voice_channel(request: VoiceChannelRequest): try: await bot_manager.leave_voice_channel(request.guild_id) return {"status": f"Left guild {request.guild_id} voice channel."} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.get("/status") async def get_status(): status = { "bot_running": bot_manager.bot is not None and not bot_manager.bot.is_closed(), "connected_guilds": list(bot_manager.voice_clients.keys()) } return status app.include_router(op25_controller.router, prefix="/op25") app.include_router(pulse.router, prefix="/pulse")