import asyncio import logging import discord from discord.ext import commands from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import Optional, Dict from NoiseGatev2 import NoiseGate import op25_controller import pulse # Initialize logging logging.basicConfig(level=logging.INFO) LOGGER = logging.getLogger(__name__) # Define FastAPI app app = FastAPI() intents = discord.Intents.default() intents.voice_states = True intents.guilds = True class BotConfig(BaseModel): token: str class VoiceChannelRequest(BaseModel): guild_id: int channel_id: int 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 not 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(): LOGGER.info(f'Logged in as {self.bot.user}') @self.bot.event async def on_voice_state_update(member, before, after): # Check if the bot was disconnected if member == self.bot.user and after.channel is None: guild_id = before.channel.guild.id LOGGER.info(f"Bot was disconnected from channel in guild {guild_id}. Attempting to reconnect...") try: leave_voice_channel(guild_id) except Exception as e: LOGGER.warning(f"Error to leave voice channel", e) # Attempt to reconnect to the channel after a brief pause await asyncio.sleep(2) await self.join_voice_channel(guild_id, before.channel.id) 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() LOGGER.info("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.") try: voice_client = await channel.connect(timeout=60.0, reconnect=True) streamHandler = NoiseGate( _input_device_index=device_id, _voice_connection=voice_client, _noise_gate_threshold=ng_threshold) streamHandler.run() self.voice_clients[guild_id] = voice_client LOGGER.info(f"Joined guild {guild_id} voice channel {channel_id}.") except Exception as e: LOGGER.error(f"Failed to connect to voice channel: {e}") 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] LOGGER.info(f"Left guild {guild_id} voice channel.") 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: LOGGER.error(f"Error starting bot: {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: LOGGER.error(f"Error stopping bot: {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: LOGGER.error(f"Error joining voice channel: {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: LOGGER.error(f"Error leaving voice channel: {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")