Compare commits
4 Commits
55304170b1
...
41b50e352f
Author | SHA1 | Date | |
---|---|---|---|
41b50e352f | |||
1ff4f4e831 | |||
b2847ebe95 | |||
825843ed86 |
@ -11,7 +11,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Debug environment variable flag.
|
||||
- Command to build player data from existing log files.
|
||||
- Database migration management with the aerich package.
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -13,4 +13,4 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . /app/
|
||||
|
||||
CMD ["sh", "-c", "aerich upgrade && python bot.py"]
|
||||
CMD ["python", "bot.py"]
|
2
bot.py
2
bot.py
@ -25,7 +25,7 @@ TORTOISE_ORM = {
|
||||
"connections": { "default": f"sqlite://{str(DATA_DIR)}/db.sqlite" },
|
||||
"apps": {
|
||||
"models": {
|
||||
"models": ["utils.models", "aerich.models"],
|
||||
"models": ["utils.models"],
|
||||
"default_connection": "default"
|
||||
}
|
||||
}
|
||||
|
@ -45,17 +45,19 @@ class PlayersCog(commands.Cog):
|
||||
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("**/*.txt"):
|
||||
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")
|
||||
|
||||
@tasks.loop(seconds=3)
|
||||
|
@ -3,21 +3,30 @@ from tortoise import BaseDBAsyncClient
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS "players" (
|
||||
CREATE TABLE IF NOT EXISTS "ingame_coordinates" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"x" INT NOT NULL,
|
||||
"y" INT NOT NULL,
|
||||
"z" INT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "players" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"username" VARCHAR(20) NOT NULL UNIQUE,
|
||||
"last_connection" TIMESTAMP,
|
||||
"last_disconnection" TIMESTAMP,
|
||||
"play_time_seconds" INT NOT NULL DEFAULT 0,
|
||||
"is_dead" INT NOT NULL DEFAULT 0
|
||||
) /* */;
|
||||
CREATE TABLE IF NOT EXISTS "player_deaths" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"coordinate_x" INT NOT NULL,
|
||||
"coordinate_y" INT NOT NULL,
|
||||
"coordinate_z" INT NOT NULL,
|
||||
"cause" VARCHAR(32) NOT NULL,
|
||||
"timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"coordinates_id" INT NOT NULL REFERENCES "ingame_coordinates" ("id") ON DELETE CASCADE,
|
||||
"player_id" INT NOT NULL REFERENCES "players" ("id") ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "player_session" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"connected_at" TIMESTAMP NOT NULL,
|
||||
"disconnected_at" TIMESTAMP NOT NULL,
|
||||
"connected_coords_id" INT NOT NULL REFERENCES "ingame_coordinates" ("id") ON DELETE CASCADE,
|
||||
"disconnected_coords_id" INT REFERENCES "ingame_coordinates" ("id") ON DELETE CASCADE,
|
||||
"player_id" INT NOT NULL REFERENCES "players" ("id") ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "steam_profile_summary" (
|
@ -1,4 +1,3 @@
|
||||
aerich==0.8.0
|
||||
aiofiles==24.1.0
|
||||
aiohappyeyeballs==2.4.4
|
||||
aiohttp==3.11.9
|
||||
|
@ -4,6 +4,7 @@ Database schemas.
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from pytz import timezone
|
||||
|
||||
import httpx
|
||||
from tortoise import fields
|
||||
@ -61,13 +62,15 @@ class PlayerSession(Model):
|
||||
on_delete=fields.CASCADE
|
||||
)
|
||||
connected_at = fields.DatetimeField()
|
||||
disconnected_at = fields.DatetimeField(null=False)
|
||||
disconnected_at = fields.DatetimeField(null=True)
|
||||
connected_coords = fields.ForeignKeyField(
|
||||
model_name="models.Coordinates",
|
||||
related_name="connected_coords",
|
||||
on_delete=fields.CASCADE
|
||||
)
|
||||
disconnected_coords = fields.ForeignKeyField(
|
||||
model_name="models.Coordinates",
|
||||
related_name="disconnected_coords",
|
||||
on_delete=fields.CASCADE,
|
||||
null=True
|
||||
)
|
||||
@ -93,10 +96,18 @@ class Player(Model):
|
||||
table = "players"
|
||||
|
||||
async def get_playtime(self) -> timedelta:
|
||||
playtime = await PlayerSession.filter(player=self).exclude(disconnected_at=None).annotate(
|
||||
total_playtime=Sum(F("disconnected_at") - F("connected_at"))
|
||||
).values()
|
||||
return timedelta(seconds=playtime[0]["total_playtime"].total_seconds() if playtime else timedelta())
|
||||
sessions = await PlayerSession.filter(player=self)
|
||||
total_playtime = timedelta()
|
||||
now = datetime.now()
|
||||
utc = timezone("UTC")
|
||||
|
||||
# I know this is terrible efficiency-wise, but the tortoise docs
|
||||
# are so bad and the annotations don't work like Django's models. Deal with it!
|
||||
for session in sessions:
|
||||
disconnected_at = session.disconnected_at or now
|
||||
total_playtime += disconnected_at.astimezone(utc) - session.connected_at.astimezone(utc)
|
||||
|
||||
return total_playtime
|
||||
|
||||
async def get_deaths(self) -> list[PlayerDeath]:
|
||||
return await PlayerDeath.filter(player=self)
|
||||
@ -139,7 +150,8 @@ class Player(Model):
|
||||
log.debug("creating session for player: %s", self.username)
|
||||
existing_session = await self.get_latest_session(ignore_closed_sessions=True)
|
||||
if existing_session:
|
||||
raise ValueError("Tried to open session while an open one exists.")
|
||||
log.debug("deleting an unfinished session to open a new one")
|
||||
await existing_session.delete()
|
||||
|
||||
coordinates = await Coordinates.create(x=coord_x, y=coord_y, z=coord_z)
|
||||
return await PlayerSession.create(
|
||||
@ -213,7 +225,8 @@ class Player(Model):
|
||||
raise ValueError("You must fetch the steam_profile_summary before creating an embed.")
|
||||
|
||||
death_count = len(await self.get_deaths())
|
||||
playtime = str(await self.get_playtime())
|
||||
playtime = str(await self.get_playtime()).split(".")[0] # remove the miliseconds
|
||||
log.debug("death count is: %s and playtime is: %s", death_count, playtime)
|
||||
|
||||
embed = Embed(
|
||||
title="Player",
|
||||
|
@ -28,6 +28,7 @@ class LogFileReader:
|
||||
raise FileNotFoundError(self.file_path)
|
||||
|
||||
async with aiofiles.open(self.file_path, "r", encoding="utf-8") as file:
|
||||
log.debug("file open, and jumping to line: %s", self._last_line_number)
|
||||
if self._last_line_number == 0 and not self.track_from_start:
|
||||
await file.seek(0, 2)
|
||||
self._last_line_number = await file.tell()
|
||||
|
Reference in New Issue
Block a user