244 lines
7.5 KiB
Python
244 lines
7.5 KiB
Python
from asyncio import Event
|
|
from datetime import datetime, timedelta
|
|
from data.queries import JOINTYPE
|
|
from utils.lib import utc_now, strfdelta
|
|
from modules.profiles.profiles.profiles import ProfilesRegistry
|
|
|
|
from . import logger
|
|
from .data import (
|
|
SubathonData,
|
|
Subathon,
|
|
RunningSubathon,
|
|
SubathonContribution,
|
|
SubathonGoal,
|
|
)
|
|
|
|
|
|
class ActiveSubathon:
|
|
def __init__(self, subathondata: Subathon, runningdata: RunningSubathon | None):
|
|
self.subathondata = subathondata
|
|
self.runningdata = runningdata
|
|
|
|
@property
|
|
def running(self):
|
|
return self.runningdata is not None
|
|
|
|
@property
|
|
def name(self):
|
|
return self.subathondata.name or "Subathon"
|
|
|
|
async def check_cap(self):
|
|
if not (cap := self.subathondata.timecap):
|
|
return False
|
|
else:
|
|
score = await self.get_score()
|
|
time_earned = self.get_score_time(score)
|
|
total_time = self.subathondata.initial_time + time_earned
|
|
return total_time >= cap
|
|
|
|
async def check_finished(self):
|
|
"""
|
|
Return True if the subathon duration has exceeded its earned time.
|
|
"""
|
|
return await self.get_remaining() <= 0
|
|
|
|
async def pause(self):
|
|
"""
|
|
Pause the active subathon.
|
|
If the subathon is in 'overtime', i.e. it has already finished,
|
|
then the total time is saved as the earned time not including the overtime.
|
|
"""
|
|
if not self.running:
|
|
raise ValueError("This subathon is not running!")
|
|
assert self.runningdata is not None
|
|
|
|
new_duration = await self.get_duration()
|
|
|
|
await self.subathondata.update(duration=new_duration)
|
|
await self.runningdata.delete()
|
|
self.runningdata = None
|
|
|
|
async def resume(self):
|
|
if self.running:
|
|
raise ValueError("This subathon is already running!")
|
|
self.runningdata = await RunningSubathon.create(
|
|
subathon_id=self.subathondata.subathon_id
|
|
)
|
|
|
|
async def get_score(self) -> float:
|
|
rows = await SubathonContribution.fetch_where(
|
|
subathon_id=self.subathondata.subathon_id
|
|
)
|
|
return sum(row.score for row in rows)
|
|
|
|
def get_score_time(self, score: float) -> int:
|
|
# Get time contributed by this score
|
|
return int(score * self.subathondata.score_time)
|
|
|
|
async def get_duration(self) -> int:
|
|
"""
|
|
Number of seconds that this subathon has run for.
|
|
Includes current running session if it exists.
|
|
|
|
If the subathon is already finished (in overtime)
|
|
returns the earned time instead of the true duration.
|
|
"""
|
|
duration = self.subathondata.duration
|
|
# Add running duration if required
|
|
if self.runningdata:
|
|
now = utc_now()
|
|
added = int((now - self.runningdata.last_started).total_seconds())
|
|
duration += added
|
|
earned = await self.get_earned()
|
|
|
|
return min(duration, earned)
|
|
|
|
async def get_earned(self) -> int:
|
|
"""
|
|
Number of seconds earned in the subathon so far.
|
|
Includes initial time.
|
|
Takes into account the cap.
|
|
"""
|
|
score = await self.get_score()
|
|
time_earned = self.get_score_time(score)
|
|
total_time = self.subathondata.initial_time + time_earned
|
|
if cap := self.subathondata.timecap:
|
|
total_time = min(total_time, cap)
|
|
return total_time
|
|
|
|
async def get_remaining(self) -> int:
|
|
"""
|
|
Number of seconds remaining on the subathon timer.
|
|
Will be 0 or slightly negative if finished.
|
|
"""
|
|
total_time = await self.get_earned()
|
|
return total_time - await self.get_duration() - 1
|
|
|
|
async def get_ending(self):
|
|
"""
|
|
Ending time of the subathon.
|
|
May be in the past if the subathon has already ended.
|
|
"""
|
|
now = utc_now()
|
|
remaining = await self.get_remaining()
|
|
return now + timedelta(seconds=remaining)
|
|
|
|
async def add_contribution(
|
|
self, profileid: int | None, score: float, event_id: int | None
|
|
) -> SubathonContribution:
|
|
return await SubathonContribution.create(
|
|
subathon_id=self.subathondata.subathon_id,
|
|
profileid=profileid,
|
|
score=score,
|
|
event_id=event_id,
|
|
)
|
|
|
|
def fetch_contributions(self, **kwargs):
|
|
query = SubathonContribution.fetch_where(
|
|
subathon_id=self.subathondata.subathon_id, **kwargs
|
|
).order_by("created_at")
|
|
return query
|
|
|
|
async def get_goals(self) -> list[SubathonGoal]:
|
|
goals = await SubathonGoal.fetch_where(
|
|
subathon_id=self.subathondata.subathon_id
|
|
).order_by("required_score")
|
|
return goals
|
|
|
|
async def get_total_subs(self) -> int:
|
|
"""
|
|
Get the total number of sub points
|
|
contributed to this subathon.
|
|
"""
|
|
pointcol = (
|
|
"CASE "
|
|
"WHEN tier = 1000 THEN 1 "
|
|
"WHEN tier = 2000 THEN 2 "
|
|
"WHEN tier = 3000 THEN 6 "
|
|
"END"
|
|
)
|
|
total_points = 0
|
|
|
|
result = await (
|
|
SubathonContribution.table.select_where(
|
|
subathon_id=self.subathondata.subathon_id
|
|
)
|
|
.join(
|
|
"subscribe_events",
|
|
using=("event_id",),
|
|
join_type=JOINTYPE.INNER,
|
|
)
|
|
.select(total=f"SUM({pointcol})")
|
|
.with_no_adapter()
|
|
)
|
|
|
|
total_points += result[0]["total"] or 0
|
|
|
|
result = await (
|
|
SubathonContribution.table.select_where(
|
|
subathon_id=self.subathondata.subathon_id
|
|
)
|
|
.join(
|
|
"subscribe_message_events",
|
|
using=("event_id",),
|
|
join_type=JOINTYPE.INNER,
|
|
)
|
|
.select(total=f"SUM({pointcol})")
|
|
.with_no_adapter()
|
|
)
|
|
|
|
total_points += result[0]["total"] or 0
|
|
|
|
result = await (
|
|
SubathonContribution.table.select_where(
|
|
subathon_id=self.subathondata.subathon_id
|
|
)
|
|
.join(
|
|
"gift_events",
|
|
using=("event_id",),
|
|
join_type=JOINTYPE.INNER,
|
|
)
|
|
.select(total=f"SUM(gifted_count * ({pointcol}))")
|
|
.with_no_adapter()
|
|
)
|
|
|
|
total_points += result[0]["total"] or 0
|
|
|
|
return total_points
|
|
|
|
async def get_total_bits(self) -> int:
|
|
"""
|
|
Get the total number of bits
|
|
contributed to this subathon.
|
|
"""
|
|
result = await (
|
|
SubathonContribution.table.select_where(
|
|
subathon_id=self.subathondata.subathon_id
|
|
)
|
|
.join(
|
|
"bits_events",
|
|
using=("event_id",),
|
|
join_type=JOINTYPE.INNER,
|
|
)
|
|
.select(total="SUM(bits)")
|
|
.with_no_adapter()
|
|
)
|
|
return result[0]["total"] or 0
|
|
|
|
|
|
class SubathonRegistry:
|
|
def __init__(self, data: SubathonData, profiler: ProfilesRegistry):
|
|
self.data = data
|
|
self.profiler = profiler
|
|
|
|
async def init(self):
|
|
await self.data.init()
|
|
|
|
async def get_active_subathon(self, communityid: int) -> ActiveSubathon | None:
|
|
rows = await Subathon.fetch_where(communityid=communityid, ended_at=None)
|
|
if rows:
|
|
subathondata = rows[0]
|
|
running = await RunningSubathon.fetch(subathondata.subathon_id)
|
|
subba = ActiveSubathon(subathondata, running)
|
|
return subba
|