Workflow?
This commit is contained in:
parent
d67006a008
commit
9235d6ec07
4 changed files with 113 additions and 33 deletions
54
.forgejo/workflows/deploy.yml
Normal file
54
.forgejo/workflows/deploy.yml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
name: Build & Deploy MSTBot
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "v*.*.*" # only match version tags like v1.0.0
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
name: Build, push & deploy
|
||||
runs-on: docker-cli-node24
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Build & push image
|
||||
run: |
|
||||
IMAGE="${{ vars.REGISTRY_URL }}/minecraftstatus/mstbot"
|
||||
|
||||
docker build --network=host -t $IMAGE:latest .
|
||||
docker push $IMAGE:latest
|
||||
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags ]]; then
|
||||
VERSION_TAG="${{ github.ref_name }}"
|
||||
docker tag $IMAGE:latest $IMAGE:$VERSION_TAG
|
||||
docker push $IMAGE:$VERSION_TAG
|
||||
fi
|
||||
|
||||
- name: Deploy to LXC
|
||||
uses: https://github.com/appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ vars.MSTBOT_HOST }}
|
||||
username: ${{ vars.MSTBOT_USER }}
|
||||
key: ${{ secrets.MSTBOT_SSH_KEY }}
|
||||
script: |
|
||||
set -euo pipefail
|
||||
cd /opt/mstbot
|
||||
curl -f -H "Authorization: token ${{ secrets.FORGEJO_TOKEN }}" -o docker-compose.yml "https://git.benrmorgan.com/api/v1/repos/bMorgan/MinecraftStatus/raw/main/docker-compose.yml"
|
||||
docker compose down
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
|
||||
- name: Create Release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: actions/forgejo-release@v2.7.3
|
||||
with:
|
||||
direction: upload
|
||||
title: ${{ github.ref_name }}
|
||||
tag: ${{ github.ref_name }}
|
||||
token: ${{ secrets.FORGEJO_TOKEN }}
|
||||
release-dir: .
|
||||
release-notes-assistant: true
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
services:
|
||||
bot:
|
||||
image: registry.benrmorgan.com/minecraftstatus:latest
|
||||
image: registry.benrmorgan.com/minecraftstatus/mstbot:latest
|
||||
env_file:
|
||||
- ./secrets/.env
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
PRAGMA journal_mode = WAL;
|
||||
CREATE TABLE names (id INTEGER NOT NULL, name TEXT NOT NULL, UNIQUE (id, name));
|
||||
CREATE TABLE times (id INTEGER NOT NULL, name TEXT NOT NULL, seconds REAL NOT NULL DEFAULT 0, UNIQUE (id, name));
|
||||
CREATE TABLE servers (id INTEGER PRIMARY KEY, ip TEXT NOT NULL, port INTEGER NOT NULL DEFAULT 25565, last_query TIMESTAMP, show_hours BOOLEAN NOT NULL DEFAULT FALSE, announce_joins BOOLEAN NOT NULL DEFAULT FALSE, announce_joins_id INTEGER) WITHOUT ROWID;
|
||||
CREATE TABLE servers (id INTEGER PRIMARY KEY, ip TEXT NOT NULL, port INTEGER NOT NULL DEFAULT 25565, query_port INTEGER NOT NULL DEFAULT 25565, last_query TIMESTAMP, show_hours BOOLEAN NOT NULL DEFAULT FALSE, announce_joins BOOLEAN NOT NULL DEFAULT FALSE, announce_joins_id INTEGER) WITHOUT ROWID;
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
# TODO: save invite link to README and show query port enable instructions
|
||||
# TODO: Feature: allow discord users to associate their mc usernames, change server nickname?
|
||||
# TODO: Feature: allow other users to query playtime by username, default to their own. This and above require new columns in names table.
|
||||
# TODO: If on startup the schema has changed, create new db and attempt to migrate old data into new schema
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
|
@ -78,21 +79,40 @@ class Admin(commands.Cog):
|
|||
@commands.command(brief="Sets up new Minecraft server querier",
|
||||
description="Sets up new Minecraft server querier. Removes previous querier, if applicable. Use a valid domain name or IP address. The default port (25565) is used unless otherwise specified. Specify a channel ID to announce player joins there.")
|
||||
@commands.has_permissions(administrator=True)
|
||||
async def setup(self, ctx: discord.ext.commands.context.Context, ip: str, port: int = 25565, annChanID: Union[int, str, None] = None):
|
||||
async def setup(self, ctx: discord.ext.commands.context.Context, ip: str, port: int = 25565, query_port: int = 25565, annChanID: Union[int, str, None] = None):
|
||||
"""Sets up a new Minecraft server querier."""
|
||||
val_ip, val_port = await validate_ip_port(ctx, ip, port)
|
||||
val_ip, val_port, val_query_port = await validate_ip_port(ctx, ip, port, query_port)
|
||||
if not val_ip:
|
||||
return
|
||||
|
||||
mc = JavaServer.lookup(f"{val_ip}:{val_port}")
|
||||
server_port_ip = f"{val_ip}:{val_port}"
|
||||
await log(ctx, f"Attempting to establish connection to {server_port_ip} with query port {val_query_port}...")
|
||||
mc = JavaServer(val_ip, val_port, query_port=val_query_port)
|
||||
|
||||
status = None
|
||||
query = None
|
||||
try:
|
||||
status = mc.status()
|
||||
query = mc.query()
|
||||
names = query.players.list
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
await log(ctx, "Setup query error, query port enabled?")
|
||||
await log(ctx, (
|
||||
"The setup Query protocal ping failed, but the Minecraft server appears to be accessible.\n"
|
||||
"Does the server.properties file contain `enable-query=true`? Changes to server.properties do not take effect until the Minecraft server is restarted.\n"
|
||||
f"Is UDP port {val_query_port} forwarded and open on the firewall?"
|
||||
))
|
||||
if status is None:
|
||||
await log(ctx, (
|
||||
"A Status protocol ping also failed.\n"
|
||||
"Does the server.properties file contain `enable-status=true`?\n"
|
||||
f"Is TCP port {val_port} forwarded and open on the firewall?"
|
||||
))
|
||||
except ConnectionRefusedError:
|
||||
await log(ctx, "Setup query failed, server down?")
|
||||
await log(ctx, (
|
||||
f"Setup query failed. I could not reach a Minecraft server at {server_port_ip}.\n"
|
||||
f"Is TCP port {val_port} forwarded and open on the firewall?\n"
|
||||
f"Can you and a friend (on a different network) connect to and play on {server_port_ip}?"
|
||||
))
|
||||
else:
|
||||
with sqlite_conn() as conn:
|
||||
conn.execute("""
|
||||
|
|
@ -114,7 +134,7 @@ class Admin(commands.Cog):
|
|||
bot.loop.create_task(status_task(ctx.guild.id))
|
||||
servers.append(ctx.guild.id)
|
||||
|
||||
await log(ctx, f"Setup new server query with IP: {val_ip}")
|
||||
await log(ctx, f"Successfully set up a new server querier for {server_port_ip}!")
|
||||
|
||||
@commands.command(brief="Removes querier and status channels",
|
||||
description="Removes querier and status channels. Deletes your guild's data from my server. Data deletion occurs automatically when I am removed from your guild.")
|
||||
|
|
@ -262,7 +282,7 @@ async def log(ctx: commands.Context, *msg: str):
|
|||
await safe_send(" ".join(msg), ctx=ctx)
|
||||
|
||||
|
||||
async def validate_ip_port(ctx, ip, port):
|
||||
async def validate_ip_port(ctx, ip, port, query_port):
|
||||
"""Validates IP and Port"""
|
||||
split = ip.split(":")
|
||||
if len(split) > 1:
|
||||
|
|
@ -272,11 +292,15 @@ async def validate_ip_port(ctx, ip, port):
|
|||
port = int(split[1])
|
||||
except ValueError:
|
||||
await log(ctx, str(port), "is not a valid port number, please try again.")
|
||||
return None, None
|
||||
return None, None, None
|
||||
|
||||
if port not in range(65536):
|
||||
await log(ctx, str(port), "is not a valid port number, please try again.")
|
||||
return None, None
|
||||
return None, None, None
|
||||
|
||||
if query_port not in range(65536):
|
||||
await log(ctx, str(query_port), "is not a valid port number, please try again.")
|
||||
return None, None, None
|
||||
|
||||
domain = re.search(r"^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,6}$", ip)
|
||||
addr = re.search(
|
||||
|
|
@ -285,14 +309,14 @@ async def validate_ip_port(ctx, ip, port):
|
|||
if domain is None or domain.group(0) != ip:
|
||||
if addr is None or addr.group(0) != ip:
|
||||
await log(ctx, ip, "is not a valid domain or IP address, please try again.")
|
||||
return None, None
|
||||
return None, None, None
|
||||
|
||||
if re.search(r"(^127\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)",
|
||||
ip) is not None:
|
||||
await log(ctx, ip, "is a private IP. I won't be able to query it.")
|
||||
return None, None
|
||||
return None, None, None
|
||||
|
||||
return ip, port
|
||||
return ip, port, query_port
|
||||
|
||||
def get_server_by_id(sid: int):
|
||||
for server in bot.guilds:
|
||||
|
|
@ -357,14 +381,12 @@ def sqlite_conn():
|
|||
|
||||
def getMCIP(sid: int, conn: sqlite3.Connection):
|
||||
row = conn.execute(
|
||||
"SELECT ip, port FROM servers WHERE id = ?",
|
||||
"SELECT ip, port, query_port FROM servers WHERE id = ?",
|
||||
(sid,)
|
||||
).fetchone()
|
||||
|
||||
ip = row[0]
|
||||
port = row[1]
|
||||
|
||||
return ip, port
|
||||
[ip, port, query_port] = row
|
||||
return ip, port, query_port
|
||||
|
||||
|
||||
def getMCNames(sid: int, conn: sqlite3.Connection):
|
||||
|
|
@ -525,8 +547,8 @@ async def status_task(sid: int):
|
|||
return
|
||||
|
||||
with sqlite_conn() as conn:
|
||||
ip, port = getMCIP(sid, conn)
|
||||
mc = JavaServer(ip, port)
|
||||
ip, port, query_port = getMCIP(sid, conn)
|
||||
mc = JavaServer(ip, port, query_port)
|
||||
currServ = get_server_by_id(sid)
|
||||
wait = 10
|
||||
|
||||
|
|
@ -560,7 +582,7 @@ async def status_task(sid: int):
|
|||
for name in jNames:
|
||||
aChannels = find_channels(serv=currServ, chanid=int(annChanId))
|
||||
|
||||
if len(aChannels) > 0:
|
||||
if len(aChannels):
|
||||
if annRole is None:
|
||||
await safe_send(name + " joined the game!", chan=aChannels[0])
|
||||
else:
|
||||
|
|
@ -590,35 +612,36 @@ async def status_task(sid: int):
|
|||
currServ.me: discord.PermissionOverwrite(connect=True, view_channel=True, manage_channels=True)
|
||||
}
|
||||
|
||||
if len(iChannels) > 0:
|
||||
ipStr = f"IP: {ip}"
|
||||
if len(iChannels):
|
||||
lastIPName = await get_last_channel_name(iChannels[0])
|
||||
|
||||
ipStr = "IP: " + ip
|
||||
if lastIPName != ipStr:
|
||||
print(currServ.name, "Update: IP changed!")
|
||||
await limiter.submit(iChannels[0].id, iChannels[0], ipStr)
|
||||
else:
|
||||
await currServ.create_voice_channel("IP: " + ip, overwrites=overwrites)
|
||||
await currServ.create_voice_channel(ipStr, overwrites=overwrites)
|
||||
|
||||
if len(pChannels) > 0:
|
||||
p_str_temp = f"Players: {str(players)}/{str(max)}"
|
||||
if len(pChannels):
|
||||
lastPName = await get_last_channel_name(pChannels[0])
|
||||
|
||||
if players == -1:
|
||||
pStr = lastPName
|
||||
else:
|
||||
pStr = "Players: " + str(players) + "/" + str(max)
|
||||
pStr = p_str_temp
|
||||
|
||||
if lastPName != pStr:
|
||||
print(currServ.name, "Update: Players changed!")
|
||||
await limiter.submit(pChannels[0].id, pChannels[0], pStr)
|
||||
else:
|
||||
await currServ.create_voice_channel(f"Players: {str(players)}/{str(max)}",
|
||||
await currServ.create_voice_channel(p_str_temp,
|
||||
overwrites=overwrites)
|
||||
|
||||
do_show_hours = getShowHours(sid, conn)
|
||||
if do_show_hours and not first_iter and lastSeconds != -1:
|
||||
tStr = "Player Hrs: " + str(round(getPersonSeconds(sid, conn)/3600))
|
||||
if len(tChannels) > 0:
|
||||
if len(tChannels):
|
||||
lastTName = await get_last_channel_name(tChannels[0])
|
||||
|
||||
if lastTName != tStr:
|
||||
|
|
@ -628,12 +651,15 @@ async def status_task(sid: int):
|
|||
await currServ.create_voice_channel(tStr,
|
||||
overwrites=overwrites)
|
||||
elif not do_show_hours:
|
||||
if len(tChannels) > 0:
|
||||
if len(tChannels):
|
||||
for channel in tChannels:
|
||||
await channel.delete()
|
||||
|
||||
print(currServ.name, "Query:", ip, str(players) + "/" + str(max), datetime.now(timezone.utc).strftime("%H:%M:%S"),
|
||||
str(deltaSeconds), "IP:", str(await limiter.get_time_remaining(iChannels[0].id)), "P:", str(await limiter.get_time_remaining(pChannels[0].id)), "T:", str(await limiter.get_time_remaining(tChannels[0].id)))
|
||||
|
||||
ip_channel_str = f"IP: {str(await limiter.get_time_remaining(iChannels[0].id)) if len(iChannels) else 'N/A'}"
|
||||
p_channel_str = f"P: {str(await limiter.get_time_remaining(pChannels[0].id)) if len(pChannels) else 'N/A'}"
|
||||
t_channel_str = f"T: {str(await limiter.get_time_remaining(tChannels[0].id)) if len(tChannels) else 'N/A'}"
|
||||
print(currServ.name, "Query:", ip, p_str_temp, datetime.now(timezone.utc).strftime("%H:%M:%S"),
|
||||
str(deltaSeconds), ip_channel_str, p_channel_str, t_channel_str)
|
||||
|
||||
await asyncio.sleep(wait)
|
||||
first_iter = False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue