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:
|
services:
|
||||||
bot:
|
bot:
|
||||||
image: registry.benrmorgan.com/minecraftstatus:latest
|
image: registry.benrmorgan.com/minecraftstatus/mstbot:latest
|
||||||
env_file:
|
env_file:
|
||||||
- ./secrets/.env
|
- ./secrets/.env
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
PRAGMA journal_mode = WAL;
|
PRAGMA journal_mode = WAL;
|
||||||
CREATE TABLE names (id INTEGER NOT NULL, name TEXT NOT NULL, UNIQUE (id, name));
|
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 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: 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 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: 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 asyncio
|
||||||
import os
|
import os
|
||||||
|
|
@ -78,21 +79,40 @@ class Admin(commands.Cog):
|
||||||
@commands.command(brief="Sets up new Minecraft server querier",
|
@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.")
|
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)
|
@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."""
|
"""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:
|
if not val_ip:
|
||||||
return
|
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:
|
try:
|
||||||
|
status = mc.status()
|
||||||
query = mc.query()
|
query = mc.query()
|
||||||
names = query.players.list
|
names = query.players.list
|
||||||
except asyncio.exceptions.TimeoutError:
|
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:
|
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:
|
else:
|
||||||
with sqlite_conn() as conn:
|
with sqlite_conn() as conn:
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
|
|
@ -114,7 +134,7 @@ class Admin(commands.Cog):
|
||||||
bot.loop.create_task(status_task(ctx.guild.id))
|
bot.loop.create_task(status_task(ctx.guild.id))
|
||||||
servers.append(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",
|
@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.")
|
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)
|
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"""
|
"""Validates IP and Port"""
|
||||||
split = ip.split(":")
|
split = ip.split(":")
|
||||||
if len(split) > 1:
|
if len(split) > 1:
|
||||||
|
|
@ -272,11 +292,15 @@ async def validate_ip_port(ctx, ip, port):
|
||||||
port = int(split[1])
|
port = int(split[1])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await log(ctx, str(port), "is not a valid port number, please try again.")
|
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):
|
if port not in range(65536):
|
||||||
await log(ctx, str(port), "is not a valid port number, please try again.")
|
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)
|
domain = re.search(r"^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,6}$", ip)
|
||||||
addr = re.search(
|
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 domain is None or domain.group(0) != ip:
|
||||||
if addr is None or addr.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.")
|
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\.)",
|
if re.search(r"(^127\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)",
|
||||||
ip) is not None:
|
ip) is not None:
|
||||||
await log(ctx, ip, "is a private IP. I won't be able to query it.")
|
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):
|
def get_server_by_id(sid: int):
|
||||||
for server in bot.guilds:
|
for server in bot.guilds:
|
||||||
|
|
@ -357,14 +381,12 @@ def sqlite_conn():
|
||||||
|
|
||||||
def getMCIP(sid: int, conn: sqlite3.Connection):
|
def getMCIP(sid: int, conn: sqlite3.Connection):
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT ip, port FROM servers WHERE id = ?",
|
"SELECT ip, port, query_port FROM servers WHERE id = ?",
|
||||||
(sid,)
|
(sid,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
ip = row[0]
|
[ip, port, query_port] = row
|
||||||
port = row[1]
|
return ip, port, query_port
|
||||||
|
|
||||||
return ip, port
|
|
||||||
|
|
||||||
|
|
||||||
def getMCNames(sid: int, conn: sqlite3.Connection):
|
def getMCNames(sid: int, conn: sqlite3.Connection):
|
||||||
|
|
@ -525,8 +547,8 @@ async def status_task(sid: int):
|
||||||
return
|
return
|
||||||
|
|
||||||
with sqlite_conn() as conn:
|
with sqlite_conn() as conn:
|
||||||
ip, port = getMCIP(sid, conn)
|
ip, port, query_port = getMCIP(sid, conn)
|
||||||
mc = JavaServer(ip, port)
|
mc = JavaServer(ip, port, query_port)
|
||||||
currServ = get_server_by_id(sid)
|
currServ = get_server_by_id(sid)
|
||||||
wait = 10
|
wait = 10
|
||||||
|
|
||||||
|
|
@ -560,7 +582,7 @@ async def status_task(sid: int):
|
||||||
for name in jNames:
|
for name in jNames:
|
||||||
aChannels = find_channels(serv=currServ, chanid=int(annChanId))
|
aChannels = find_channels(serv=currServ, chanid=int(annChanId))
|
||||||
|
|
||||||
if len(aChannels) > 0:
|
if len(aChannels):
|
||||||
if annRole is None:
|
if annRole is None:
|
||||||
await safe_send(name + " joined the game!", chan=aChannels[0])
|
await safe_send(name + " joined the game!", chan=aChannels[0])
|
||||||
else:
|
else:
|
||||||
|
|
@ -590,35 +612,36 @@ async def status_task(sid: int):
|
||||||
currServ.me: discord.PermissionOverwrite(connect=True, view_channel=True, manage_channels=True)
|
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])
|
lastIPName = await get_last_channel_name(iChannels[0])
|
||||||
|
|
||||||
ipStr = "IP: " + ip
|
|
||||||
if lastIPName != ipStr:
|
if lastIPName != ipStr:
|
||||||
print(currServ.name, "Update: IP changed!")
|
print(currServ.name, "Update: IP changed!")
|
||||||
await limiter.submit(iChannels[0].id, iChannels[0], ipStr)
|
await limiter.submit(iChannels[0].id, iChannels[0], ipStr)
|
||||||
else:
|
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])
|
lastPName = await get_last_channel_name(pChannels[0])
|
||||||
|
|
||||||
if players == -1:
|
if players == -1:
|
||||||
pStr = lastPName
|
pStr = lastPName
|
||||||
else:
|
else:
|
||||||
pStr = "Players: " + str(players) + "/" + str(max)
|
pStr = p_str_temp
|
||||||
|
|
||||||
if lastPName != pStr:
|
if lastPName != pStr:
|
||||||
print(currServ.name, "Update: Players changed!")
|
print(currServ.name, "Update: Players changed!")
|
||||||
await limiter.submit(pChannels[0].id, pChannels[0], pStr)
|
await limiter.submit(pChannels[0].id, pChannels[0], pStr)
|
||||||
else:
|
else:
|
||||||
await currServ.create_voice_channel(f"Players: {str(players)}/{str(max)}",
|
await currServ.create_voice_channel(p_str_temp,
|
||||||
overwrites=overwrites)
|
overwrites=overwrites)
|
||||||
|
|
||||||
do_show_hours = getShowHours(sid, conn)
|
do_show_hours = getShowHours(sid, conn)
|
||||||
if do_show_hours and not first_iter and lastSeconds != -1:
|
if do_show_hours and not first_iter and lastSeconds != -1:
|
||||||
tStr = "Player Hrs: " + str(round(getPersonSeconds(sid, conn)/3600))
|
tStr = "Player Hrs: " + str(round(getPersonSeconds(sid, conn)/3600))
|
||||||
if len(tChannels) > 0:
|
if len(tChannels):
|
||||||
lastTName = await get_last_channel_name(tChannels[0])
|
lastTName = await get_last_channel_name(tChannels[0])
|
||||||
|
|
||||||
if lastTName != tStr:
|
if lastTName != tStr:
|
||||||
|
|
@ -628,12 +651,15 @@ async def status_task(sid: int):
|
||||||
await currServ.create_voice_channel(tStr,
|
await currServ.create_voice_channel(tStr,
|
||||||
overwrites=overwrites)
|
overwrites=overwrites)
|
||||||
elif not do_show_hours:
|
elif not do_show_hours:
|
||||||
if len(tChannels) > 0:
|
if len(tChannels):
|
||||||
for channel in tChannels:
|
for channel in tChannels:
|
||||||
await channel.delete()
|
await channel.delete()
|
||||||
|
|
||||||
print(currServ.name, "Query:", ip, str(players) + "/" + str(max), datetime.now(timezone.utc).strftime("%H:%M:%S"),
|
ip_channel_str = f"IP: {str(await limiter.get_time_remaining(iChannels[0].id)) if len(iChannels) else 'N/A'}"
|
||||||
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)))
|
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)
|
await asyncio.sleep(wait)
|
||||||
first_iter = False
|
first_iter = False
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue