This repository has been archived on 2025-02-16. You can view files and clone it, but cannot push or open issues or pull requests.
Spiffo/cogs/players.py
Corban-Lee Jones 1be47acdd8
All checks were successful
Build and Push Docker Image / build (push) Successful in 13s
rotate expired user log files
2024-12-13 12:17:03 +00:00

207 lines
7.0 KiB
Python

"""
Handles tasks related to in-game players, such as connect/disconnect alerts.
"""
import re
import logging
from os import getenv
from pathlib import Path
from datetime import datetime
from discord import Colour, Interaction, Permissions, app_commands
from discord.ext import commands, tasks
from utils.reader import LogFileReader
from utils.models import Player
ZOMBOID_FOLDER_PATH = Path(getenv("SPIFFO__ZOMBOID_FOLDER_PATH"))
LOGS_FOLDER_PATH = ZOMBOID_FOLDER_PATH / "Logs"
TIMESTAMP_FORMAT = getenv("SPIFFO__ZOMBOID_LOG_TIMESTAMP_FORMAT", "%d-%m-%y %H:%M:%S.%f")
log = logging.getLogger(__name__)
class PlayersCog(commands.Cog):
"""
Handles tasks related to in-game players.
"""
file_handler: LogFileReader
def __init__(self, bot: commands.Bot):
self.bot = bot
self.create_file_handler()
self.listen_for_changes.start()
cmd_group = app_commands.Group(
name="players",
description="Commands for the players cog.",
default_permissions=Permissions.all(),
guild_only=True
)
@cmd_group.command(name="build-from-logs")
async def build_from_logs(self, inter: Interaction):
"""
Build player data from existing and older log files.
"""
await inter.response.defer()
self.listen_for_changes.stop()
log.info("Building player data from logs.")
# Delete the existing data, as we will reconstruct it.
await Player.all().delete()
for log_file in LOGS_FOLDER_PATH.glob("**/*_user.txt"):
log.debug("building from log file: %s", str(log_file))
file_handler = LogFileReader(log_file, track_from_start=True)
for line in await file_handler.read():
await self.process_log_line(line, alert=False)
self.listen_for_changes.start()
await inter.followup.send("Completed")
def create_file_handler(self):
user_log_file_path = next(LOGS_FOLDER_PATH.glob("*_user.txt"), None)
self.file_handler = LogFileReader(user_log_file_path)
@tasks.loop(seconds=3)
async def listen_for_changes(self):
"""
Listen for changes in the user.txt log file, and process them.
"""
log.debug("listening for changes")
try:
for line in await self.file_handler.read():
await self.process_log_line(line)
except FileNotFoundError:
log.info("User log file not found, assuming it rotated to a new file.")
self.create_file_handler()
async def process_log_line(self, line: str, alert: bool = True):
log.debug("processing log line")
if "died" in line:
await self.process_player_death(line, alert)
elif "fully connected" in line:
await self.process_connected_player(line, alert)
elif "disconnected player" in line:
await self.process_disconnected_player(line, alert)
async def process_player_death(self, line: str, alert: bool = False):
log.debug("processing player death")
re_pattern = r"\[(?P<timestamp>[\d\- :\.]+)\] user (?P<username>.+?) died at \((?P<x>\d+),(?P<y>\d+),(?P<z>\d+)\) \((?P<cause>.+?)\)"
re_match = re.search(re_pattern, line)
if not re_match:
log.warning("failed to parse player death log: %s", line)
return
username = re_match.group("username")
player = await Player.get_or_none(username=username)
if not player:
log.warning("Player returned none, cannot add death: %s", username)
return
await player.add_death(
coord_x=re_match.group("x"),
coord_y=re_match.group("y"),
coord_z=re_match.group("z"),
cause=re_match.group("cause"),
timestamp=datetime.strptime(
re_match.group("timestamp"),
TIMESTAMP_FORMAT
)
)
await player.save()
log.debug("successfully registered player death to %s", re_match.group("username"))
if not alert:
return
channel = await self.bot.get_ingame_channel()
embed = await player.get_embed()
embed.title = "Player Has Died"
embed.colour = Colour.dark_orange()
await channel.send(embed=embed)
async def process_connected_player(self, line: str, alert: bool = False):
"""
"""
log.debug("processing connected player")
re_pattern = r'\[(?P<timestamp>.*?)\] (?P<steam_id>\d+) "(?P<username>.*?)" fully connected \((?P<x>\d+),(?P<y>\d+),(?P<z>\d+)\)'
re_match = re.search(re_pattern, line)
if not re_match:
log.warning("Failed to parse player data: %s", line)
return
player, created = await Player.get_or_create(username=re_match.group("username"))
await player.open_session(
timestamp=datetime.strptime(re_match.group("timestamp"), TIMESTAMP_FORMAT),
coord_x=re_match.group("x"),
coord_y=re_match.group("y"),
coord_z=re_match.group("z")
)
await player.update_outdated_steam_summary(
re_match.group("steam_id"),
self.bot.steam_api_key
)
await player.save()
# This connection method is called when the player respawns
if player.is_dead:
player.is_dead = False # player must be alive if fully connected
await player.save()
return
if not alert:
return
channel = await self.bot.get_ingame_channel()
embed = await player.get_embed()
embed.title = "Player Has Connected"
embed.colour = Colour.brand_green()
await channel.send(embed=embed)
async def process_disconnected_player(self, line: str, alert: bool = False):
"""
"""
log.debug("processing disconnected player")
re_pattern = r'\[(?P<timestamp>.*?)\] (?P<steam_id>\d+) "(?P<username>.*?)" disconnected player \((?P<x>\d+),(?P<y>\d+),(?P<z>\d+)\)'
re_match = re.search(re_pattern, line)
if not re_match:
log.warning("Failed to parse player data: %s", line)
return
player, created = await Player.get_or_create(username=re_match.group("username"))
await player.close_session(
timestamp=datetime.strptime(re_match.group("timestamp"), TIMESTAMP_FORMAT),
coord_x=re_match.group("x"),
coord_y=re_match.group("y"),
coord_z=re_match.group("z")
)
await player.update_outdated_steam_summary(
re_match.group("steam_id"),
self.bot.steam_api_key
)
await player.save()
if player.is_dead or not alert:
return
channel = await self.bot.get_ingame_channel()
embed = await player.get_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__)