Changed: Final

This commit is contained in:
DerGrumpf 2024-12-18 22:17:51 +01:00
parent 3e616132d4
commit 8b1b6fdd54
14 changed files with 393 additions and 124 deletions

BIN
bot/assets/death.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
bot/assets/end.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
bot/assets/fight.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
bot/assets/init.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
bot/assets/safe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
bot/bot.db Normal file

Binary file not shown.

View File

@ -1,9 +1,11 @@
import discord
from discord.ext import commands
from mcrcon import MCRcon
import enum
from transitions import Machine
from cogs.spawner import containers
from cogs.spawner import get_server
import utils
color = discord.Color.from_rgb(181, 24, 60)
class Whitelist:
"A workflow machine for managing Whitelist states"
@ -11,25 +13,6 @@ class Whitelist:
Off = None
toggle = None
class RCON(MCRcon):
def __init__(self, ip: str, secret: str, port: int = 31066):
super().__init__(ip, secret, port)
self.connect()
def whitelist(self):
self.whitelist.toggle()
cmds = "/whitelist {}".format(self.whitelist.current_state.id)
print(cmds)
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()
class States(enum.Enum):
NOTHING = 0
INIT = 1
@ -41,7 +24,6 @@ class States(enum.Enum):
class Minecraft(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.servers = dict()
transitions = [
['init_game', States.NOTHING, States.INIT],
@ -68,21 +50,12 @@ class Minecraft(commands.Cog):
server_name: str
Server on which the Session should be initialized
"""
server_name = server_name.title()
c = None
for container in containers:
if server_name == container.name:
c = container
break
c = get_server(server_name)
if not c:
await ctx.send("---The server doesn't run---")
return
conn = RCON(str(c.ip), c.rcon_pass, c.rcon_port)
self.servers[server_name] = conn
cmds = [
"/effect give @a minecraft:resistance infinite 255 true",
"/effect give @a minecraft:saturation infinite 4 true",
@ -92,9 +65,29 @@ class Minecraft(commands.Cog):
"/worldborder set 5",
"/whitelist off"
]
c.rcon.rconnect()
c.rcon.sendcmd(cmds)
conn.sendcmd(cmds)
await ctx.send("init Border Wars Game")
embed = discord.Embed(
title=f"Border Wars @ {server_name.title()}",
description='''
Session initialized
Get ready to get a good armorset.
**Explanation**
This phase last as long as the moderator wants.
Every player is immune to damage and can't die by hunger.
Look out for a good direction to scout.
Happy Border Wars!
''',
color=color,
timestamp=utils.now()
)
file = discord.File("../assets/init.png", filename="init.png")
embed.set_thumbnail(url="attachment://init.png")
await ctx.send(file=file, embed=embed)
@commands.hybrid_command(name='safe')
async def safe(self, ctx: commands.Context, server_name: str):
@ -108,25 +101,12 @@ class Minecraft(commands.Cog):
server_name: str
Server on which the Session should be initialized
"""
server_name = server_name.title()
c = None
for container in containers:
if server_name == container.name:
c = container
break
c = get_server(server_name)
if not c:
await ctx.send("---The server doesn't run---")
return
conn = self.servers.get(server_name)
if not conn:
await ctx.send("---Border Wars Session not Initialized---")
return
cmds = [
'''/title @a subtitle ["",{"text":"bei ","color":"blue"},{"text":"BORDER WARS!","bold":true,"color":"red"}]''',
'''/title @a title {"text":"Viel Glück!","bold":true,"color":"blue"}''',
@ -137,8 +117,28 @@ class Minecraft(commands.Cog):
"/effect clear @a"
]
conn.sendcmd(cmds)
await ctx.send("Switched to Safe Phase")
c.rcon.sendcmd(cmds)
embed = discord.Embed(
title=f"Border Wars @ {server_name.title()}",
description='''
Switched to Safe Phase
Hide from other players and scout for resources.
**Explanation**
This phase lasts 30 minutes, after that the fighting phase begins.
Every player who dies will keep there inventory.
Get a good armorset and be ready to fight.
Happy Border Wars!
''',
color=color,
timestamp=utils.now()
)
file = discord.File("../assets/safe.png", filename="safe.png")
embed.set_thumbnail(url="attachment://safe.png")
await ctx.send(file=file, embed=embed)
@commands.hybrid_command(name='fight')
async def fight(self, ctx: commands.Context, server_name: str):
@ -152,25 +152,12 @@ class Minecraft(commands.Cog):
server_name: str
Server on which the Session should be initialized
"""
server_name = server_name.title()
c = None
for container in containers:
if server_name == container.name:
c = container
break
c = get_server(server_name)
if not c:
await ctx.send("---The server doesn't run---")
return
conn = self.servers.get(server_name)
if not conn:
await ctx.send("---Border Wars Session not Initialized---")
return
cmds = [
'''/title @a subtitle {"text":"ÜBERLEBEN!","bold":true,"color":"red"}''',
'''/title @a title {"text":"Möge der beste","color":"blue"}''',
@ -179,8 +166,28 @@ class Minecraft(commands.Cog):
"/gamerule keepInventory false"
]
conn.sendcmd(cmds)
await ctx.send("Switched to Fight Phase")
c.rcon.sendcmd(cmds)
embed = discord.Embed(
title=f"Border Wars @ {server_name.title()}",
description='''
Switched to Fight Phase
Now the real fun begins.
**Explanation**
This phase lasts 60 minutes, after that the game ends.
Every player can die now and therefor will lose.
Look out for other players.
Happy Border Wars!
''',
color=color,
timestamp=utils.now()
)
file = discord.File("../assets/fight.png", filename="fight.png")
embed.set_thumbnail(url="attachment://fight.png")
await ctx.send(file=file, embed=embed)
@commands.hybrid_command(name='death')
async def death(self, ctx: commands.Context, server_name: str):
@ -194,33 +201,40 @@ class Minecraft(commands.Cog):
server_name: str
Server on which the Session should be initialized
"""
server_name = server_name.title()
c = None
for container in containers:
if server_name == container.name:
c = container
break
c = get_server(server_name)
if not c:
await ctx.send("---The server doesn't run---")
return
conn = self.servers.get(server_name)
if not conn:
await ctx.send("---Border Wars Session not Initialized---")
return
cmds = [
'''/title @a title ["",{"text":"Sudden ","color":"dark_blue"},{"text":"DEATH!","bold":true,"color":"red"}]''',
"/playsound minecraft:entity.ender_dragon.growl ambient @a 0 64 0 80",
"/worldborder set 5 600"
]
conn.sendcmd(cmds)
await ctx.send("Switched to Sudden Death Phase")
c.rcon.sendcmd(cmds)
embed = discord.Embed(
title=f"Border Wars @ {server_name.title()}",
description='''
Switched to Sudden Death
Only one can be the Winner.
**Explanation**
This phase lasts as long as only one player survives.
The worldborder will now shrink 'till zero zero.
Good Luck.
Happy Border Wars!
''',
color=color,
timestamp=utils.now()
)
file = discord.File("../assets/death.png", filename="death.png")
embed.set_thumbnail(url="attachment://death.png")
await ctx.send(file=file, embed=embed)
@commands.hybrid_command(name='end')
async def end(self, ctx: commands.Context, server_name: str, playername: str):
@ -236,25 +250,12 @@ class Minecraft(commands.Cog):
playername: str
Player which is announced as the Winner
"""
server_name = server_name.title()
c = None
for container in containers:
if server_name == container.name:
c = container
break
c = get_server(server_name)
if not c:
await ctx.send("---The server doesn't run---")
return
conn = self.servers.get(server_name)
if not conn:
await ctx.send("---Border Wars Session not Initialized---")
return
cmds = [
"/worldborder center 0 0",
"/worldborder set 75",
@ -264,8 +265,20 @@ class Minecraft(commands.Cog):
"/playsound minecraft:entity.ender_dragon_death ambient @a 0 64 0 80",
]
conn.sendcmd(cmds)
await ctx.send("Ended Border Wars Session")
c.rcon.sendcmd(cmds)
embed = discord.Embed(
title=f"Border Wars @ {server_name.title()}",
description=f'''
*Game Over*
Congratulations **{playername}**.
Hope you had fun @ **Border Wars!**
''',
color=color,
timestamp=utils.now()
)
file = discord.File("../assets/end.png", filename="end.png")
embed.set_thumbnail(url="attachment://end.png")
await ctx.send(file=file, embed=embed)

View File

@ -4,35 +4,65 @@ import docker
import random
import socket
from contextlib import closing
from datetime import datetime
import pytz
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
ip: IPv4Address
rcon: RCON
port: int
players: int
rcon_pass: str
rcon_port: 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)
@ -75,6 +105,13 @@ class Spawner(commands.Cog):
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'''
@ -83,17 +120,21 @@ class Spawner(commands.Cog):
This could take up to **5 minutes**
''',
color=color,
timestamp=datetime.now(pytz.timezone('Europe/Berlin'))
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",
@ -120,7 +161,6 @@ class Spawner(commands.Cog):
"MAX_MEMORY": "2G",
"USE_AIKAR_FLAGS": "true",
#"MODS_FILE": "/extras/mods.txt",
"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,
@ -138,6 +178,7 @@ class Spawner(commands.Cog):
}
# 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:
@ -145,6 +186,7 @@ class Spawner(commands.Cog):
if world_url:
env["WORLD"] = world_url
# setting up the container
container = self.client.containers.run(
image='itzg/minecraft-server:latest',
environment=env,
@ -157,14 +199,18 @@ class Spawner(commands.Cog):
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']
server = Server(container, server_name, IPv4Address(ip), port, max_players, passwd, rcon_port)
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'''
@ -174,7 +220,7 @@ class Spawner(commands.Cog):
garde-studios.de:{port}
''',
color=color,
timestamp=datetime.now(pytz.timezone('Europe/Berlin'))
timestamp=utils.now()
)
file = discord.File("../assets/docker.png", filename="docker.png")
embed.set_thumbnail(url="attachment://docker.png")
@ -191,12 +237,17 @@ class Spawner(commands.Cog):
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=datetime.now(pytz.timezone('Europe/Berlin'))
timestamp=utils.now()
)
for container in containers:
@ -223,6 +274,11 @@ class Spawner(commands.Cog):
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()
@ -247,7 +303,7 @@ class Spawner(commands.Cog):
title="Killed",
description=f"{server_name} killed!",
color=color,
timestamp=datetime.now(pytz.timezone('Europe/Berlin'))
timestamp=utils.now()
)
file = discord.File("../assets/rip.png", filename="rip.png")

View File

@ -1,12 +1,165 @@
import discord
from discord.ext import commands
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase
from models.users import *
from mojang import API
import utils
#engine = create_engine("sqlite://user.sqlite", echo=True)
#connection = engine.connect()
class User(DeclarativeBase):
pass
color = discord.Color.from_rgb(79, 227, 119)
class UserManager(commands.Cog):
pass
def __init__(self, bot: commands.Bot):
self.bot = bot
def gen_user_info(self, user_name: str, user_id: int):
'''
Generates user output Embed
Parameters
----------
user_name: str
given user name
user_id: int
discord user id to access database info
'''
user = User.get(User.username == user_id)
embed = discord.Embed(
title=user_name if not user.is_admin else f'{user_name} (Admin)',
description=f'''
Name: {user.mc_name}
UUID: {user.mc_uuid}
Registered since {user.registration_date.strftime("%d.%m.%Y, %H:%M:%S")}
''',
color=color,
timestamp=utils.now()
)
return embed
@commands.hybrid_command(name='info')
async def info(self, ctx: commands.Context):
'''
Registers Users to internal Database
and links there minecraft username to there discord account.
Parameters
----------
ctx: commands.Context
The context of the command invocation
'''
try:
user = User.get(User.username == ctx.author.id)
await ctx.send(embed=self.gen_user_info(ctx.author.name, ctx.author.id))
except:
await ctx.send(f"{ctx.author.name} isn't registered")
@commands.hybrid_command(name='register')
async def register(self, ctx: commands.Context, minecraft_name: str):
'''
Registers Users to internal Database
and links there minecraft username to there discord account.
Parameters
----------
ctx: commands.Context
The context of the command invocation
minecraft_name: str
The minecraft user name to link with
'''
# Get minecraft uuid
api = API()
uuid = api.get_uuid(minecraft_name)
if not uuid:
await ctx.send("Username doesn't exist")
return
# build user
try:
user = User(
username=ctx.author.id,
mc_name=minecraft_name,
mc_uuid=uuid,
is_admin=False if not ctx.author.id == 418848241036165160 else True
)
user.save()
except IntegrityError:
await ctx.send("You're already registered")
return
await ctx.send(embed=self.gen_user_info(ctx.author.name, ctx.author.id))
@commands.hybrid_command(name='delete')
async def delete(self, ctx: commands.Context):
'''
Registers Users to internal Database
and links there minecraft username to there discord account.
Parameters
----------
ctx: commands.Context
The context of the command invocation
'''
try:
user = User.get(User.username == ctx.author.id)
user.delete_instance()
await ctx.send(f"Purged {ctx.author.name} from database!")
except:
await ctx.send(f"{ctx.author.name} isn't registered!")
@commands.hybrid_command(name='op')
async def op(self, ctx: commands.Context, member: discord.Member):
'''
Toggels if User is an Admin
Parameters
----------
ctx: commands.Context
The context of the command invocation
'''
user = User.get(User.username == ctx.author.id)
if not user.is_admin:
await ctx.send("You're not allowed to use this command")
return
if member.id == 418848241036165160:
await ctx.send(f"{member.name} is always an admin")
return
user = User.get(User.username == member.id)
user.is_admin = not user.is_admin
user.save()
await ctx.send(embed=self.gen_user_info(member.name, member.id))
@commands.hybrid_command(name='all')
async def all(self, ctx: commands.Context):
'''
Returns User info
Parameters
----------
ctx: commands.Context
The context of the command invocation
'''
try:
user = User.get(User.username == ctx.author.id)
if not user.is_admin:
await ctx.send("You're not allowed to use this command")
return
except:
await ctx.send(f"{ctx.author.name} isn't registered")
return
embed = discord.Embed(
title="All Users",
description="Registered Users in Database",
color=color,
timestamp=utils.now()
)
rows = User.select()
for row in rows:
member = await ctx.guild.fetch_member(row.username)
member = member if not row.is_admin else f'{member} (Admin)'
reg_date = row.registration_date.strftime("%d.%m.%Y, %H:%M:%S")
embed.add_field(name=member, value="{}\n{}\n{}\n".format(row.mc_name, row.mc_uuid, reg_date))
await ctx.send(embed=embed)

10
bot/models/base.py Normal file
View File

@ -0,0 +1,10 @@
from peewee import *
from playhouse.postgres_ext import *
import os
import datetime
db = SqliteDatabase('bot.db')
class BaseModel(Model):
class Meta:
database = db

13
bot/models/containers.py Normal file
View File

@ -0,0 +1,13 @@
from peewee import *
from models.base import BaseModel
import datetime
class Container(BaseModel):
# Container
name =
# players =
port =
# RCON
rcon_port =
rcon_pwd =

12
bot/models/users.py Normal file
View File

@ -0,0 +1,12 @@
from peewee import *
from models.base import BaseModel
import datetime
class User(BaseModel):
username = CharField(unique=True)
mc_name = CharField(unique=True)
mc_uuid = CharField(unique=True)
is_admin = BooleanField(default=False)
registration_date = DateTimeField(default=datetime.datetime.now())
User.create_table()

View File

@ -11,13 +11,18 @@ docker==7.1.0
frozenlist==1.4.1
greenlet==3.1.1
idna==3.10
Jinja2==3.1.4
MarkupSafe==3.0.1
mcrcon==0.7.0
mojang==1.1.0
multidict==6.1.0
peewee==3.17.7
psycopg2-binary==2.9.9
Pygments==2.18.0
python-dotenv==1.0.1
pytz==2024.2
requests==2.32.3
six==1.16.0
SQLAlchemy==2.0.35
transitions==0.9.2
typing_extensions==4.12.2
urllib3==2.2.3

7
bot/utils.py Normal file
View File

@ -0,0 +1,7 @@
import datetime
import pytz
now = lambda: datetime.datetime.now(pytz.timezone('Europe/Berlin'))
if __name__ == '__main__':
print(now())