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__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.gitea/
|
.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 import Intents
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from tortoise import Tortoise
|
||||||
|
|
||||||
load_dotenv(override=True)
|
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"
|
DATA_DIR = getenv("SPIFFO__DATA_FOLDER_PATH")
|
||||||
|
|
||||||
|
|
||||||
class DiscordBot(commands.Bot):
|
class DiscordBot(commands.Bot):
|
||||||
@ -41,23 +42,30 @@ class DiscordBot(commands.Bot):
|
|||||||
"passwd": getenv("SPIFFO__RCON_PASSWORD")
|
"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):
|
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
|
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")
|
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):
|
async def load_cogs(self):
|
||||||
"""
|
"""
|
||||||
Load any extensions found in the cogs dictionary.
|
Load any extensions found in the cogs dictionary.
|
||||||
|
@ -10,13 +10,12 @@ from pathlib import Path
|
|||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
from rcon.source import rcon
|
from rcon.source import rcon
|
||||||
from discord import Embed, Colour
|
|
||||||
from discord.ext import commands, tasks
|
from discord.ext import commands, tasks
|
||||||
|
|
||||||
|
from utils.reader import LogFileReader
|
||||||
|
|
||||||
ZOMBOID_FOLDER_PATH = Path(getenv("SPIFFO__ZOMBOID_FOLDER_PATH"))
|
ZOMBOID_FOLDER_PATH = Path(getenv("SPIFFO__ZOMBOID_FOLDER_PATH"))
|
||||||
CONSOLE_FILE_PATH = ZOMBOID_FOLDER_PATH / "server-console.txt"
|
CONSOLE_FILE_PATH = ZOMBOID_FOLDER_PATH / "server-console.txt"
|
||||||
assert ZOMBOID_FOLDER_PATH.exists()
|
|
||||||
assert CONSOLE_FILE_PATH.exists()
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -29,37 +28,47 @@ class ConsoleCog(commands.Cog):
|
|||||||
|
|
||||||
def __init__(self, bot: commands.Bot):
|
def __init__(self, bot: commands.Bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.monitor_console.start()
|
self.listen_for_changes.start()
|
||||||
|
|
||||||
@tasks.loop(seconds=1)
|
@tasks.loop(seconds=3)
|
||||||
async def monitor_console(self):
|
async def listen_for_changes(self):
|
||||||
"""
|
try:
|
||||||
Check the latest version of the console log file.
|
file_reader = LogFileReader(CONSOLE_FILE_PATH)
|
||||||
"""
|
except FileNotFoundError:
|
||||||
|
self.listen_for_changes.cancel()
|
||||||
|
|
||||||
if not CONSOLE_FILE_PATH.exists():
|
async for line in file_reader.read():
|
||||||
self.monitor_console.cancel()
|
await self.process_console_line(line)
|
||||||
raise FileNotFoundError("Server console file doesn't exist, task cancelled.")
|
|
||||||
|
|
||||||
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,
|
# if not CONSOLE_FILE_PATH.exists():
|
||||||
# instead lets grab the latest line, to prevent spam.
|
# self.monitor_console.cancel()
|
||||||
if self._last_line_number == 0:
|
# raise FileNotFoundError("Server console file doesn't exist, task cancelled.")
|
||||||
await file.seek(0, 2)
|
|
||||||
self._last_line_number = await file.tell()
|
|
||||||
return
|
|
||||||
|
|
||||||
await file.seek(self._last_line_number)
|
# async with aiofiles.open(CONSOLE_FILE_PATH, "r", encoding="utf-8") as file:
|
||||||
lines = await file.readlines()
|
|
||||||
if not lines:
|
|
||||||
log.debug("no new lines to read")
|
|
||||||
return
|
|
||||||
|
|
||||||
for line in lines:
|
# # If we are at 0, restarting the bot would cause rapid fire of all log lines,
|
||||||
await self.process_console_line(line.strip())
|
# # 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):
|
async def process_console_line(self, line: str):
|
||||||
"""
|
"""
|
||||||
|
@ -2,6 +2,8 @@ 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
|
||||||
|
aiosqlite==0.20.0
|
||||||
|
annotated-types==0.7.0
|
||||||
anyio==4.7.0
|
anyio==4.7.0
|
||||||
attrs==24.2.0
|
attrs==24.2.0
|
||||||
bump2version==1.0.1
|
bump2version==1.0.1
|
||||||
@ -12,10 +14,16 @@ h11==0.14.0
|
|||||||
httpcore==1.0.7
|
httpcore==1.0.7
|
||||||
httpx==0.28.0
|
httpx==0.28.0
|
||||||
idna==3.10
|
idna==3.10
|
||||||
|
iso8601==2.1.0
|
||||||
multidict==6.1.0
|
multidict==6.1.0
|
||||||
propcache==0.2.1
|
propcache==0.2.1
|
||||||
|
pydantic==2.10.3
|
||||||
|
pydantic_core==2.27.1
|
||||||
|
pypika-tortoise==0.3.2
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
|
pytz==2024.2
|
||||||
rcon==2.4.9
|
rcon==2.4.9
|
||||||
sniffio==1.3.1
|
sniffio==1.3.1
|
||||||
|
tortoise-orm==0.22.1
|
||||||
typing_extensions==4.12.2
|
typing_extensions==4.12.2
|
||||||
yarl==1.18.3
|
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