From f34241acaccc64d653eb54b124928baf769b187d Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sat, 24 May 2025 02:09:35 -0400 Subject: [PATCH] Init --- .gitignore | 0 Dockerfile | 19 ++ Makefile | 32 +++ app/bot.py | 168 ++++++++++++++ app/commands/drb.py | 97 ++++++++ app/internal/drb_srv_api.py | 425 ++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + 7 files changed, 743 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 app/bot.py create mode 100644 app/commands/drb.py create mode 100644 app/internal/drb_srv_api.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ba62921 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# Use an official Python runtime as a parent image +FROM python:3.13-slim + +# Set the working directory in the container +WORKDIR /app + +# Move the requirements file +COPY ./requirements.txt . + +# Install any needed packages specified in requirements.txt +# Make sure you have a requirements.txt file in the same directory as your Dockerfile +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the current directory contents into the container at /usr/src/app +COPY ./app/ . + +# Run bot.py when the container launches +# Use the exec form to avoid issues with signal handling +CMD ["python", "bot.py"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..48db145 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +# Define the name for your Docker image +IMAGE_NAME = drb_server_discord_bot + +# Default target when you just run 'make' +all: build + +# Target to build the Docker image +build: + docker build -t $(IMAGE_NAME) . + +# Target to run the Docker container +# Requires the DISCORD_BOT_TOKEN environment variable to be set +run: build + docker run -it --rm --name $(IMAGE_NAME)_container --network=host -e DISCORD_BOT_TOKEN="${DISCORD_BOT_TOKEN}" $(IMAGE_NAME) + +# Target to stop the running container +stop: + docker stop $(IMAGE_NAME)_container || true + +# Target to remove the container +remove: + docker rm $(IMAGE_NAME)_container || true + +# Target to clean up the built image +clean: + docker rmi $(IMAGE_NAME) || true + +# Target to stop, remove, and clean everything +clean_all: stop remove clean + +# Phony targets to avoid conflicts with files of the same name +.PHONY: all build run stop remove clean clean_all diff --git a/app/bot.py b/app/bot.py new file mode 100644 index 0000000..578fc1c --- /dev/null +++ b/app/bot.py @@ -0,0 +1,168 @@ +import discord +from discord.ext import commands +import os +import logging + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +# Define intents +# You might need to adjust these based on the features your bot uses +intents = discord.Intents.default() +intents.message_content = True # Required to read message content in most cases +intents.members = True # Required for member-related events and fetching members +intents.presences = True + +# Initialize the bot +# command_prefix is the character(s) that trigger bot commands (e.g., !command) +bot = commands.Bot(command_prefix='!', intents=intents) + +# --- Event Handlers --- + +@bot.event +async def on_ready(): + """Logs when the bot is ready and connected to Discord.""" + logging.info(f'Logged in as {bot.user.name} ({bot.user.id})') + logging.info('------') + + logging.info('Loading Modules') + logging.info('------') + await load_extensions() # Load command modules when the bot is ready + + logging.info('Registering slash commands') + logging.info('------') + for server in bot.guilds: + num_synced = await bot.tree.sync(guild=discord.Object(id=server.id)) + logging.info(f"Registered {num_synced} commands for server ID: '{server.id}'") + +@bot.event +async def on_command_error(ctx, error): + """Handles errors that occur when running commands.""" + if isinstance(error, commands.CommandNotFound): + # Ignore CommandNotFound errors to avoid spamming the console/chat + return + elif isinstance(error, commands.MissingRequiredArgument): + await ctx.send(f'Error: Missing required argument(s). Usage: `{ctx.command.usage or "No usage info provided."}`') + elif isinstance(error, commands.BadArgument): + await ctx.send(f'Error: Invalid argument(s) provided. Usage: `{ctx.command.usage or "No usage info provided."}`') + elif isinstance(error, commands.MissingPermissions): + await ctx.send("Error: You don't have the necessary permissions to run this command.") + elif isinstance(error, commands.BotMissingPermissions): + await ctx.send("Error: I don't have the necessary permissions to run this command.") + else: + # Log other errors for debugging + logging.error(f'Ignoring exception in command {ctx.command}:', exc_info=error) + await ctx.send(f'An unexpected error occurred: {error}') + +# --- Extension (Command Module) Loading --- + +async def load_extensions(): + """Loads all command modules from the 'commands' directory.""" + logging.info("Attempting to load extensions...") + commands_dir = 'commands' + if not os.path.exists(commands_dir): + logging.warning(f"'{commands_dir}' directory not found. No commands will be loaded.") + return + + for filename in os.listdir(commands_dir): + # Check if the file is a Python file and not a hidden file + if filename.endswith('.py') and not filename.startswith('_'): + # Construct the module path (e.g., commands.example) + module_name = f'{commands_dir}.{filename[:-3]}' + try: + await bot.load_extension(module_name) + logging.info(f'Successfully loaded extension: {module_name}') + except Exception as e: + logging.error(f'Failed to load extension {module_name}: {e}', exc_info=True) + +async def unload_extensions(): + """Unloads all loaded command modules.""" + logging.info("Attempting to unload extensions...") + for extension in list(bot.extensions): # Iterate over a copy as unloading modifies the list + try: + await bot.unload_extension(extension) + logging.info(f'Successfully unloaded extension: {extension}') + except Exception as e: + logging.error(f'Failed to unload extension {extension}: {e}', exc_info=True) + +async def reload_extensions(): + """Reloads all loaded command modules.""" + logging.info("Attempting to reload extensions...") + await unload_extensions() + await load_extensions() + logging.info("Extensions reloaded.") + +# --- Admin Commands (Optional, for managing extensions) --- +# You might want to restrict these commands to specific users or roles + +@bot.command(name='load', hidden=True) +@commands.is_owner() # Requires the user to be the bot owner (set via application settings) +async def load_command(ctx, extension_name: str): + """Loads a specific command extension.""" + module_name = f'commands.{extension_name}' + try: + await bot.load_extension(module_name) + await ctx.send(f'Successfully loaded extension: `{module_name}`') + logging.info(f'Manual load: Successfully loaded extension: {module_name}') + except commands.ExtensionAlreadyLoaded: + await ctx.send(f'Extension `{module_name}` is already loaded.') + except commands.ExtensionNotFound: + await ctx.send(f'Extension `{module_name}` not found.') + except Exception as e: + await ctx.send(f'Failed to load extension `{module_name}`: {e}') + logging.error(f'Manual load: Failed to load extension {module_name}: {e}', exc_info=True) + +@bot.command(name='unload', hidden=True) +@commands.is_owner() +async def unload_command(ctx, extension_name: str): + """Unloads a specific command extension.""" + module_name = f'commands.{extension_name}' + try: + await bot.unload_extension(module_name) + await ctx.send(f'Successfully unloaded extension: `{module_name}`') + logging.info(f'Manual unload: Successfully unloaded extension: {module_name}') + except commands.ExtensionNotFound: + await ctx.send(f'Extension `{module_name}` not found or not loaded.') + except Exception as e: + await ctx.send(f'Failed to unload extension `{module_name}`: {e}') + logging.error(f'Manual unload: Failed to unload extension {module_name}: {e}', exc_info=True) + +@bot.command(name='reload', hidden=True) +@commands.is_owner() +async def reload_command(ctx, extension_name: str = None): + """Reloads a specific command extension or all extensions.""" + if extension_name: + module_name = f'commands.{extension_name}' + try: + await bot.reload_extension(module_name) + await ctx.send(f'Successfully reloaded extension: `{module_name}`') + logging.info(f'Manual reload: Successfully reloaded extension: {module_name}') + except commands.ExtensionNotFound: + await ctx.send(f'Extension `{module_name}` not found or not loaded.') + except Exception as e: + await ctx.send(f'Failed to reload extension `{module_name}`: {e}') + logging.error(f'Manual reload: Failed to reload extension {module_name}: {e}', exc_info=True) + else: + # Reload all extensions if no specific name is provided + await reload_extensions() + await ctx.send('All extensions reloaded.') + + +# --- Running the Bot --- + +if __name__ == "__main__": + # Get the bot token from environment variables + # It's recommended to use environment variables for sensitive information + DISCORD_BOT_TOKEN = os.getenv('DISCORD_BOT_TOKEN') + + if DISCORD_BOT_TOKEN is None: + logging.error("DISCORD_BOT_TOKEN environment variable not set.") + logging.error("Please set the DISCORD_BOT_TOKEN environment variable with your bot's token.") + else: + try: + # Run the bot + bot.run(DISCORD_BOT_TOKEN) + except discord.errors.LoginFailure: + logging.error("Invalid Discord bot token provided. Please check your token.") + except Exception as e: + logging.error(f"An error occurred while running the bot: {e}", exc_info=True) diff --git a/app/commands/drb.py b/app/commands/drb.py new file mode 100644 index 0000000..86d6069 --- /dev/null +++ b/app/commands/drb.py @@ -0,0 +1,97 @@ +import discord +from discord.ext import commands +from discord import app_commands # Import the app_commands module +from internal.drb_srv_api import SystemAPIWrapper, NodeAPIWrapper + +# This is a standard setup for a command module (often called a "Cog") +# It inherits from commands.Cog +class DrbCommands(commands.Cog): + """A collection of example slash commands.""" + + # The __init__ method takes the bot instance as an argument + def __init__(self, bot): + self.bot = bot + self.drb_sys_api = SystemAPIWrapper() + self.drb_node_api = NodeAPIWrapper() + + # Connect a bot with the selected system to the channel the requester is in / move the bot to the channel the requester is in + @commands.hybrid_command(name="join", description="Request a bot to join your channel listening to a system.") + @app_commands.describe(system_name="The name of the system to join.") + async def join(self, ctx: commands.Context, system_name:str): + # Get the system details + sys_search_results = await self.drb_sys_api.search_systems(name=system_name) + + selected_system = None + # Make sure there is a system found + if len(sys_search_results) == 1: + selected_system = sys_search_results[0] + else: + pass # Replace with code to ask the user which they would like + + if not selected_system: return + + # Get all the nodes that have this system + avail_on_nodes = selected_system['avail_on_nodes'] + + selected_node_id = None + if len(avail_on_nodes) == 0: + return + if len(avail_on_nodes) == 1: + selected_node_id = avail_on_nodes[0] + else: + return # TODO - Implement this + # Check to see if there is a preferred node for the system, if not select one + # check to make sure it's available, if not loop back and select the next one + + if not selected_node_id: return + + # Get the channel the user is currently in + channel_id = ctx.author.voice.channel.id + + # Get the guild the user messaged from + guild_id = ctx.guild.id + + print(selected_node_id, selected_system['_id'], guild_id, channel_id) + + api_response = await self.drb_node_api.join(selected_node_id, selected_system['_id'], guild_id, channel_id) + + if isinstance(ctx, commands.Context): + # This was invoked as a prefix command (e.g., !hello) + await ctx.send(f"{api_response}!") + elif isinstance(ctx, discord.Interaction): + # This was invoked as a slash command (/hello) + await ctx.response.send_message(f"{api_response}!") + + @join.autocomplete("system_name") + async def system_name_autocomplete(self, + interaction: discord.Interaction, + current: str, + ) -> list[app_commands.Choice[str]]: + """ + Autocomplete for system_name, fetches systems. + """ + # Fetch systems from your database (async call) + systems = await self.drb_sys_api.search_systems(name=current) + + # Create app_commands.Choice objects + return [ + app_commands.Choice(name=system, value=system) + for system in systems[:25] # Discord has a limit of 25 choices + ] + + @commands.hybrid_command(name="leave", description="Request that a bot leave your server.") + async def leave(self, ctx: commands.Context): + if isinstance(ctx, commands.Context): + # This was invoked as a prefix command (e.g., !hello) + await ctx.send(f"Hello, {ctx.author.display_name}!") + elif isinstance(ctx, discord.Interaction): + # This was invoked as a slash command (/hello) + await ctx.response.send_message(f"Hello, {ctx.user.display_name}!") + + # Disconnect the selected bot from the server the requster is in + +# --- Setup Function --- +async def setup(bot): + """Adds the SlashExampleCommands cog to the bot and syncs slash commands.""" + await bot.add_cog(DrbCommands(bot)) + print("Hybrid commands cog loaded. Remember to sync commands!") \ No newline at end of file diff --git a/app/internal/drb_srv_api.py b/app/internal/drb_srv_api.py new file mode 100644 index 0000000..ca96af6 --- /dev/null +++ b/app/internal/drb_srv_api.py @@ -0,0 +1,425 @@ +import requests +import json +import os + +class CoreAPI: + def _handle_response(self, response): + """ + Handles the API response, checking for successful status codes and + raising exceptions for errors. + + Args: + response (requests.Response): The response object from a requests call. + + Returns: + dict or list: The JSON response body if the request was successful. + + Raises: + requests.exceptions.HTTPError: If the response status code indicates an error. + requests.exceptions.RequestException: For other request-related errors. + json.JSONDecodeError: If the response body is not valid JSON. + """ + try: + response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx) + if response.content: + return response.json() + return None # Handle cases with no response body (e.g., 204 No Content) + except requests.exceptions.RequestException as e: + print(f"Request failed: {e}") + raise + except json.JSONDecodeError: + print(f"Failed to decode JSON from response: {response.text}") + raise + + +class NodeAPIWrapper(CoreAPI): + """ + A wrapper class for interacting with the Node API endpoints. + """ + def __init__(self): + """ + Initializes the API wrapper with the base URL of the Quart application. + + Args: + base_url (str): The base URL of your Quart application (e.g., "http://localhost:5000"). + """ + self.base_url = f"{os.getenv('API_BASE_URL', 'http://localhost:5000')}/nodes" + + async def get_nodes(self) -> requests.Response: + """ + API endpoint to list currently connected client IDs. + + Returns: + requests.Response: The response object from the API. + On success (200), response.json() will be a list of client IDs. + """ + url = self.base_url + try: + response = requests.get(url) + return self._handle_response(response) + except (requests.exceptions.RequestException, json.JSONDecodeError) as e: + print(f"Error creating system: {e}") + raise + + async def join(self, client_id: str, system_id: str, guild_id: str, channel_id: str) -> requests.Response: + """ + Send a join command to the specific system specified. + + Args: + client_id (str): The ID of the client to send the command to. + system_id (str): The system ID for the join command. + guild_id (str): The guild ID for the join command. + channel_id (str): The channel ID for the join command. + + Returns: + requests.Response: The response object from the API. + On success (200), response.json() will contain status information. + """ + url = f"{self.base_url}/join" + payload = { + "client_id": client_id, + "system_id": system_id, + "guild_id": guild_id, + "channel_id": channel_id + } + try: + response = requests.post(url, json=payload) + return self._handle_response(response) + except (requests.exceptions.RequestException, json.JSONDecodeError) as e: + print(f"Error creating system: {e}") + raise + + async def leave(self, client_id: str, guild_id: str) -> requests.Response: + """ + Send a leave command to the specific client. + + Args: + client_id (str): The ID of the client to send the command to. + guild_id (str): The guild ID for the leave command. + + Returns: + requests.Response: The response object from the API. + On success (200), response.json() will contain status information. + """ + url = f"{self.base_url}/leave" + payload = { + "client_id": client_id, + "guild_id": guild_id + } + try: + response = requests.post(url, json=payload) + return self._handle_response(response) + except (requests.exceptions.RequestException, json.JSONDecodeError) as e: + print(f"Error creating system: {e}") + raise + + +class SystemAPIWrapper(CoreAPI): + """ + A Python wrapper class for interacting with the System API endpoints + using the requests library. + """ + + def __init__(self): + """ + Initializes the SystemAPIWrapper with the base URL of the API. + + Args: + base_url (str): The base URL of the System API (e.g., "http://localhost:5000/systems"). + """ + self.base_url = f"{os.getenv('API_BASE_URL', 'http://localhost:5000')}/systems" + + async def create_system(self, system_data): + """ + Creates a new system. + + Args: + system_data (dict): A dictionary containing the system data. + + Returns: + dict: The created system object. + + Raises: + requests.exceptions.RequestException: If the request fails. + json.JSONDecodeError: If the response is not valid JSON. + """ + url = f"{self.base_url}/" + try: + response = requests.post(url, json=system_data) + return self._handle_response(response) + except (requests.exceptions.RequestException, json.JSONDecodeError) as e: + print(f"Error creating system: {e}") + raise + + async def list_systems(self): + """ + Retrieves a list of all systems. + + Returns: + list: A list of system objects. + + Raises: + requests.exceptions.RequestException: If the request fails. + json.JSONDecodeError: If the response is not valid JSON. + """ + url = f"{self.base_url}/" + try: + response = requests.get(url) + return self._handle_response(response) + except (requests.exceptions.RequestException, json.JSONDecodeError) as e: + print(f"Error listing systems: {e}") + raise + + async def get_system_by_id(self, system_id): + """ + Retrieves details for a specific system by ID. + + Args: + system_id (str): The ID of the system. + + Returns: + dict: The system object. + + Raises: + requests.exceptions.RequestException: If the request fails (e.g., 404 if not found). + json.JSONDecodeError: If the response is not valid JSON. + """ + url = f"{self.base_url}/{system_id}" + try: + response = requests.get(url) + return self._handle_response(response) + except (requests.exceptions.RequestException, json.JSONDecodeError) as e: + print(f"Error getting system by ID {system_id}: {e}") + raise + + async def get_systems_by_client_id(self, client_id): + """ + Retrieves a list of systems available on a specific client ID. + + Args: + client_id (str): The ID of the client. + + Returns: + list: A list of system objects. + + Raises: + requests.exceptions.RequestException: If the request fails (e.g., 404 if client not found or no systems). + json.JSONDecodeError: If the response is not valid JSON. + """ + url = f"{self.base_url}/client/{client_id}" + try: + response = requests.get(url) + return self._handle_response(response) + except (requests.exceptions.RequestException, json.JSONDecodeError) as e: + print(f"Error getting systems by client ID {client_id}: {e}") + raise + + async def update_system(self, system_id, updated_system_data): + """ + Updates a specific system by ID. + + Args: + system_id (str): The ID of the system to update. + updated_system_data (dict): A dictionary containing the updated system data. + + Returns: + dict: The updated system object. + + Raises: + requests.exceptions.RequestException: If the request fails. + json.JSONDecodeError: If the response is not valid JSON. + """ + url = f"{self.base_url}/{system_id}" + try: + response = requests.put(url, json=updated_system_data) + return self._handle_response(response) + except (requests.exceptions.RequestException, json.JSONDecodeError) as e: + print(f"Error updating system {system_id}: {e}") + raise + + async def delete_system(self, system_id): + """ + Deletes a specific system by ID. + + Args: + system_id (str): The ID of the system to delete. + + Returns: + dict: The deleted system object. + + Raises: + requests.exceptions.RequestException: If the request fails. + json.JSONDecodeError: If the response is not valid JSON. + """ + url = f"{self.base_url}/{system_id}" + try: + response = requests.delete(url) + return self._handle_response(response) + except (requests.exceptions.RequestException, json.JSONDecodeError) as e: + print(f"Error deleting system {system_id}: {e}") + raise + + async def search_systems(self, **kwargs): + """ + Searches for systems based on provided query parameters. + + Args: + **kwargs: Keyword arguments representing the query parameters + (e.g., name="MySystem", frequency_khz=1000). + + Returns: + list: A list of system objects matching the criteria. + + Raises: + requests.exceptions.RequestException: If the request fails. + json.JSONDecodeError: If the response is not valid JSON. + """ + url = f"{self.base_url}/search" + try: + # requests.get automatically handles the params dictionary + response = requests.get(url, params=kwargs) + return self._handle_response(response) + except (requests.exceptions.RequestException, json.JSONDecodeError) as e: + print(f"Error searching systems with parameters {kwargs}: {e}") + raise + + async def assign_client_to_system(self, system_id, client_id): + """ + Assigns a client ID to a system's available_on_nodes list. + + Args: + system_id (str): The ID of the system. + client_id (str): The ID of the client to assign. + + Returns: + dict: A dictionary containing the status and the updated system object. + + Raises: + requests.exceptions.RequestException: If the request fails. + json.JSONDecodeError: If the response is not valid JSON. + """ + url = f"{self.base_url}/{system_id}/assign" + payload = {"client_id": client_id} + try: + response = requests.post(url, json=payload) + return self._handle_response(response) + except (requests.exceptions.RequestException, json.JSONDecodeError) as e: + print(f"Error assigning client {client_id} to system {system_id}: {e}") + raise + + async def dismiss_client_from_system(self, system_id, client_id): + """ + Dismisses (removes) a client ID from a system's available_on_nodes list. + + Args: + system_id (str): The ID of the system. + client_id (str): The ID of the client to dismiss. + + Returns: + dict: A dictionary containing the status and the updated system object. + + Raises: + requests.exceptions.RequestException: If the request fails. + json.JSONDecodeError: If the response is not valid JSON. + """ + url = f"{self.base_url}/{system_id}/dismiss" + payload = {"client_id": client_id} + try: + response = requests.post(url, json=payload) + return self._handle_response(response) + except (requests.exceptions.RequestException, json.JSONDecodeError) as e: + print(f"Error dismissing client {client_id} from system {system_id}: {e}") + raise + +# Example Usage (assuming your Quart app is running on http://localhost:5000) +if __name__ == '__main__': + api = SystemAPIWrapper("http://localhost:5000/systems") + + # Example: Create a new system + new_system_data = { + "name": "Test System", + "frequency_khz": 100, + "avail_on_nodes": [] + } + try: + created_system = api.create_system(new_system_data) + print("Created System:", created_system) + except requests.exceptions.HTTPError as e: + print(f"Failed to create system: {e.response.status_code} - {e.response.text}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + + # Example: List all systems + try: + all_systems = api.list_systems() + print("\nAll Systems:", all_systems) + except requests.exceptions.RequestException as e: + print(f"Failed to list systems: {e}") + + + # Example: Get a system by ID (replace with a known system ID) + system_id_to_get = "some_system_id" # Replace with an actual ID + try: + system = api.get_system_by_id(system_id_to_get) + print(f"\nSystem with ID {system_id_to_get}:", system) + except requests.exceptions.HTTPError as e: + print(f"Failed to get system by ID {system_id_to_get}: {e.response.status_code} - {e.response.text}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + + + # Example: Get systems by client ID (replace with a known client ID) + client_id_to_get = "some_client_id" # Replace with an actual ID + try: + client_systems = api.get_systems_by_client_id(client_id_to_get) + print(f"\nSystems for client ID {client_id_to_get}:", client_systems) + except requests.exceptions.HTTPError as e: + print(f"Failed to get systems by client ID {client_id_to_get}: {e.response.status_code} - {e.response.text}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + + + # Example: Update a system (replace with a known system ID) + system_id_to_update = "some_system_id" # Replace with an actual ID + updated_data = {"frequency_khz": 120} + try: + updated_system = api.update_system(system_id_to_update, updated_data) + print(f"\nUpdated system {system_id_to_update}:", updated_system) + except requests.exceptions.HTTPError as e: + print(f"Failed to update system {system_id_to_update}: {e.response.status_code} - {e.response.text}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + + + # Example: Assign a client to a system (replace with known IDs) + system_id_to_assign = "some_system_id" # Replace with an actual ID + client_id_to_assign = "new_client_1" # Replace with an actual ID + try: + assign_result = api.assign_client_to_system(system_id_to_assign, client_id_to_assign) + print(f"\nAssign client {client_id_to_assign} to system {system_id_to_assign} result:", assign_result) + except requests.exceptions.HTTPError as e: + print(f"Failed to assign client to system: {e.response.status_code} - {e.response.text}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + + # Example: Dismiss a client from a system (replace with known IDs) + system_id_to_dismiss = "some_system_id" # Replace with an actual ID + client_id_to_dismiss = "new_client_1" # Replace with an actual ID + try: + dismiss_result = api.dismiss_client_from_system(system_id_to_dismiss, client_id_to_dismiss) + print(f"\nDismiss client {client_id_to_dismiss} from system {system_id_to_dismiss} result:", dismiss_result) + except requests.exceptions.HTTPError as e: + print(f"Failed to dismiss client from system: {e.response.status_code} - {e.response.text}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + + + # Example: Delete a system (replace with a known system ID) + system_id_to_delete = "some_system_id" # Replace with an actual ID + try: + deleted_system = api.delete_system(system_id_to_delete) + print(f"\nDeleted system {system_id_to_delete}:", deleted_system) + except requests.exceptions.HTTPError as e: + print(f"Failed to delete system {system_id_to_delete}: {e.response.status_code} - {e.response.text}") + except Exception as e: + print(f"An unexpected error occurred: {e}") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..99cce2c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +discord.py +requests \ No newline at end of file