Compare commits
44 Commits
afef3ae674
...
511b7b038e
Author | SHA1 | Date | |
---|---|---|---|
511b7b038e | |||
27a2a1f558 | |||
6286887632 | |||
f6aa67c739 | |||
19ec17a69a | |||
8344bfa3b5 | |||
512254a05d | |||
7bb2c8dbef | |||
09cfb2ac58 | |||
cffe8546c1 | |||
cf580d741d | |||
8bfcf7eca4 | |||
c302b54eae | |||
78d2c7a850 | |||
226c05526d | |||
c947aa2148 | |||
95e8672fd5 | |||
4e42a07b27 | |||
f7f2401174 | |||
3a85af4028 | |||
ed22e552a1 | |||
21bd1589a4 | |||
5e216862a6 | |||
60ad27126b | |||
7a18ebc22d | |||
c09acbe9e3 | |||
82d2eb6ed2 | |||
82fcedd5a4 | |||
fa1ce10b0f | |||
3bc52db5aa | |||
3a286251f4 | |||
271f067eb3 | |||
c291f486da | |||
f150bf9ddb | |||
258544fad5 | |||
dd94b878ca | |||
40e0f199eb | |||
9faa3b14d3 | |||
308fa4d197 | |||
2ca87c1227 | |||
de511f67c7 | |||
7642da6b43 | |||
cb745bc8b1 | |||
0e566d9042 |
4
.bumpversion.cfg
Normal file
4
.bumpversion.cfg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[bumpversion]
|
||||||
|
current_version = 0.0.2
|
||||||
|
commit = True
|
||||||
|
tag = True
|
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.gitignore
|
||||||
|
.env
|
||||||
|
venv/
|
||||||
|
logs/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.gitea/
|
||||||
|
.vscode/
|
62
.gitea/workflows/docker-build.yaml
Normal file
62
.gitea/workflows/docker-build.yaml
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
name: Build and Push Docker Image
|
||||||
|
run-name: ${{ gitea.actor }} is building and pushing a Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- staging
|
||||||
|
- dev
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- staging
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
retries: 2
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Install bump2version
|
||||||
|
run: pip install bump2version
|
||||||
|
|
||||||
|
- name: Get current version from bump2version
|
||||||
|
id: version
|
||||||
|
run: echo "VERSION=$(bump2version --dry-run --list patch | grep current_version | sed -r s,"^.*=",,)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set Docker tag based on branch
|
||||||
|
id: tag
|
||||||
|
run: |
|
||||||
|
# master branch uses specific version tagging, others use the branch name
|
||||||
|
if [[ "${{ gitea.ref_name }}" == "master" ]]; then
|
||||||
|
TAG="${{ env.VERSION }}"
|
||||||
|
else
|
||||||
|
TAG="${{ gitea.ref_name }}"
|
||||||
|
fi
|
||||||
|
echo "TAG=$TAG" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: |
|
||||||
|
docker build -t spiffo:${{ env.TAG }} .
|
||||||
|
|
||||||
|
- name: Login to Docker registry
|
||||||
|
run: echo ${{ secrets.DOCKER_TOKEN }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
|
||||||
|
|
||||||
|
- name: Tag & Push Docker image
|
||||||
|
run: |
|
||||||
|
# Push the branch-specific or version-specific tag
|
||||||
|
docker tag spiffo:${{ env.TAG }} xordk/spiffo:${{ env.TAG }}
|
||||||
|
docker push xordk/spiffo:${{ env.TAG }}
|
||||||
|
|
||||||
|
# If on master, push an additional "latest" tag
|
||||||
|
if [[ "${{ gitea.ref_name }}" == "master" ]]; then
|
||||||
|
docker tag spiffo:${{ env.TAG }} xordk/spiffo:latest
|
||||||
|
docker push xordk/spiffo:latest
|
||||||
|
fi
|
23
CHANGELOG.md
Normal file
23
CHANGELOG.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.0.2] - 2024-12-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Commands for accessing remote console.
|
||||||
|
- Shorthand commands for commonly used rcon commands.
|
||||||
|
- Online user count that is automatically updated and set as the bot's activity presence.
|
||||||
|
- Console reader for handling events in-game.
|
||||||
|
- Announce when user has joined/left the server.
|
||||||
|
- Mod update checker and manager, with announcements for server restart.
|
||||||
|
|
||||||
|
## [0.0.1] - 2024-12-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Initial project files and repository
|
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
FROM python:3.12.5-slim-bullseye
|
||||||
|
|
||||||
|
# python related environment variables
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# install python dependencies
|
||||||
|
COPY requirements.txt /app/
|
||||||
|
RUN pip install --upgrade pip
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
|
CMD ["python", "bot.py"]
|
5
bot.py
5
bot.py
@ -19,6 +19,7 @@ load_dotenv(override=True)
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
DATA_DIR = BASE_DIR / "data"
|
||||||
|
|
||||||
|
|
||||||
class DiscordBot(commands.Bot):
|
class DiscordBot(commands.Bot):
|
||||||
@ -26,10 +27,14 @@ class DiscordBot(commands.Bot):
|
|||||||
Represents a Discord bot.
|
Represents a Discord bot.
|
||||||
Contains controls to interact with the bot via the Discord API.
|
Contains controls to interact with the bot via the Discord API.
|
||||||
"""
|
"""
|
||||||
|
in_game_channel_id: int
|
||||||
|
steam_api_key: str
|
||||||
rcon_details: dict
|
rcon_details: dict
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(command_prefix="-", intents=Intents.all())
|
super().__init__(command_prefix="-", intents=Intents.all())
|
||||||
|
self.in_game_channel_id = int(getenv("SPIFFO__DISCORD_CHANNEL_ID"))
|
||||||
|
self.steam_api_key = getenv("SPIFFO__STEAM_API_KEY")
|
||||||
self.rcon_details = {
|
self.rcon_details = {
|
||||||
"host": getenv("SPIFFO__RCON_HOST"),
|
"host": getenv("SPIFFO__RCON_HOST"),
|
||||||
"port": getenv("SPIFFO__RCON_PORT"),
|
"port": getenv("SPIFFO__RCON_PORT"),
|
||||||
|
52
cogs/activity.py
Normal file
52
cogs/activity.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"""
|
||||||
|
Handles the activity displayed on the bot's Discord profile.
|
||||||
|
The current intent is to display the server's player count as the activity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from discord import Game, Status
|
||||||
|
from discord.ext import commands, tasks
|
||||||
|
from rcon.source import rcon
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityCog(commands.Cog):
|
||||||
|
"""
|
||||||
|
Handles the bot's profile activity.
|
||||||
|
"""
|
||||||
|
def __init__(self, bot: commands.Bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_ready(self):
|
||||||
|
if not self.update_activity.is_running():
|
||||||
|
self.update_activity.start()
|
||||||
|
|
||||||
|
@tasks.loop(seconds=30)
|
||||||
|
async def update_activity(self):
|
||||||
|
"""
|
||||||
|
Update the bot's profile activity on an interval of 30 seconds.
|
||||||
|
"""
|
||||||
|
log.debug("updating activity")
|
||||||
|
|
||||||
|
players_message = await rcon("players", **self.bot.rcon_details)
|
||||||
|
re_match = re.search(r"Players connected \((\d+)\):\s*", players_message)
|
||||||
|
|
||||||
|
if not re_match:
|
||||||
|
log.error("Failed to parse player count from rcon response: %s", players_message)
|
||||||
|
return
|
||||||
|
|
||||||
|
players_count = int(re_match.group(1))
|
||||||
|
activity = Game(name=f"with {players_count} survivor{'s' if players_count != 1 else ''}!")
|
||||||
|
await self.bot.change_presence(activity=activity, status=Status.online)
|
||||||
|
|
||||||
|
log.debug("player count in activity updated to: %s", players_count)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot: commands.Bot):
|
||||||
|
cog = ActivityCog(bot)
|
||||||
|
await bot.add_cog(cog)
|
||||||
|
log.info("Added %s cog", cog.__class__.__name__)
|
@ -36,7 +36,6 @@ COMMANDS_BLACKLIST = [
|
|||||||
"teleport",
|
"teleport",
|
||||||
"teleportto",
|
"teleportto",
|
||||||
"thunder"
|
"thunder"
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -51,6 +50,19 @@ class CommandCog(commands.Cog):
|
|||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
log.info("%s cog is ready", self.__class__.__name__)
|
log.info("%s cog is ready", self.__class__.__name__)
|
||||||
|
|
||||||
|
admin_group = app_commands.Group(
|
||||||
|
name="admin",
|
||||||
|
description="Admin-only commands",
|
||||||
|
default_permissions=Permissions.all(),
|
||||||
|
guild_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@admin_group.command(name="force-mod-restart-event")
|
||||||
|
async def admin_force_mod_restart_event(self, inter: Interaction):
|
||||||
|
await inter.response.send_message(content="Acknowledged", ephemeral=True)
|
||||||
|
cog = self.bot.get_cog("ConsoleCog")
|
||||||
|
await cog.handle_mod_needs_update("")
|
||||||
|
|
||||||
rcon_group = app_commands.Group(
|
rcon_group = app_commands.Group(
|
||||||
name="rcon",
|
name="rcon",
|
||||||
description="Remote console commands",
|
description="Remote console commands",
|
||||||
@ -59,17 +71,13 @@ class CommandCog(commands.Cog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def send_rcon_response(self, inter: Interaction, command: str) -> str:
|
async def send_rcon_response(self, inter: Interaction, command: str) -> str:
|
||||||
# if inter.user.id != 377453890523627522:
|
await inter.response.defer()
|
||||||
# log.warning("Bad user tried to send rcon command: '%s', '%s'", inter.user.name, command)
|
|
||||||
# await inter.response.send_message("Permissions Error", ephemeral=True)
|
|
||||||
# return
|
|
||||||
|
|
||||||
if command in COMMANDS_BLACKLIST:
|
if command in COMMANDS_BLACKLIST:
|
||||||
log.warning("Attempt to use banned command: '%s', '%s'", inter.user.name, command)
|
log.warning("Attempt to use banned command: '%s', '%s'", inter.user.name, command)
|
||||||
await inter.response.send_message("Blacklisted command", ephemeral=True)
|
await inter.followup.send("Blacklisted command")
|
||||||
return
|
return
|
||||||
|
|
||||||
await inter.response.defer()
|
|
||||||
response = await rcon(command, **self.bot.rcon_details)
|
response = await rcon(command, **self.bot.rcon_details)
|
||||||
await inter.followup.send(content=response)
|
await inter.followup.send(content=response)
|
||||||
|
|
||||||
|
127
cogs/console.py
Normal file
127
cogs/console.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"""
|
||||||
|
Reads and handles updates in the server console file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
from os import getenv
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
from rcon.source import rcon
|
||||||
|
from discord import Embed, Colour
|
||||||
|
from discord.ext import commands, tasks
|
||||||
|
|
||||||
|
ZOMBOID_FOLDER_PATH = Path(getenv("SPIFFO__ZOMBOID_FOLDER_PATH"))
|
||||||
|
CONSOLE_FILE_PATH = ZOMBOID_FOLDER_PATH / "server-console.txt"
|
||||||
|
assert ZOMBOID_FOLDER_PATH.exists()
|
||||||
|
assert CONSOLE_FILE_PATH.exists()
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleCog(commands.Cog):
|
||||||
|
"""
|
||||||
|
Reads and handles the server-console.txt file.
|
||||||
|
"""
|
||||||
|
_last_line_number = 0
|
||||||
|
|
||||||
|
def __init__(self, bot: commands.Bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.monitor_console.start()
|
||||||
|
|
||||||
|
@tasks.loop(seconds=1)
|
||||||
|
async def monitor_console(self):
|
||||||
|
"""
|
||||||
|
Check the latest version of the console log file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not CONSOLE_FILE_PATH.exists():
|
||||||
|
self.monitor_console.cancel()
|
||||||
|
raise FileNotFoundError("Server console file doesn't exist, task cancelled.")
|
||||||
|
|
||||||
|
async with aiofiles.open(CONSOLE_FILE_PATH, "r", encoding="utf-8") as file:
|
||||||
|
|
||||||
|
# If we are at 0, restarting the bot would cause rapid fire of all log lines,
|
||||||
|
# instead lets grab the latest line, to prevent spam.
|
||||||
|
if self._last_line_number == 0:
|
||||||
|
await file.seek(0, 2)
|
||||||
|
self._last_line_number = await file.tell()
|
||||||
|
return
|
||||||
|
|
||||||
|
await file.seek(self._last_line_number)
|
||||||
|
lines = await file.readlines()
|
||||||
|
if not lines:
|
||||||
|
log.debug("no new lines to read")
|
||||||
|
return
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
await self.process_console_line(line.strip())
|
||||||
|
|
||||||
|
self._last_line_number = await file.tell()
|
||||||
|
|
||||||
|
async def process_console_line(self, line: str):
|
||||||
|
"""
|
||||||
|
Determine how to handle the given line from the server console.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if "CheckModsNeedUpdate: Mods need update" in line:
|
||||||
|
await self.handle_mod_needs_update(line)
|
||||||
|
|
||||||
|
elif "ConnectionManager: [fully-connected]" in line:
|
||||||
|
cog = self.bot.get_cog("PlayersCog")
|
||||||
|
await cog.handle_player_connected(line)
|
||||||
|
|
||||||
|
elif "ConnectionManager: [disconnect]" in line:
|
||||||
|
cog = self.bot.get_cog("PlayersCog")
|
||||||
|
await cog.handle_player_disconnected(line)
|
||||||
|
|
||||||
|
async def alert_and_wait_for_restart(self, intervals_ms: list[int], reason: str):
|
||||||
|
for interval_ms in intervals_ms:
|
||||||
|
seconds_remaining = interval_ms / 1000
|
||||||
|
log.info("Planned restart in %s seconds, reason: %s", seconds_remaining, reason)
|
||||||
|
await rcon(f"Planned restart in {seconds_remaining} seconds, reason: {reason}", **self.bot.rcon_details)
|
||||||
|
while seconds_remaining > 0:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
seconds_remaining -= 1
|
||||||
|
|
||||||
|
async def kick_all_users(self):
|
||||||
|
players_message = await rcon("players", **self.bot.rcon_details)
|
||||||
|
re_pattern = r"Players connected \(\d+\):\s*(-[\w-]+(?:\s*[\w-]+)*)"
|
||||||
|
re_match = re.search(re_pattern, players_message)
|
||||||
|
if not re_match:
|
||||||
|
log.info("No players found to kick")
|
||||||
|
|
||||||
|
usernames_string = re_match.group(1)
|
||||||
|
for i, username in enumerate(usernames_string.split("-")):
|
||||||
|
if not username:
|
||||||
|
continue
|
||||||
|
|
||||||
|
await rcon(f'kickuser "{username}" -r "Server is Updating. Kicked to ensure your progress is saved"', **self.bot.rcon_details)
|
||||||
|
|
||||||
|
log.info("Kicked '%s' users for restart", i + 1)
|
||||||
|
|
||||||
|
async def handle_mod_needs_update(self, line: str):
|
||||||
|
"""
|
||||||
|
Report when one or more mods need to be updated.
|
||||||
|
"""
|
||||||
|
log.info("one or more mods are outdated")
|
||||||
|
|
||||||
|
await self.alert_and_wait_for_restart(
|
||||||
|
intervals_ms=[300000, 60000, 6000],
|
||||||
|
reason="Mod needs updating"
|
||||||
|
)
|
||||||
|
await self.kick_all_users()
|
||||||
|
await rcon("save", **self.bot.rcon_details)
|
||||||
|
await rcon("quit", **self.bot.rcon_details)
|
||||||
|
|
||||||
|
channel = self.bot.get_channel(self.bot.in_game_channel_id)
|
||||||
|
channel = await self.bot.fetch_channel(self.bot.in_game_channel_id) if not channel else channel
|
||||||
|
await channel.send(content="The server is currently being restarted for mod updates.")
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot: commands.Bot):
|
||||||
|
cog = ConsoleCog(bot)
|
||||||
|
await bot.add_cog(cog)
|
||||||
|
log.info("Added %s cog", cog.__class__.__name__)
|
147
cogs/players.py
Normal file
147
cogs/players.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
"""
|
||||||
|
Handles tasks related to in-game players, such as connect/disconnect alerts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from discord import Embed, Colour
|
||||||
|
from discord.ext import commands, tasks
|
||||||
|
from rcon.source import rcon
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class SteamProfileSummary:
|
||||||
|
steam_id: int
|
||||||
|
profile_name: str
|
||||||
|
url: str
|
||||||
|
avatar_url: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class ZomboidUser:
|
||||||
|
guid: str
|
||||||
|
ip: str
|
||||||
|
steam_id: str
|
||||||
|
access: str
|
||||||
|
username: str
|
||||||
|
connection_type: str
|
||||||
|
steam_profile_summary: SteamProfileSummary | None = None
|
||||||
|
|
||||||
|
async def fetch_steam_profile(self, steam_api_key: str):
|
||||||
|
if not steam_api_key:
|
||||||
|
log.warning("No steam API key, can't get profile summary.")
|
||||||
|
return
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(url=f"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key={steam_api_key}&steamids={self.steam_id}")
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
all_data = response.json()
|
||||||
|
user_data = all_data["response"]["players"][0]
|
||||||
|
|
||||||
|
log.debug("fetched user data for: %s", self.steam_id)
|
||||||
|
|
||||||
|
self.steam_profile_summary = SteamProfileSummary(
|
||||||
|
steam_id=user_data["steamid"],
|
||||||
|
profile_name=user_data["personaname"],
|
||||||
|
url=user_data["profileurl"],
|
||||||
|
avatar_url=user_data["avatarfull"]
|
||||||
|
)
|
||||||
|
|
||||||
|
log.debug("successfully parsed steam profile summary for: %s", self.steam_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def embed(self) -> Embed:
|
||||||
|
if not self.steam_profile_summary:
|
||||||
|
raise ValueError("You must fetch the steam_profile_summary before creating an embed.")
|
||||||
|
|
||||||
|
embed = Embed(
|
||||||
|
title="Player",
|
||||||
|
description=(
|
||||||
|
f"{self.username} ([{self.steam_profile_summary.profile_name}]({self.steam_profile_summary.url}))\n"
|
||||||
|
"kills: ???\n"
|
||||||
|
"Playtime: ???"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
embed.set_thumbnail(url=self.steam_profile_summary.avatar_url)
|
||||||
|
return embed
|
||||||
|
|
||||||
|
|
||||||
|
class PlayersCog(commands.Cog):
|
||||||
|
"""
|
||||||
|
Handles tasks related to in-game players.
|
||||||
|
"""
|
||||||
|
def __init__(self, bot: commands.Bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
async def handle_player_connected(self, line: str):
|
||||||
|
"""
|
||||||
|
Report when a user has joined the server into a specified Discord channel.
|
||||||
|
Example of line:
|
||||||
|
ConnectionManager: [fully-connected] "" connection: guid=*** ip=*** steam-id=*** access=admin username="corbz" connection-type="UDPRakNet"
|
||||||
|
"""
|
||||||
|
re_pattern = r"guid=(\d+)\s+ip=([\d\.]+)\s+steam-id=(\d+)\s+access=(\w*)\s+username=\"([^\"]+)\"\s+connection-type=\"([^\"]+)\""
|
||||||
|
re_match = re.search(re_pattern, line)
|
||||||
|
if not re_match:
|
||||||
|
log.warning("failed to parse player data: %s", line)
|
||||||
|
return
|
||||||
|
|
||||||
|
user = ZomboidUser(
|
||||||
|
guid=re_match.group(1),
|
||||||
|
ip=re_match.group(2),
|
||||||
|
steam_id=re_match.group(3),
|
||||||
|
access=re_match.group(4),
|
||||||
|
username=re_match.group(5),
|
||||||
|
connection_type=re_match.group(6)
|
||||||
|
)
|
||||||
|
await user.fetch_steam_profile(self.bot.steam_api_key)
|
||||||
|
|
||||||
|
channel = self.bot.get_channel(self.bot.in_game_channel_id)
|
||||||
|
channel = await self.bot.fetch_channel(self.bot.in_game_channel_id) if not channel else channel
|
||||||
|
|
||||||
|
embed = user.embed
|
||||||
|
embed.title = "Player Has Connected"
|
||||||
|
embed.colour = Colour.brand_green()
|
||||||
|
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
|
||||||
|
async def handle_player_disconnected(self, line: str):
|
||||||
|
"""
|
||||||
|
Report when a user has left the server into a specified Discord channel.
|
||||||
|
Example of line:
|
||||||
|
ConnectionManager: [disconnect] "receive-disconnect" connection: guid=*** ip=*** steam-id=*** access=admin username="corbz" connection-type="Disconnected"
|
||||||
|
"""
|
||||||
|
re_pattern = r"guid=(\d+)\s+ip=([\d\.]+)\s+steam-id=(\d+)\s+access=(\w*)\s+username=\"([^\"]+)\"\s+connection-type=\"([^\"]+)\""
|
||||||
|
re_match = re.search(re_pattern, line)
|
||||||
|
if not re_match:
|
||||||
|
log.warning("failed to parse player data: %s", line)
|
||||||
|
return
|
||||||
|
|
||||||
|
user = ZomboidUser(
|
||||||
|
guid=re_match.group(1),
|
||||||
|
ip=re_match.group(2),
|
||||||
|
steam_id=re_match.group(3),
|
||||||
|
access=re_match.group(4),
|
||||||
|
username=re_match.group(5),
|
||||||
|
connection_type=re_match.group(6)
|
||||||
|
)
|
||||||
|
await user.fetch_steam_profile(self.bot.steam_api_key)
|
||||||
|
|
||||||
|
channel = self.bot.get_channel(self.bot.in_game_channel_id)
|
||||||
|
channel = await self.bot.fetch_channel(self.bot.in_game_channel_id) if not channel else channel
|
||||||
|
|
||||||
|
embed = user.embed
|
||||||
|
embed.title = "Player Has Disconnected"
|
||||||
|
embed.colour = Colour.brand_red()
|
||||||
|
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
|
||||||
|
async def setup(bot: commands.Bot):
|
||||||
|
cog = PlayersCog(bot)
|
||||||
|
await bot.add_cog(cog)
|
||||||
|
log.info("Added %s cog", cog.__class__.__name__)
|
@ -1,12 +1,21 @@
|
|||||||
|
aiofiles==24.1.0
|
||||||
aiohappyeyeballs==2.4.4
|
aiohappyeyeballs==2.4.4
|
||||||
aiohttp==3.11.9
|
aiohttp==3.11.9
|
||||||
aiosignal==1.3.1
|
aiosignal==1.3.1
|
||||||
|
anyio==4.7.0
|
||||||
attrs==24.2.0
|
attrs==24.2.0
|
||||||
|
bump2version==1.0.1
|
||||||
|
certifi==2024.8.30
|
||||||
discord.py==2.4.0
|
discord.py==2.4.0
|
||||||
frozenlist==1.5.0
|
frozenlist==1.5.0
|
||||||
|
h11==0.14.0
|
||||||
|
httpcore==1.0.7
|
||||||
|
httpx==0.28.0
|
||||||
idna==3.10
|
idna==3.10
|
||||||
multidict==6.1.0
|
multidict==6.1.0
|
||||||
propcache==0.2.1
|
propcache==0.2.1
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
rcon==2.4.9
|
rcon==2.4.9
|
||||||
|
sniffio==1.3.1
|
||||||
|
typing_extensions==4.12.2
|
||||||
yarl==1.18.3
|
yarl==1.18.3
|
||||||
|
Reference in New Issue
Block a user