database, data folder path & file reader class
All checks were successful
Build and Push Docker Image / build (push) Successful in 28s

This commit is contained in:
Corban-Lee Jones 2024-12-07 23:23:10 +00:00
parent 9522ec6ee0
commit 2b6f68de0d
7 changed files with 155 additions and 39 deletions

View File

@ -5,4 +5,7 @@ logs/
__pycache__/
*.pyc
.gitea/
.vscode/
.vscode/
db.sqlite
db.sqlite-shm
db.sqlite-wal

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.env
venv/
logs/
__pycache__/
*.pyc
db.sqlite
db.sqlite-shm
db.sqlite-wal

30
bot.py
View File

@ -14,12 +14,13 @@ from pathlib import Path
from discord import Intents
from discord.ext import commands
from dotenv import load_dotenv
from tortoise import Tortoise
load_dotenv(override=True)
log = logging.getLogger(__name__)
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
DATA_DIR = getenv("SPIFFO__DATA_FOLDER_PATH")
class DiscordBot(commands.Bot):
@ -41,23 +42,30 @@ class DiscordBot(commands.Bot):
"passwd": getenv("SPIFFO__RCON_PASSWORD")
}
async def sync_app_commands(self):
"""
Sync application commands between Discord and the bot.
"""
await self.wait_until_ready()
await self.tree.sync()
log.info("Application commands successfully synced")
async def on_ready(self):
"""
Execute init operations that require the bot to be ready.
Execute initial operations that require the bot to be ready.
Ideally should not be manually called, this is handled by discord.py
"""
await self.sync_app_commands()
# Sync app commands
await self.wait_until_ready()
await self.tree.sync()
# Open database connection
await Tortoise.init(
db_url= f"sqlite://{str(DATA_DIR)}/db.sqlite",
modules={"models": ["utils.models"]}
)
await Tortoise.generate_schemas()
log.info("Discord Bot is ready")
async def close(self):
await Tortoise.close_connections()
await super().close();
log.info("Shutdown successfully and safely")
async def load_cogs(self):
"""
Load any extensions found in the cogs dictionary.

View File

@ -10,13 +10,12 @@ from pathlib import Path
import aiofiles
from rcon.source import rcon
from discord import Embed, Colour
from discord.ext import commands, tasks
from utils.reader import LogFileReader
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__)
@ -29,37 +28,47 @@ class ConsoleCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.monitor_console.start()
self.listen_for_changes.start()
@tasks.loop(seconds=1)
async def monitor_console(self):
"""
Check the latest version of the console log file.
"""
@tasks.loop(seconds=3)
async def listen_for_changes(self):
try:
file_reader = LogFileReader(CONSOLE_FILE_PATH)
except FileNotFoundError:
self.listen_for_changes.cancel()
if not CONSOLE_FILE_PATH.exists():
self.monitor_console.cancel()
raise FileNotFoundError("Server console file doesn't exist, task cancelled.")
async for line in file_reader.read():
await self.process_console_line(line)
async with aiofiles.open(CONSOLE_FILE_PATH, "r", encoding="utf-8") as file:
# @tasks.loop(seconds=1)
# async def monitor_console(self):
# """
# Check the latest version of the console log 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
# if not CONSOLE_FILE_PATH.exists():
# self.monitor_console.cancel()
# raise FileNotFoundError("Server console file doesn't exist, task cancelled.")
await file.seek(self._last_line_number)
lines = await file.readlines()
if not lines:
log.debug("no new lines to read")
return
# async with aiofiles.open(CONSOLE_FILE_PATH, "r", encoding="utf-8") as file:
for line in lines:
await self.process_console_line(line.strip())
# # 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
self._last_line_number = await file.tell()
# 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):
"""

View File

@ -2,6 +2,8 @@ aiofiles==24.1.0
aiohappyeyeballs==2.4.4
aiohttp==3.11.9
aiosignal==1.3.1
aiosqlite==0.20.0
annotated-types==0.7.0
anyio==4.7.0
attrs==24.2.0
bump2version==1.0.1
@ -12,10 +14,16 @@ h11==0.14.0
httpcore==1.0.7
httpx==0.28.0
idna==3.10
iso8601==2.1.0
multidict==6.1.0
propcache==0.2.1
pydantic==2.10.3
pydantic_core==2.27.1
pypika-tortoise==0.3.2
python-dotenv==1.0.1
pytz==2024.2
rcon==2.4.9
sniffio==1.3.1
tortoise-orm==0.22.1
typing_extensions==4.12.2
yarl==1.18.3

36
utils/models.py Normal file
View File

@ -0,0 +1,36 @@
"""
Database schemas.
"""
from tortoise import Tortoise, fields
from tortoise.models import Model
class Player(Model):
username = fields.CharField(max_length=20)
steam_id = fields.CharField(max_length=20)
last_connection = fields.DatetimeField()
last_disconnection = fields.DatetimeField()
deaths = fields.ManyToManyField(
model_name="models.PlayerDeath",
on_delete=fields.CASCADE
)
@property
def is_online(self):
if not self.last_connection:
return False
return (self.last_connection and not self.last_disconnection) \
or self.last_connection > self.last_disconnection
class PlayerDeath(Model):
coordinate_x = fields.IntField()
coordinate_y = fields.IntField()
coordinate_z = fields.IntField()
cause = fields.CharField(max_length=32)
timestamp = fields.DatetimeField(auto_now_add=True)
class Meta:
table = "player_deaths"

44
utils/reader.py Normal file
View File

@ -0,0 +1,44 @@
"""
"""
import logging
from pathlib import Path
import aiofiles
log = logging.getLogger(__name__)
class LogFileReader:
"""
"""
def __init__(self, file_path: Path):
if type(file_path) != Path:
raise TypeError(f"file_path must be type Path, not {type(file_path)}")
self.file_path = file_path
self._last_line_number = 0
async def read(self):
if not self.file_path.exists():
log.error("Cannot read non-existant file path: '%s'", self.file_path)
raise FileNotFoundError(self.file_path)
async with aiofiles.open(self.file_path, "r", encoding="utf-8") as file:
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:
yield line.strip()
self._last_line_number = await file.tell()