import json import logging import discord import websockets from pydactyl import PterodactylClient, exceptions from redbot.core import Config, commands from redbot.core.bot import Red class Pterodactyl(commands.Cog): """Pterodactyl allows you to manage your Pterodactyl Panel from Discord.""" def __init__(self, bot: Red): self.bot = bot self.config = Config.get_conf(self, identifier=457581387213637448123567, force_registration=True) self.config.register_global( base_url=None, api_key=None, server_id=None, console_channel=None, startup_jar=None, startup_arguments=None, power_action_in_progress=False ) self.logger = logging.getLogger('red.sea.pterodactyl') self.client = None self.task = None self.websocket = None async def establish_websocket_connection(self): self.logger.debug("Establishing WebSocket connection") base_url = await self.config.base_url() api_key = await self.config.api_key() server_id = await self.config.server_id() extra_headers = { "origin": base_url } try: client = PterodactylClient(base_url, api_key, debug=True).client websocket_credentials = client.servers.get_websocket(server_id) self.logger.debug("Websocket connection details retrieved:\nSocket: %s\nToken: %s", websocket_credentials['data']['socket'], websocket_credentials['data']['token']) except exceptions.ClientConfigError as e: self.logger.error('Failed to initialize Pterodactyl client: %s', e) return except exceptions.PterodactylApiError as e: self.logger.error('Failed to retrieve Pterodactyl websocket: %s', e) return async for websocket in websockets.connect(websocket_credentials['data']['socket'], extra_headers=extra_headers): try: self.logger.debug("WebSocket connection established") auth_message = json.dumps({"event": "auth", "args": [websocket_credentials['data']['token']]}) await websocket.send(auth_message) self.logger.debug("Authentication message sent") self.client = client self.websocket = websocket while True: message = await websocket.recv() if json.loads(message)['event'] in ['token expiring', 'token expired']: self.logger.debug("Received token expiring/expired event. Refreshing token.") websocket_credentials = client.servers.get_websocket(server_id) auth_message = json.dumps({"event": "auth", "args": [websocket_credentials['data']['token']]}) await websocket.send(auth_message) self.logger.debug("Authentication message sent") if json.loads(message)['event'] == 'auth success': self.logger.debug("Authentication successful") if json.loads(message)['event'] == 'console output' and await self.config.console_channel() is not None: channel = self.bot.get_channel(await self.config.console_channel) await channel.send(json.loads(message)['args'][0]) #FIXME - Add pagification for long messages to prevent Discord API errors except websockets.exceptions.ConnectionClosed as e: self.logger.debug("WebSocket connection closed: %s", e) websocket_credentials = client.servers.get_websocket(server_id) continue def get_task(self): return self.bot.loop.create_task(self.establish_websocket_connection(), name="Pterodactyl Websocket Connection") async def cog_load(self): self.task = self.get_task() async def cog_unload(self): self.task.cancel() await self.client._session.close() # pylint: disable=protected-access @commands.cog.listener() async def on_message(self, message: discord.Message): if message.channel.id == await self.config.console_channel(): self.websocket.send(json.dumps({"event": "send command", "args": [message.content]})) @commands.group(autohelp = True, name = "pterodactyl", aliases = ["ptero"]) async def pterodactyl(self, ctx: commands.Context): """Pterodactyl allows you to manage your Pterodactyl Panel from Discord.""" @pterodactyl.group(autohelp = True, name = "config", aliases = ["settings", "set"]) async def pterodactyl_config(self, ctx: commands.Context): """Configure Pterodactyl settings.""" @pterodactyl_config.command(name = "url") async def pterodactyl_config_base_url(self, ctx: commands.Context, base_url: str): """Set the base URL of your Pterodactyl Panel. Please include the protocol (http/https).""" await self.config.base_url.set(base_url) await ctx.send(f"Base URL set to {base_url}") self.logger.debug("Configuration value set: base_url = %s\nRestarting task...", base_url) self.task.cancel() self.task = self.get_task() @pterodactyl_config.command(name = "apikey") async def pterodactyl_config_api_key(self, ctx: commands.Context, api_key: str): """Set the API key for your Pterodactyl Panel.""" await self.config.api_key.set(api_key) await ctx.send(f"API key set to `{api_key[:5]}...{api_key[-4:]}`") self.logger.debug("Configuration value set: api_key = %s\nRestarting task...", api_key) self.task.cancel() self.task = self.get_task() @pterodactyl_config.command(name = "serverid") async def pterodactyl_config_server_id(self, ctx: commands.Context, server_id: str): """Set the server ID for your Pterodactyl Panel.""" await self.config.server_id.set(server_id) await ctx.send(f"Server ID set to {server_id}") self.logger.debug("Configuration value set: server_id = %s\nRestarting task...", server_id) self.task.cancel() self.task = self.get_task()