import discord from discord.ext import commands import docker import random import socket from contextlib import closing from dataclasses import dataclass from ipaddress import IPv4Address import secrets from mcrcon import MCRcon import asyncio from models.users import * import utils class RCON(MCRcon): def __init__(self, ip: str, secret: str, port: int): super().__init__(ip, secret, port) def rconnect(self): self.connect() def whitelist(self): pass def sendcmd(self, cmds) -> None: if isinstance(cmds, str): return self.command(str) if isinstance(cmds, list): return [self.command(cmd) for cmd in cmds] def __del__(self): self.disconnect() @dataclass class Server: container: None name: str rcon: RCON port: int players: int # Global List of all running Containers containers = list() def get_server(name): name = name.title() for container in containers: if container.name == name: return container return None color = discord.Color.from_rgb(13, 183, 237) def seed_generator(): ''' Generates a random minecraft seed ''' seed = random.randrange(1_000_000_000, 100_000_000_000_000) if random.randrange(0,2) == 0: seed *= -1 return str(seed) def find_free_port(): ''' Returns the next available IPv4 Port ''' with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: s.bind(('', 0)) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) return s.getsockname()[1] class Spawner(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self.client = docker.from_env() self.client.images.pull('itzg/minecraft-server:latest') @commands.hybrid_command(name='spawn') async def spawn(self, ctx: commands.Context, server_name: str, world_url: str = None, seed: str = None, enable_command_blocks: bool = False, max_players: int = 10, enable_hardcore: bool = False, ): ''' Spawns a standard defined Minecraft Server Either from a World Download Link or a Seed Parameters ---------- ctx: commands.Context The context of the command invocation server_name: str Name of the Server world_url: str Download link of a minecraft world (Should be a downloadable ZIPP Archive seed: str Seed to generate a World from enable_command_blocks: bool Enable or disable command Block max_players: int Maximum Number of Players who can join the Server enable_hardcore: bool Enables Hardcore Minecraft ''' try: User.get(User.username == ctx.author.id) except: await ctx.send(f"{ctx.author.name} isn't registered!") return # Send user a Waiting screen to avoid confusion embed = discord.Embed( title="Starting Server", description=f''' Setting up {server_name} This could take up to **5 minutes** ''', color=color, timestamp=utils.now() ) file = discord.File("../assets/clock.png", filename="clock.png") embed.set_thumbnail(url="attachment://clock.png") start = await ctx.send(file=file, embed=embed) # Set up needed variables # Server Stuff port = find_free_port() server_name = server_name.title() # Rcon Stuff passwd = secrets.token_hex(32) rcon_port = find_free_port() # Image Enviroment env = { "EULA": "true", "TYPE": "FABRIC", "VERSION": "1.21.1", "SERVER_NAME": server_name, "LEVEL": server_name, "ONLINE_MODE": "true", "TZ": "Europe/Berlin", "MOTD": "\u00a7d\u00a7khhh\u00a76Powered by\u00a7b Garde Studios\u00a76!\u00a7d\u00a7khhh", "OVERRIDE_SERVER_PROPERTIES": "true", "ENABLE_COMMAND_BLOCK": enable_command_blocks, "GAMEMODE": "survival", "FORCE_GAMEMODE": "true", "RCON_PASSWORD": passwd, "RCON_PORT": rcon_port, "BROADCAST_CONSOLE_TO_OPS": "false", "BROADCAST_RCON_TO_OPS": "false", "SERVER_PORT": port, "FORCE_REDOWNLOAD": "true", "INIT_MEMORY": "500M", "MAX_MEMORY": "2G", "USE_AIKAR_FLAGS": "true", "OPS_FILE": "https://git.cyperpunk.de/Garde-Studios/Uno-MC/raw/branch/main/ops.json", "SYNC_SKIP_NEWER_IN_DESTINATION": "false", "MAX_PLAYERS": max_players, "ANNOUNCE_PLAYER_ACHIEVMENTS": "true", "HARDCORE": enable_hardcore, "SNOOPER_ENABLED": "false", "SPAWN_PROTECTION": 0, "VIEW_DISTANCE": 12, "ALLOW_FLIGHT": "false", # "RESOURCE_PACK": "", # "RESOURCE_PACK_SHA1": "", } # Decide between seed or world url if no ones given seed would be randomly generated if not seed and not world_url: seed = seed_generator() if seed: env["SEED"] = seed if world_url: env["WORLD"] = world_url # setting up the container container = self.client.containers.run( image='itzg/minecraft-server:latest', environment=env, detach=True, hostname=server_name, name=server_name, network_mode='bridge', ports={port:port, rcon_port:rcon_port}, restart_policy={"Name": "always"}, volumes={'mods.txt': {'bind': '/extras/mods.txt', 'mode': 'ro'}} ) # Connect Container to the appropiate network net = self.client.networks.get('bot_rcon') net.connect(container) # save container info ip = self.client.containers.get(server_name).attrs['NetworkSettings']['Networks']['bot_rcon']['IPAddress'] rcon = RCON(ip, passwd, rcon_port) server = Server(container, server_name, rcon, port, max_players) containers.append(server) # Send user the confirmation embed = discord.Embed( title="Success", description=f''' **{server_name}** has started! **Connection URL**: garde-studios.de:{port} ''', color=color, timestamp=utils.now() ) file = discord.File("../assets/docker.png", filename="docker.png") embed.set_thumbnail(url="attachment://docker.png") await start.delete() await ctx.send(file=file, embed=embed) @commands.hybrid_command(name='servers') async def servers(self, ctx: commands.Context): ''' List all currently Running Servers Parameters ---------- ctx: commands.Context The context of the command invocation ''' try: User.get(User.username == ctx.author.id) except: await ctx.send(f"{ctx.author.name} isn't registered!") return embed = discord.Embed( title="Currently Running Servers", description="List of all currently running Minecraft Servers", color=color, timestamp=utils.now() ) for container in containers: desc = f''' *Status*: {container.container.status} *URL*: garde-studios.de:{container.port} ''' embed.add_field(name=f'{container.name} 0/{container.players}', value=desc) file = discord.File("../assets/cloud.png", filename="cloud.png") embed.set_thumbnail(url="attachment://cloud.png") await ctx.send(file=file, embed=embed) @commands.hybrid_command(name='kill') async def kill(self, ctx: commands.Context, server_name: str): ''' Kill & remove currently Running Servers Parameters ---------- ctx: commands.Context The context of the command invocation server_name: str Name of the server that should be removed ''' try: User.get(User.username == ctx.author.id) except: await ctx.send(f"{ctx.author.name} isn't registered!") return server_name = server_name.title() # Check if Server exist rm = str() conn = None for container in containers: if container.name == server_name: rm = server_name conn = container break if not rm: await ctx.send("---Server not found---") return conn.container.remove(force=True) #self.client.volumes.get(server_name).remove() containers.remove(conn) embed = discord.Embed( title="Killed", description=f"{server_name} killed!", color=color, timestamp=utils.now() ) file = discord.File("../assets/rip.png", filename="rip.png") embed.set_thumbnail(url="attachment://rip.png") await ctx.send(file=file, embed=embed) if __name__ == '__main__': for _ in range(10): print("|", seed_generator(), "|") print("Port:", find_free_port())