diff options
| author | ojhern24 <github@mail.tysonth.com> | 2023-05-05 18:52:09 -0400 |
|---|---|---|
| committer | ojhern24 <github@mail.tysonth.com> | 2023-05-05 18:52:09 -0400 |
| commit | 6da779d59fa6b7351cffdf888898fce8b40fa65f (patch) | |
| tree | 961b05c76ef3c59610578fe54481c6fd7f49d0b3 | |
| download | DiscordGameServerManager-6da779d59fa6b7351cffdf888898fce8b40fa65f.zip | |
Refresh of old repository
| -rw-r--r-- | README.md | 53 | ||||
| -rw-r--r-- | bot.py | 239 | ||||
| -rw-r--r-- | botConfig.py | 28 | ||||
| -rw-r--r-- | cogs/minecraft.py | 188 | ||||
| -rw-r--r-- | cogs/minecraft/coordinates/test.txt | 1 | ||||
| -rw-r--r-- | minecraftConfig.py | 22 | ||||
| -rw-r--r-- | scripts/README.md | 5 | ||||
| -rw-r--r-- | scripts/runMinecraft.sh | 14 | ||||
| -rw-r--r-- | scripts/test.sh | 1 | ||||
| -rw-r--r-- | sounds/README.md | 5 |
10 files changed, 556 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..39ad82d --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# Discord Game Server Manager +Discord Bot interface that allows users and server admins to interact and execute commands in game servers. Additional tools included. + +This was written with only private use in mind, so it currently does not have any security features in place that you would expect a public Discord bot to have. + +This has not been tested on Windows, so it assumes you will be running this under a common Debian distribution and a bash shell, but it should be easy to set up with a little know-how. + +## Requirements + +`apt-get install screen` +`pip install discord.py` +`pip install mcstatus` +`pip install speedtest-cli` + +## Configuration + +You will also need to fill in the bot token and other information in the config files stored in root: + +`botConfig.py` +`minecraftConfig.py` + +Sound files are also not provided as they are copyrighted material but a list of sounds the program expects is included **(not required)**. + +## Current Features + +Execute a game server script with some initial setup required from the host running the bot **(abuse prevention partially implemented)** + +Display host's external IP address to provide ease of access if the host has a dynamic IP address and no DDNS configured or inaccessible **(disabled by default)** + +Perform a speedtest to test connection stability **(disabled by default)** + +Restrict the usage of certain commands **(all off by default)** + +###### Game Specific + +Interface with Minecraft server via [mcstatus](https://github.com/Dinnerbone/mcstatus) to provide further support for players + +Tools to increase game immersion such as storing Minecraft coordinates **(still a rudimentary system)** + +## Planned Features + +Disable and enable commands through admin-only accessible commands to require less editing of the python files + +Releasing a Docker image that would require minimal setup to deploy + +___ + +###### Supported Games +*Minecraft* + +###### Planned Games + +Sonic Robo Blast 2 @@ -0,0 +1,239 @@ +import logging +import subprocess +import time +import urllib.request +from os import path + +import discord +from discord.ext import commands +from discord.ext.commands import BucketType + +import botConfig + +# Log information +logger = logging.getLogger('discord') +logger.setLevel(logging.DEBUG) +handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w') +handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s')) +logger.addHandler(handler) + + +# Functions + +def log(text): + print('[General]: ' + text) + + +# Bot Config +token = botConfig.token +guild = botConfig.guild + +# Links +botRepo = botConfig.bot_repo + +# Open Ports +minecraftPort = botConfig.minecraft_port + +# Scripts +minecraftServer = botConfig.minecraft_server_script + +# Sounds +hello = botConfig.hello_sound + +# Initialize bot +client = commands.Bot(command_prefix=botConfig.prefix) + +# Initialize extensions +extensions = ( + "cogs.minecraft", +) + +print('Loading cogs:') + +for extension in extensions: + try: + client.load_extension(extension) + except Exception as e: + print(f'Failed to load extension {extension}') + +print('\n') + + +# Events + +@client.event +async def on_ready(): + log('Logged on as: (' + str(client.user) + ').') + + +@client.event +async def on_message(message): + # don't respond to ourselves + if message.author == client.user: + return + + await client.process_commands(message) + + if message.content == 'hello mario': + await message.channel.send('Hello!') + try: + await message.channel.send(file=discord.File(hello)) + except Exception as e: + log('User has found an easter egg but no sound file was provided!') + + +# Easter Egg + + +# @client.event +# async def on_command_error(error, ctx): +# await error.send('An error has occurred.') + +# Commands + +# *General* + +@client.command(aliases=['github', 'repo', 'repository', 'source', 'download'], + brief='Links to the current repository for this bot!') +async def codebase(ctx): + await ctx.send(botRepo) + + +@client.command(brief='Outputs the latency between Discord and the bot') +async def ping(ctx): + await ctx.send(f'Pong! `{round(client.latency * 1000)}ms`') + + +# *Server* + +# Please check /scripts and configure .env and add your directories, otherwise this will not work +@client.command(aliases=['boot', 'start'], brief='Initializes a server for compatible games', + description='Initializes a server for compatible games, type {0}run usage for more info.'.format( + botConfig.prefix)) +@commands.cooldown(1, 30, BucketType.guild) +async def run(ctx, *, game): + if game == 'usage': + await ctx.send('```{0}run [game], Example: $run minecraft\n\n'.format(botConfig.prefix) + + 'List of compatible games:\n' + + '• Minecraft```') + + # Run a server if .running does not exist + elif not path.exists('.running'): + + if game == 'minecraft' or game == 'mc': + try: + await ctx.send( + 'The Minecraft server script has been executed, please wait a moment as the server initializes.') + subprocess.call(minecraftServer, shell=True) + log('A Minecraft server has been initialized.') + + except Exception as e: + await ctx.send('Could not successfully initialize the server, please contact bot administrator.') + log('A Minecraft server could not be initialized. Please check /scripts/runMinecraft.sh to make sure ' + 'everything is set correctly. You must also ensure the script has executable permissions.') + + # Start 50-second timer to inform server should now be in service + time.sleep(50) + await ctx.send('The Minecraft server should now be up and running!') + + # Otherwise, inform the user a server cannot be executed and give further instructions. + else: + await ctx.send( + 'A server is already running! Please contact a server administrator to request a restart or termination ' + 'of the current session.') + + +@client.command(aliases=['address'], brief='Displays the server\'s external IP and open ports') +@commands.cooldown(1, 30, BucketType.guild) +async def ip(ctx): + # Check if command is allowed by bot Administrator + if botConfig.ip_cmd_allowed: + # Contact URL stored in botConfig and store IP address as string + ext_ip = urllib.request.urlopen(botConfig.ip_grab_website).read().decode('utf8') + await ctx.send("Server IP: `" + ext_ip + "`" + + "\nOpen Ports:" + + "\n```Minecraft: " + minecraftPort + "```") + + else: + await ctx.send('Bot administrator has not authorized this command.') + log('IP grab attempt blocked. To change this behavior open botConfig.py and find the line: ' + + '\'ipCmdAllowed = False\'' + + '\nand replace False with True') + + +@client.command(aliases=['bandwidth'], brief='Perform a speedtest, powered by Ookla™') +@commands.cooldown(1, 90, BucketType.guild) +async def speedtest(ctx): + # Check if command is allowed by bot Administrator + if botConfig.speedtest_cmd_allowed: + import speedtest + + log('A user has just initiated a speedtest.') + await ctx.send('Attempting to perform Speedtest...') + + # https://github.com/sivel/speedtest-cli/wiki + + servers = [] + threads = None + + attempt = speedtest.Speedtest() + attempt.get_servers(servers) + attempt.get_best_server() + + await ctx.send('Performing download test...') + attempt.download(threads=threads) + + await ctx.send('Performing upload test...') + attempt.upload(threads=threads) + + log('Here are the results: ' + attempt.results.share()) + await ctx.send(attempt.results.share()) + + else: + await ctx.send('Bot administrator has not authorized this command.') + log('Speedtest attempt blocked. To change this behavior open botConfig.py and find the line: ' + + '\'speedtestCmdAllowed = False\'' + + '\nand replace False with True') + + +# Error Handlers + +@run.error +async def run_error(ctx, error): + # Check if currently on cooldown + if isinstance(error, commands.CommandOnCooldown): + await ctx.send( + 'This command has a 30 second cooldown. You may use it again in `{:.2f}`s'.format(error.retry_after)) + + else: + await ctx.send("You must specify a game!") + + +@ip.error +async def ip_error(ctx, error): + # Check if currently on cooldown + if isinstance(error, commands.CommandOnCooldown): + await ctx.send( + 'This command has a 30 second cooldown. You may use it again in `{:.2f}`s'.format(error.retry_after)) + + else: + await ctx.send("An unknown error has occurred.") + + +@speedtest.error +async def speedtest_error(ctx, error): + # Check if currently on cooldown + if isinstance(error, commands.CommandOnCooldown): + await ctx.send( + 'This command has a 90 second cooldown. You may use it again in `{:.2f}`s'.format(error.retry_after)) + + else: + await ctx.send("An unknown error has occurred.") + + +# Run bot with corresponding token +client.run(token) + +# To do list +# Disable Easter eggs if missing in config +# Make $run and $speedtest run asynchronously diff --git a/botConfig.py b/botConfig.py new file mode 100644 index 0000000..442d214 --- /dev/null +++ b/botConfig.py @@ -0,0 +1,28 @@ +# Bot Information + +token = '' +guild = '' +prefix = '$' + +# Commands Permissions +ip_cmd_allowed = False +speedtest_cmd_allowed = False + +# Links +bot_repo = 'https://vc.tysonth.com/DiscordGameServerManager/about/' +ip_grab_website = 'https://ident.me' +# --Alternative-- +# ip_grab_website = 'https://ipinfo.io/ip' + +# Ports + +minecraft_port = 'None' + +# Scripts + +minecraft_server_script = 'scripts/runMinecraft.sh' + +# Sounds + +# hello_sound = 'sounds/hello.wav' +hello_sound = 'None' diff --git a/cogs/minecraft.py b/cogs/minecraft.py new file mode 100644 index 0000000..ee86835 --- /dev/null +++ b/cogs/minecraft.py @@ -0,0 +1,188 @@ +import os +import os.path +from os import path + +from discord.ext import commands +from discord.ext.commands import BucketType +from mcstatus import MinecraftServer + +import botConfig +import minecraftConfig + + +class Minecraft(commands.Cog): + + def __init__(self, client): + self.client = client + + @commands.Cog.listener() + async def on_ready(self): + + # Inform users that the Minecraft server can now be initialized. + channel = self.client.get_channel(minecraftConfig.server_status) + + await channel.send( + '[Minecraft]: The server computer has successfully logged in, you can now start the Minecraft server ' + 'with:\n`{0}run minecraft`'.format( + botConfig.prefix)) + log('Users have been informed the Minecraft server can now be started.') + + @commands.command(aliases=['coords'], brief='Allows users to store coordinates into a directory', + description='Store a set of coordinates with a corresponding tag, type {0}coordinates usage for ' + 'more info.'.format( + botConfig.prefix)) + async def coordinates(self, ctx, *args): + + if args[0] == "usage": + await ctx.send( + '```{0}coordinates make [tag] [x] [y] [z], Example: {0}coordinates make home -1000 64 2000\n'.format( + botConfig.prefix) + + '{0}coordinates get [tag], Example: {0}coordinates get home\n'.format(botConfig.prefix) + + '{0}coordinates replace [tag] [x] [y] [z], Example: {0}coordinates replace home -3000 72 2500\n'.format( + botConfig.prefix) + + '{0}coordinates delete [tag], Example: {0}coordinates delete home```'.format(botConfig.prefix)) + + else: + # Open coordinatesDirectory as stored in minecraftConfig.py + filename = minecraftConfig.coordinates_directory + args[1] + ".txt" + + # If the user sends more than 4 arguments + if len(args) > 5: + await ctx.send('You have sent too many parameters!') + + # Do this if the user wants to store a new set of coordinates + elif args[0] == "make" or args[0] == "store": + + # Continue creating set if it doesn't exist + if not path.exists(filename): + location_file = open(filename, "w") + + location = '(' + args[2] + ', ' + args[3] + ', ' + args[4] + ')' + location_file.write(location) + + await ctx.send('Coordinates have been saved as: `' + args[1] + '`!') + location_file.close() + + log(args[1] + ".txt has been created.") + + # Inform the user this set already exists + else: + await ctx.send( + "The name you've selected for your location already exists! Please use another and try again" + + "or use `{0}coordinates replace [tag] [x] [y] [z]` (no square brackets).".format( + botConfig.prefix)) + + # Read the coordinates out to the user + elif args[0] == "get": + + if path.exists(filename): + + location_file = open(filename, "r") + location = location_file.read() + + await ctx.send('Coordinates: ' + location) + location_file.close() + + else: + await ctx.send("`" + args[1] + "` does not exist!") + + # Overwrite a set of coordinates + elif args[0] == "replace" or args[0] == "overwrite": + + # Check if file exists + if path.exists(filename): + location_file = open(filename, "w") + + location = '(' + args[2] + ', ' + args[3] + ', ' + args[4] + ')' + location_file.write(location) + + await ctx.send('Coordinates have overwritten: `' + args[1] + '`!') + location_file.close() + + log(args[1] + ".txt has been overwritten.") + + else: + await ctx.send("`" + args[1] + "` does not exist!") + + # Delete a set of coordinates + elif args[0] == "delete" or args[0] == "erase": + + # Check if file exists + if path.exists(filename): + os.remove(filename) + await ctx.send('Coordinates that were saved as `' + args[1] + '` have been deleted.') + log(args[1] + ".txt has been deleted.") + + else: + await ctx.send('These coordinates do not exist, check to make sure the name is correct.') + + @commands.command(aliases=['minecraft'], + brief='Subset of tools for Minecraft, refer to {0}mc usage'.format(botConfig.prefix)) + @commands.cooldown(1, 5, BucketType.guild) + async def mc(self, ctx, *, arg): + + if arg == "mods": + await ctx.send(minecraftConfig.mods_directory) + + if minecraftConfig.mods_password != "None": + await ctx.send("Password: `" + minecraftConfig.mods_password + "`") + + elif arg == "status": + + # Pull computer's local IP address and port number + server_address = MinecraftServer('localhost', minecraftConfig.minecraft_port) + + # Record status and output to user + status = server_address.status() + await ctx.send( + "`{0}` is currently online with {1} player(s) and responded in `{2}μs`".format(status.description, + status.players.online, + status.latency * 1000)) + + elif arg == "query": + + # Same as status, but must have query enabled + server_address = MinecraftServer('localhost', minecraftConfig.minecraft_query_port) + query = server_address.query() + + # If list of players is empty, inform the users the server is vacant. + if len(query.players.names) == 0: + await ctx.send("`{0}` has no players online.".format(query.motd)) + + # List players currently in-game + else: + await ctx.send("`{0}` has the following players online: ```\n{1}```".format(query.motd, ", ".join( + query.players.names))) + + # Error Handlers + + @coordinates.error + async def coordinates_error(self, ctx): + await ctx.send("Your parameters are incorrect, try again!") + + @mc.error + async def mc_error(self, ctx, error): + + # Check if currently on cooldown + if isinstance(error, commands.CommandOnCooldown): + await ctx.send( + 'This command has a 5 second cooldown. You may use it again in `{:.2f}s`'.format(error.retry_after)) + + else: + await ctx.send( + "The server is either down, has query disabled and/or the host's ports may not be properly forwarded.") + + +# Functions + +def setup(client): + client.add_cog(Minecraft(client)) + print('[Minecraft]') + + +def log(text): + print('[Minecraft]: ' + text) + +# To do list +# Rework {0}coordinates command to store as a dictionary instead of individual text files +# Add minimum parameter check to {0}coordinates make diff --git a/cogs/minecraft/coordinates/test.txt b/cogs/minecraft/coordinates/test.txt new file mode 100644 index 0000000..6de9ccb --- /dev/null +++ b/cogs/minecraft/coordinates/test.txt @@ -0,0 +1 @@ +(0, 0, 0)
\ No newline at end of file diff --git a/minecraftConfig.py b/minecraftConfig.py new file mode 100644 index 0000000..4799a2d --- /dev/null +++ b/minecraftConfig.py @@ -0,0 +1,22 @@ +# Address (You only need to have one uncommented, if 'localhost' does not work, try the second option or supply your +# public IP +ip_address = 'localhost' +# ip_address = '127.0.0.1' +# ip_address = 'Insert Public IP Address here' + +# Channels + +server_status = 'Insert channel ID for where bot will make announcements' + +# Directories + +coordinates_directory = 'cogs/minecraft/coordinates/' + +# Open Ports +minecraft_port = 25565 +minecraft_query_port = 25575 + +# Links + +mods_directory = 'Bot administrator has not added a URL for this.' +mods_password = 'None' diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..0651ef5 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,5 @@ +## Access terminals after they are executed +In your terminal, use the command: `screen -r [name here]` +Example: `screen -r minecraft` + +**Remember to give these scripts execution permissions otherwise they will not work!** diff --git a/scripts/runMinecraft.sh b/scripts/runMinecraft.sh new file mode 100644 index 0000000..9d09583 --- /dev/null +++ b/scripts/runMinecraft.sh @@ -0,0 +1,14 @@ +#!/bin/sh +screen -d -m -S minecraft bash -c 'touch .running; cd; cd directory/to/your/minecraft/server; ./start.sh; cd; cd directory/to/dgsm/bot; rm .running; exit; exec bash' + +# In Layman's terms: + +# [Before server starts] +# Open a virtual terminal with the name 'minecraft', create a hidden file called '.running', go to home directory, go to minecraft server directory, +# execute server initialization script (MUST HAVE EXECUTE PERMISSIONS) + +# [After server shuts down] +# Go to root directory, go to this Discord bot's directory, delete '.running' file so that the $run command can be used once again, exit the virtual terminal so that +# this current session is terminated and there are not multiple terminals named 'minecraft' + +# i hope this makes sense diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100644 index 0000000..739dc11 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1 @@ +echo "test"
\ No newline at end of file diff --git a/sounds/README.md b/sounds/README.md new file mode 100644 index 0000000..42c94b3 --- /dev/null +++ b/sounds/README.md @@ -0,0 +1,5 @@ +Due to the audio files used by the bot being copyrighted material you are required to provide these yourself. + +## List of sounds + +hello.wav - (Super Mario 64 - Mario [*"Hello!"*]) |
