Garde-Havoc/bot/cogs/spawner.py
2024-12-18 22:17:51 +01:00

318 lines
9.7 KiB
Python

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