Initial Upload

This commit is contained in:
The Wizard 2024-10-10 00:25:22 +01:00
commit d108673aaa
7 changed files with 417 additions and 0 deletions

50
bot.py Normal file
View file

@ -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.")

38
cogs/info.py Normal file
View file

@ -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}")

110
cogs/robocraft.py Normal file
View file

@ -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}")

28
config.yaml Normal file
View file

@ -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"

34
main.py Normal file
View file

@ -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()

5
requirements.txt Normal file
View file

@ -0,0 +1,5 @@
discord.py>=2.4.0
humanfriendly==10.0
psutil==6.0.0
PyYAML==6.0.2
Requests==2.32.3

152
utils.py Normal file
View file

@ -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