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.
|
- Debug environment variable flag.
|
||||||
- Command to build player data from existing log files.
|
- Command to build player data from existing log files.
|
||||||
- Database migration management with the aerich package.
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -13,4 +13,4 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
|
|
||||||
COPY . /app/
|
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" },
|
"connections": { "default": f"sqlite://{str(DATA_DIR)}/db.sqlite" },
|
||||||
"apps": {
|
"apps": {
|
||||||
"models": {
|
"models": {
|
||||||
"models": ["utils.models", "aerich.models"],
|
"models": ["utils.models"],
|
||||||
"default_connection": "default"
|
"default_connection": "default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,17 +45,19 @@ class PlayersCog(commands.Cog):
|
|||||||
Build player data from existing and older log files.
|
Build player data from existing and older log files.
|
||||||
"""
|
"""
|
||||||
await inter.response.defer()
|
await inter.response.defer()
|
||||||
|
self.listen_for_changes.stop()
|
||||||
log.info("Building player data from logs.")
|
log.info("Building player data from logs.")
|
||||||
|
|
||||||
# Delete the existing data, as we will reconstruct it.
|
# Delete the existing data, as we will reconstruct it.
|
||||||
await Player.all().delete()
|
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))
|
log.debug("building from log file: %s", str(log_file))
|
||||||
file_handler = LogFileReader(log_file, track_from_start=True)
|
file_handler = LogFileReader(log_file, track_from_start=True)
|
||||||
for line in await file_handler.read():
|
for line in await file_handler.read():
|
||||||
await self.process_log_line(line, alert=False)
|
await self.process_log_line(line, alert=False)
|
||||||
|
|
||||||
|
self.listen_for_changes.start()
|
||||||
await inter.followup.send("Completed")
|
await inter.followup.send("Completed")
|
||||||
|
|
||||||
@tasks.loop(seconds=3)
|
@tasks.loop(seconds=3)
|
||||||
|
@ -3,21 +3,30 @@ from tortoise import BaseDBAsyncClient
|
|||||||
|
|
||||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
return """
|
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,
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
"username" VARCHAR(20) NOT NULL UNIQUE,
|
"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
|
"is_dead" INT NOT NULL DEFAULT 0
|
||||||
) /* */;
|
) /* */;
|
||||||
CREATE TABLE IF NOT EXISTS "player_deaths" (
|
CREATE TABLE IF NOT EXISTS "player_deaths" (
|
||||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
"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,
|
"cause" VARCHAR(32) NOT NULL,
|
||||||
"timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"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
|
"player_id" INT NOT NULL REFERENCES "players" ("id") ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS "steam_profile_summary" (
|
CREATE TABLE IF NOT EXISTS "steam_profile_summary" (
|
@ -1,4 +1,3 @@
|
|||||||
aerich==0.8.0
|
|
||||||
aiofiles==24.1.0
|
aiofiles==24.1.0
|
||||||
aiohappyeyeballs==2.4.4
|
aiohappyeyeballs==2.4.4
|
||||||
aiohttp==3.11.9
|
aiohttp==3.11.9
|
||||||
|
@ -4,6 +4,7 @@ Database schemas.
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from pytz import timezone
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from tortoise import fields
|
from tortoise import fields
|
||||||
@ -61,13 +62,15 @@ class PlayerSession(Model):
|
|||||||
on_delete=fields.CASCADE
|
on_delete=fields.CASCADE
|
||||||
)
|
)
|
||||||
connected_at = fields.DatetimeField()
|
connected_at = fields.DatetimeField()
|
||||||
disconnected_at = fields.DatetimeField(null=False)
|
disconnected_at = fields.DatetimeField(null=True)
|
||||||
connected_coords = fields.ForeignKeyField(
|
connected_coords = fields.ForeignKeyField(
|
||||||
model_name="models.Coordinates",
|
model_name="models.Coordinates",
|
||||||
|
related_name="connected_coords",
|
||||||
on_delete=fields.CASCADE
|
on_delete=fields.CASCADE
|
||||||
)
|
)
|
||||||
disconnected_coords = fields.ForeignKeyField(
|
disconnected_coords = fields.ForeignKeyField(
|
||||||
model_name="models.Coordinates",
|
model_name="models.Coordinates",
|
||||||
|
related_name="disconnected_coords",
|
||||||
on_delete=fields.CASCADE,
|
on_delete=fields.CASCADE,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
@ -93,10 +96,18 @@ class Player(Model):
|
|||||||
table = "players"
|
table = "players"
|
||||||
|
|
||||||
async def get_playtime(self) -> timedelta:
|
async def get_playtime(self) -> timedelta:
|
||||||
playtime = await PlayerSession.filter(player=self).exclude(disconnected_at=None).annotate(
|
sessions = await PlayerSession.filter(player=self)
|
||||||
total_playtime=Sum(F("disconnected_at") - F("connected_at"))
|
total_playtime = timedelta()
|
||||||
).values()
|
now = datetime.now()
|
||||||
return timedelta(seconds=playtime[0]["total_playtime"].total_seconds() if playtime else timedelta())
|
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]:
|
async def get_deaths(self) -> list[PlayerDeath]:
|
||||||
return await PlayerDeath.filter(player=self)
|
return await PlayerDeath.filter(player=self)
|
||||||
@ -139,7 +150,8 @@ class Player(Model):
|
|||||||
log.debug("creating session for player: %s", self.username)
|
log.debug("creating session for player: %s", self.username)
|
||||||
existing_session = await self.get_latest_session(ignore_closed_sessions=True)
|
existing_session = await self.get_latest_session(ignore_closed_sessions=True)
|
||||||
if existing_session:
|
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)
|
coordinates = await Coordinates.create(x=coord_x, y=coord_y, z=coord_z)
|
||||||
return await PlayerSession.create(
|
return await PlayerSession.create(
|
||||||
@ -213,7 +225,8 @@ class Player(Model):
|
|||||||
raise ValueError("You must fetch the steam_profile_summary before creating an embed.")
|
raise ValueError("You must fetch the steam_profile_summary before creating an embed.")
|
||||||
|
|
||||||
death_count = len(await self.get_deaths())
|
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(
|
embed = Embed(
|
||||||
title="Player",
|
title="Player",
|
||||||
|
@ -28,6 +28,7 @@ class LogFileReader:
|
|||||||
raise FileNotFoundError(self.file_path)
|
raise FileNotFoundError(self.file_path)
|
||||||
|
|
||||||
async with aiofiles.open(self.file_path, "r", encoding="utf-8") as file:
|
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:
|
if self._last_line_number == 0 and not self.track_from_start:
|
||||||
await file.seek(0, 2)
|
await file.seek(0, 2)
|
||||||
self._last_line_number = await file.tell()
|
self._last_line_number = await file.tell()
|
||||||
|
Reference in New Issue
Block a user