commit d108673aaa9ddea53922fc5a7c2523c0ed0a1219 Author: The Wizard Date: Thu Oct 10 00:25:22 2024 +0100 Initial Upload diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..bf93035 --- /dev/null +++ b/bot.py @@ -0,0 +1,50 @@ +import discord +from discord.ext import commands +from datetime import datetime +import logging + +logger = logging.getLogger(__name__) + +# Greedily Grabs all Intents. +# Not the most secure, but the most bug free lol +intents = discord.Intents( + voice_states=True, + messages=True, + reactions=True, + message_content=True, + guilds=True, + members=True, +) + + +# Pretty Simple setup for Bot. +# https://github.com/Rapptz/discord.py/tree/master/examples +class RoboBot(commands.Bot): + def __init__(self, command_prefix, config, **options): + self.config = config + self.uptime = datetime.utcnow() # used for info :) + self.src = config.get("github_url") + # Set up the command prefix and intents from commands.Bot + super().__init__(command_prefix=command_prefix, intents=intents, **options) + + async def setup_hook(self): # Runs automatically + # Load cogs from config. (/cogs/{cog}) + # https://discordpy.readthedocs.io/en/stable/ext/commands/cogs.html + for cog in self.config.get("cogs", []): + try: + await self.load_extension("cogs." + cog) # f-strings better but. + logger.info(f"Successfully loaded cog: {cog}") + except Exception as e: + logger.error(f"Failed to load cog {cog}: {e}") + + async def on_ready(self): # also runs automatically, when bot is on discord + logger.info(f"Logged in as {self.user.name} (ID: {self.user.id})") + + # Create bot invite link + if self.user: + invite_url = discord.utils.oauth_url( + self.user.id, permissions=discord.Permissions(administrator=True) + ) + logger.info(f"Invite the bot to your server: {invite_url}") + + logger.info("Bot is ready and operational.") diff --git a/cogs/info.py b/cogs/info.py new file mode 100644 index 0000000..385b3c1 --- /dev/null +++ b/cogs/info.py @@ -0,0 +1,38 @@ +from discord.ext import commands +from utils import handle_api_request +import logging + +from utils import about_me_embed + +logger = logging.getLogger(__name__) + + +# Setup function for the cog +async def setup(bot): + await bot.add_cog(Info(bot)) + + +class Info(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.hybrid_command( + name="info_bot", + description="Displays information about the bot.", + aliases=["about", "bot_info"], + ) # fun fact, commands can't start with cog_ or bot_ + async def info_bot(self, ctx): + # see utils.py + embed = about_me_embed(self.bot) + await ctx.send(embed=embed) + + @commands.hybrid_command( + name="server_status", description="Fetches the game server status" + ) + async def server_status(self, ctx): + result, message = handle_api_request("server_status") + if result: + status = result.get("status", "Unknown") + await ctx.send(f"Server status: {status}") + else: + await ctx.send(f"Failed to retrieve server status: {message}") diff --git a/cogs/robocraft.py b/cogs/robocraft.py new file mode 100644 index 0000000..aca7d53 --- /dev/null +++ b/cogs/robocraft.py @@ -0,0 +1,110 @@ +from discord.ext import commands +from utils import is_mod, handle_api_request +import logging + +# didn't use discord apparently so i didn't import it. +logger = logging.getLogger(__name__) + + +# Setup function for the cog, necessary for every cog. +async def setup(bot): + await bot.add_cog(Robocraft(bot)) + + +# Scary looking but it's actually really simple. +# https://discordpy.readthedocs.io/en/stable/ext/commands/cogs.html +class Robocraft(commands.Cog): + def __init__(self, bot): + self.bot = bot + + # # This is what tells Discord.py about your commands. + # Every Command requires one of these decorators. + # all parameters optional, name defaults to function name + @commands.hybrid_command( + name="start_server", + description="Starts the game server", + aliases=["start", "boot", "bootup"], + ) + # @mod_only() for commands.command will do the same as the whole if not is_mod block. + # Unfortunately doesn't work for slash commands or hybrid commands. + + async def start_server(self, ctx): + # every command needs self and ctx (Context) + # use ctx.send() to send a message + # ctx.author for author (Member/User), ctx.guild for guild (Guild) + # ctx.message for the command message (ctx.message.content for text of command ;) ) + + # Check if the user is a mod + if not is_mod(ctx.author): + logger.warning( + f"{ctx.author} tried to start the server without permission." + ) + await ctx.send("You do not have permission to use this command.") + return + + # Handle the API request + result, message = handle_api_request("start_server") + if result: + logger.info(f"Server started by {ctx.author}.") + await ctx.send("Server started successfully!") + else: + await ctx.send(f"Failed to start server: {message}") + + @commands.hybrid_command( + name="stop_server", + description="Stops the game server", + aliases=["stop", "shutdown"], + ) + async def stop_server(self, ctx): + # Check if the user is a mod + if not is_mod(ctx.author): + logger.warning(f"{ctx.author} tried to stop the server without permission.") + await ctx.send("You do not have permission to use this command.") + return + + # Handle the API request + result, message = handle_api_request("stop_server") + if result: + logger.info(f"Server stopped by {ctx.author}.") + await ctx.send("Server stopped successfully!") + else: + await ctx.send(f"Failed to stop server: {message}") + + @commands.hybrid_command( + name="change_map", + description="Changes the game map", + aliases=["map", "new_map"], + ) + async def change_map(self, ctx, *, map_name: str): + # takes a str, oooo + # , *, means it greedily captures the rest of the string + # str could also be int, discord.User, discord.Member, or really anything you want. + # https://discordpy.readthedocs.io/en/stable/ext/commands/commands.html#parameters + # entire API docsis sacred god bless danny + + # Check if the user is a mod + if not is_mod(ctx.author): + logger.warning(f"{ctx.author} tried to change the map without permission.") + await ctx.send("You do not have permission to use this command.") + return + + # Handle the API request with the map name + # You can put whatever else you need here, mapping to whatever else the user puts in + # For here, it's just a basic dict with key map and value whatever the hell the user sends + + # Ideally, this should contain (or grab) a list of maps from the API in a way the backend understands + # But i don't know how you have that set up. + + # To make it random, + # from random import choice + # map_name = choice(maps) + # or whatever + + data = {"map": map_name} + + result, message = handle_api_request("change_map", data) + if result: + logger.info(f"Map changed to {map_name} by {ctx.author}.") + await ctx.send(f"Map changed to {map_name}!") + else: + await ctx.send(f"Failed to change map: {message}") diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..26d98f1 --- /dev/null +++ b/config.yaml @@ -0,0 +1,28 @@ +# Bot configuration +prefix: "" +token: "Your token here!" +github_url: "" + +# A list of cogs to load at startup +# These are found in /cogs/ +cogs: + - robocraft + - info + +# API configuration +api_url: "https://api.whofuckingknows.com/" +# Add routes here. +routes: + start_server: "/server/start" + stop_server: "/server/stop" + change_map: "/server/change_map" + server_status: "/server/status" + +# Mod IDs (for permissions) +# Replicate if you want admins, gamemasters, etc. +# see utils.py for details +mods: + - "123456789012345678" + - "987654321098765432" + + diff --git a/main.py b/main.py new file mode 100644 index 0000000..0d48649 --- /dev/null +++ b/main.py @@ -0,0 +1,34 @@ +import logging +from bot import RoboBot +from utils import load_config + +# Initialize logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", + datefmt="%H:%M:%S", +) +logger = logging.getLogger(__name__) + + +def main(): + # Load configuration + config = load_config() + + # Get bot token and prefix from the config + token = config.get("token") # .get() returns None if the key is not found + prefix = config.get("prefix", "!") + + if not token: + logger.error("Bot token not found in config.yaml") + return + + # Create the bot instance + bot = RoboBot(command_prefix=prefix, config=config) + + # Run the bot + bot.run(token) + + +if __name__ == "__main__": # always do this lol + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0e0d007 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +discord.py>=2.4.0 +humanfriendly==10.0 +psutil==6.0.0 +PyYAML==6.0.2 +Requests==2.32.3 diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..1455806 --- /dev/null +++ b/utils.py @@ -0,0 +1,152 @@ +import yaml +import logging +import requests +import discord +import datetime +import psutil +import humanfriendly + +# imagine a world where devs imported exactly what they needed +# but it's just not possible /s + +logger = logging.getLogger(__name__) + + +# Basic Loading with some error handling +def load_config(): + try: + with open("config.yaml", "r") as file: + config = yaml.safe_load(file) + logger.info("Configuration file loaded successfully.") + return config + except FileNotFoundError: + logger.error("Config file not found. Please ensure 'config.yaml' exists.") + return {} + except yaml.YAMLError as exc: + logger.error(f"Error parsing YAML config: {exc}") + return {} + + +# Unused Saving +def save_config(config): + try: + with open("config.yaml", "w") as file: + yaml.safe_dump(config, file) + logger.info("Configuration file saved successfully.") + except Exception as e: + logger.error(f"Error saving config: {e}") + + +# is_mod, takes a Member, returns a bool +# not the best as DM'ing the bot won't produce a list of roles and thus won't count mods. +# i just don't want to rewrite the yaml +def is_mod(member): + config = load_config() + mod_ids = config.get("mods", []) + + if str(member.id) in mod_ids: + return True + + for role in member.roles: + if str(role.id) in mod_ids: + return True + + return False + + +# Unused Decorator for commands. +# You'll come to learn there's a good amount of stuff that's just easier with commands rather than slash commands. +# See robocraft.py for some examples +def mod_only(): + def wrapper(func): + async def wrapped(self, ctx, *args, **kwargs): + if not is_mod(ctx.author): + logger.warning( + f"{ctx.author} tried to access {ctx.command} without permission." + ) + await ctx.send("You do not have permission to use this command.") + return + return await func(self, ctx, *args, **kwargs) + + return wrapped + + return wrapper + + +# Gross function, but it works. +def handle_api_request(route_key, data=None): + # route_key is your YAML key + # Data defaults to None so we don't have to set it + + config = load_config() + + # If you wanted to set up multiple API URLS, you could use them like routes (a list), or just copy how api_url is setup. + api_url = config.get("api_url") + routes = config.get("routes", {}) + + if not api_url: # you fucked up + logger.error("API URL is not configured.") + return None, "API URL is not configured." + + if route_key not in routes: # you also fucked up + logger.warning(f"Invalid route key: {route_key}") + return None, "Invalid API route key." + + route = routes[route_key] + url = f"{api_url}{route}" + + try: # Very prone to errors. Server shite. + if data: + response = requests.post(url, json=data) + else: + response = requests.post(url) + + if response.status_code == 200: + logger.info(f"API request to {url} succeeded.") + return response.json(), "Success!" + else: + return None, f"Error {response.status_code}: {response.text}" + + except Exception as e: + logger.error(f"Request failed: {e}") + return None, f"Request failed: {str(e)}" + + +# Baller about me embed, don't worry too much about this +# Good insight into how embeds work though. +def about_me_embed(bot): + cpu = psutil.cpu_percent() + memory = psutil.virtual_memory().percent + disk = psutil.disk_usage("/") + disk = { + "used_gb": round(disk.used / (1024.0**3), 2), + "available_gb": round(disk.free / (1024.0**3), 2), + "total_gb": round(disk.total / (1024.0**3), 2), + } + uptime = datetime.utcnow() - bot.uptime + if bot.src: # remember that? + description = f"This bot is [**Open Source!**](<{bot.src}>)" + else: + description = "A Discord Bot Written in Python!" # idk + + # make the embed + embed = discord.Embed( + title=f"About: {bot.user.name}", + description=description, + color=discord.Color.random(), + ) + # add fields + embed.add_field(name="CPU: ", value=f"{cpu}% of {psutil.cpu_count()} cores") + embed.add_field( + name="Memory: ", + value=f"{memory}% of {round(psutil.virtual_memory().total / (1024.0 ** 3), 2)} GB", + ) + embed.add_field( + name="Disk Usage: ", value=f"{disk['used_gb']} GB used of {disk['total_gb']} GB" + ) + embed.add_field(name="Uptime: ", value=f"{humanfriendly.format_timespan(uptime)}") + + # add avatar and footer + embed.set_image(url=bot.user.display_avatar.url) + embed.set_footer(text="Made with :3 by Flo ❤️", icon_url=bot.user.display_avatar.url) + return embed