aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorojhern24 <github@mail.tysonth.com>2023-05-05 18:52:09 -0400
committerojhern24 <github@mail.tysonth.com>2023-05-05 18:52:09 -0400
commit6da779d59fa6b7351cffdf888898fce8b40fa65f (patch)
tree961b05c76ef3c59610578fe54481c6fd7f49d0b3
downloadDiscordGameServerManager-6da779d59fa6b7351cffdf888898fce8b40fa65f.zip
Refresh of old repository
-rw-r--r--README.md53
-rw-r--r--bot.py239
-rw-r--r--botConfig.py28
-rw-r--r--cogs/minecraft.py188
-rw-r--r--cogs/minecraft/coordinates/test.txt1
-rw-r--r--minecraftConfig.py22
-rw-r--r--scripts/README.md5
-rw-r--r--scripts/runMinecraft.sh14
-rw-r--r--scripts/test.sh1
-rw-r--r--sounds/README.md5
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
diff --git a/bot.py b/bot.py
new file mode 100644
index 0000000..5f5c4e8
--- /dev/null
+++ b/bot.py
@@ -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!"*])