database, data folder path & file reader class
All checks were successful
Build and Push Docker Image / build (push) Successful in 28s
All checks were successful
Build and Push Docker Image / build (push) Successful in 28s
This commit is contained in:
parent
9522ec6ee0
commit
2b6f68de0d
@ -5,4 +5,7 @@ logs/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.gitea/
|
||||
.vscode/
|
||||
.vscode/
|
||||
db.sqlite
|
||||
db.sqlite-shm
|
||||
db.sqlite-wal
|
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
.env
|
||||
venv/
|
||||
logs/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
db.sqlite
|
||||
db.sqlite-shm
|
||||
db.sqlite-wal
|
30
bot.py
30
bot.py
@ -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.
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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
36
utils/models.py
Normal 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
44
utils/reader.py
Normal 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()
|
Reference in New Issue
Block a user