Compare commits
1 Commits
32b84625df
...
aa87fd6f9f
| Author | SHA1 | Date | |
|---|---|---|---|
| aa87fd6f9f |
@@ -4,12 +4,19 @@ from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from dataclasses import dataclass
|
||||
|
||||
from data.queries import JOINTYPE, ORDER
|
||||
from meta.sockets import Channel
|
||||
from utils.lib import utc_now
|
||||
from modules.profiles.profiles.profiles import ProfilesRegistry
|
||||
|
||||
from . import logger
|
||||
from .data import SubathonData, Subathon, RunningSubathon, SubathonContribution, SubathonGoal
|
||||
from .data import (
|
||||
SubathonData,
|
||||
Subathon,
|
||||
RunningSubathon,
|
||||
SubathonContribution,
|
||||
SubathonGoal,
|
||||
)
|
||||
from .subathon import ActiveSubathon, SubathonRegistry
|
||||
|
||||
|
||||
@@ -31,6 +38,12 @@ class ContributionPayload(TypedDict):
|
||||
timestamp: ISOTimestamp
|
||||
|
||||
|
||||
class LeaderboardItemPayload(TypedDict):
|
||||
user_name: str
|
||||
user_id: str | None
|
||||
amount: float
|
||||
|
||||
|
||||
class ScoreTablePayload(TypedDict):
|
||||
bit_score: float # points per bit
|
||||
t1_score: float # points per T1 sub
|
||||
@@ -42,38 +55,52 @@ class ScoreTablePayload(TypedDict):
|
||||
|
||||
class SubathonPayload(TypedDict):
|
||||
end_at: ISOTimestamp
|
||||
is_running: bool
|
||||
is_running: bool
|
||||
score_table: ScoreTablePayload
|
||||
name: str
|
||||
|
||||
total_contribution: float
|
||||
total_subpoints: int
|
||||
total_bits: int
|
||||
|
||||
goals_met: int
|
||||
goals_total: int
|
||||
|
||||
last_goal: GoalPayload | None
|
||||
next_goal: GoalPayload | None
|
||||
goals: list[GoalPayload]
|
||||
|
||||
last_contribution: ContributionPayload | None
|
||||
leaderboard: list[LeaderboardItemPayload]
|
||||
|
||||
|
||||
async def prepare_subathon(profiler: ProfilesRegistry, subathon: ActiveSubathon) -> SubathonPayload:
|
||||
now = utc_now()
|
||||
|
||||
async def prepare_subathon(
|
||||
profiler: ProfilesRegistry, subathon: ActiveSubathon
|
||||
) -> SubathonPayload:
|
||||
total_score = await subathon.get_score()
|
||||
|
||||
# Build the goal data
|
||||
goals = await subathon.get_goals()
|
||||
goals_met = 0
|
||||
last_goal = None
|
||||
next_goal = None
|
||||
last_goalp = None
|
||||
next_goalp = None
|
||||
goalps = []
|
||||
total_goals = len(goals)
|
||||
for goal in goals:
|
||||
goalp = GoalPayload(
|
||||
required=float(goal.required_score),
|
||||
name=goal.description,
|
||||
)
|
||||
goalps.append(goalp)
|
||||
if goal.required_score >= total_score:
|
||||
last_goal = goal
|
||||
last_goalp = goalp
|
||||
goals_met += 1
|
||||
else:
|
||||
next_goal = goal
|
||||
next_goalp = goalp
|
||||
break
|
||||
|
||||
# Build the last contribution information
|
||||
contribs = await subathon.fetch_contributions().limit(1)
|
||||
last_contrib: ContributionPayload | None = None
|
||||
last_contrib: ContributionPayload | None = None
|
||||
if contribs:
|
||||
contrib = contribs[0]
|
||||
if contrib.profileid:
|
||||
@@ -82,16 +109,17 @@ async def prepare_subathon(profiler: ProfilesRegistry, subathon: ActiveSubathon)
|
||||
name = profile.nickname
|
||||
user_id = None
|
||||
else:
|
||||
name = 'Anonymous'
|
||||
name = "Anonymous"
|
||||
user_id = None
|
||||
last_contrib = ContributionPayload(
|
||||
user_name=name,
|
||||
user_id=user_id,
|
||||
amount=float(contrib.score),
|
||||
seconds_added=subathon.get_score_time(contrib.score),
|
||||
timestamp=contrib.created_at.isoformat()
|
||||
timestamp=contrib.created_at.isoformat(),
|
||||
)
|
||||
|
||||
# Build the score table
|
||||
score_table = ScoreTablePayload(
|
||||
bit_score=float(subathon.subathondata.bit_score),
|
||||
t1_score=float(subathon.subathondata.sub1_score),
|
||||
@@ -99,41 +127,75 @@ async def prepare_subathon(profiler: ProfilesRegistry, subathon: ActiveSubathon)
|
||||
t3_score=float(subathon.subathondata.sub3_score),
|
||||
score_time=int(subathon.subathondata.score_time),
|
||||
)
|
||||
|
||||
# Build the contribution leaderboard
|
||||
query = SubathonContribution.table.select_where(
|
||||
subathon_id=subathon.subathondata.subathon_id
|
||||
)
|
||||
query.join("user_profiles", using=("profileid",), join_type=JOINTYPE.LEFT)
|
||||
query.select("subathon_id", "profileid", total="SUM(score)")
|
||||
query.order_by("total", direction=ORDER.DESC)
|
||||
query.group_by("subathon_id", "profileid")
|
||||
query.with_no_adapter()
|
||||
results = await query
|
||||
|
||||
leaderboard = []
|
||||
for row in results:
|
||||
if pid := row["profileid"]:
|
||||
profile = await profiler.get_profile(pid)
|
||||
name = (profile.nickname if profile else None) or "Unknown"
|
||||
else:
|
||||
name = "Anonymous"
|
||||
score = row["total"]
|
||||
item = LeaderboardItemPayload(
|
||||
user_name=name,
|
||||
user_id=pid,
|
||||
amount=float(score),
|
||||
)
|
||||
leaderboard.append(item)
|
||||
|
||||
# Build the raw totals
|
||||
subpoints = await subathon.get_total_subs()
|
||||
bits = await subathon.get_total_bits()
|
||||
|
||||
# Finally, put together the payload
|
||||
payload = SubathonPayload(
|
||||
name=subathon.subathondata.name,
|
||||
end_at=(await subathon.get_ending()).isoformat(),
|
||||
is_running=(subathon.running),
|
||||
score_table=score_table,
|
||||
total_contribution=float(await subathon.get_score()),
|
||||
total_subpoints=subpoints,
|
||||
total_bits=bits,
|
||||
goals_met=goals_met,
|
||||
goals_total=total_goals,
|
||||
last_goal=GoalPayload(
|
||||
required=float(last_goal.required_score),
|
||||
name=last_goal.description,
|
||||
) if last_goal is not None else None,
|
||||
next_goal=GoalPayload(
|
||||
required=float(next_goal.required_score),
|
||||
name=next_goal.description,
|
||||
) if next_goal is not None else None,
|
||||
last_contribution=last_contrib
|
||||
last_goal=last_goalp,
|
||||
next_goal=next_goalp,
|
||||
goals=goalps,
|
||||
last_contribution=last_contrib,
|
||||
leaderboard=leaderboard,
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
class TimerChannel(Channel):
|
||||
name = 'SubTimer'
|
||||
name = "SubTimer"
|
||||
|
||||
def __init__(self, profiler: ProfilesRegistry, subathons: SubathonRegistry, **kwargs):
|
||||
def __init__(
|
||||
self, profiler: ProfilesRegistry, subathons: SubathonRegistry, **kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.profiler: ProfilesRegistry = profiler
|
||||
self.subathons: SubathonRegistry = subathons
|
||||
|
||||
# Map of communities to webhooks listening for this community
|
||||
self.communities = defaultdict(set) # Map of communityid -> listening websockets
|
||||
# Map of communities to webhooks listening for this community
|
||||
self.communities = defaultdict(
|
||||
set
|
||||
) # Map of communityid -> listening websockets
|
||||
|
||||
async def on_connection(self, websocket, event):
|
||||
if not (cidstr := event.get('community')):
|
||||
if not (cidstr := event.get("community")):
|
||||
raise ValueError("Subathon timer connection missing communityid")
|
||||
elif not cidstr.isdigit():
|
||||
raise ValueError("Community id provided is not an integer")
|
||||
@@ -157,35 +219,39 @@ class TimerChannel(Channel):
|
||||
wss.discard(websocket)
|
||||
await super().del_connection(websocket)
|
||||
|
||||
async def send_subathon_update(self, communityid: int, payload: SubathonPayload, websocket=None):
|
||||
async def send_subathon_update(
|
||||
self, communityid: int, payload: SubathonPayload, websocket=None
|
||||
):
|
||||
for ws in (websocket,) if websocket else self.communities[communityid]:
|
||||
await self.send_event(
|
||||
{
|
||||
'type': "DO",
|
||||
'method': "setTimer",
|
||||
'args': payload,
|
||||
"type": "DO",
|
||||
"method": "setTimer",
|
||||
"args": payload,
|
||||
},
|
||||
websocket=ws
|
||||
websocket=ws,
|
||||
)
|
||||
|
||||
async def send_subathon_ended(self, communityid: int, payload: SubathonPayload, websocket=None):
|
||||
async def send_subathon_ended(
|
||||
self, communityid: int, payload: SubathonPayload, websocket=None
|
||||
):
|
||||
for ws in (websocket,) if websocket else self.communities[communityid]:
|
||||
await self.send_event(
|
||||
{
|
||||
'type': "DO",
|
||||
'method': "endTimer",
|
||||
'args': payload,
|
||||
"type": "DO",
|
||||
"method": "endTimer",
|
||||
"args": payload,
|
||||
},
|
||||
websocket=ws
|
||||
websocket=ws,
|
||||
)
|
||||
|
||||
async def send_no_subathon(self, communityid: int, websocket=None):
|
||||
for ws in (websocket,) if websocket else self.communities[communityid]:
|
||||
await self.send_event(
|
||||
{
|
||||
'type': "DO",
|
||||
'method': "noTimer",
|
||||
'args': {},
|
||||
"type": "DO",
|
||||
"method": "noTimer",
|
||||
"args": {},
|
||||
},
|
||||
websocket=ws
|
||||
websocket=ws,
|
||||
)
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
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
|
||||
from .data import (
|
||||
SubathonData,
|
||||
Subathon,
|
||||
RunningSubathon,
|
||||
SubathonContribution,
|
||||
SubathonGoal,
|
||||
)
|
||||
|
||||
|
||||
class ActiveSubathon:
|
||||
def __init__(self, subathondata: Subathon, runningdata: RunningSubathon | None):
|
||||
@@ -17,7 +25,7 @@ class ActiveSubathon:
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.subathondata.name or 'Subathon'
|
||||
return self.subathondata.name or "Subathon"
|
||||
|
||||
async def check_cap(self):
|
||||
if not (cap := self.subathondata.timecap):
|
||||
@@ -32,7 +40,7 @@ class ActiveSubathon:
|
||||
"""
|
||||
Return True if the subathon duration has exceeded its earned time.
|
||||
"""
|
||||
return (await self.get_remaining() <= 0)
|
||||
return await self.get_remaining() <= 0
|
||||
|
||||
async def pause(self):
|
||||
"""
|
||||
@@ -53,10 +61,14 @@ class ActiveSubathon:
|
||||
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)
|
||||
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)
|
||||
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:
|
||||
@@ -75,7 +87,7 @@ class ActiveSubathon:
|
||||
# Add running duration if required
|
||||
if self.runningdata:
|
||||
now = utc_now()
|
||||
added = int( (now - self.runningdata.last_started).total_seconds() )
|
||||
added = int((now - self.runningdata.last_started).total_seconds())
|
||||
duration += added
|
||||
earned = await self.get_earned()
|
||||
|
||||
@@ -111,23 +123,108 @@ class ActiveSubathon:
|
||||
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:
|
||||
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
|
||||
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')
|
||||
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')
|
||||
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(
|
||||
subathoid=self.subathondata.subathon_id
|
||||
)
|
||||
.join(
|
||||
"subscribe_events",
|
||||
using=("eventid",),
|
||||
join_type=JOINTYPE.INNER,
|
||||
)
|
||||
.select(total=f"SUM({pointcol})")
|
||||
.with_no_adapter()
|
||||
)
|
||||
|
||||
total_points += result["total"]
|
||||
|
||||
result = await (
|
||||
SubathonContribution.table.select_where(
|
||||
subathoid=self.subathondata.subathon_id
|
||||
)
|
||||
.join(
|
||||
"subscribe_message_events",
|
||||
using=("eventid",),
|
||||
join_type=JOINTYPE.INNER,
|
||||
)
|
||||
.select(total=f"SUM({pointcol})")
|
||||
.with_no_adapter()
|
||||
)
|
||||
|
||||
total_points += result["total"]
|
||||
|
||||
result = await (
|
||||
SubathonContribution.table.select_where(
|
||||
subathoid=self.subathondata.subathon_id
|
||||
)
|
||||
.join(
|
||||
"subscribe_gift_events",
|
||||
using=("eventid",),
|
||||
join_type=JOINTYPE.INNER,
|
||||
)
|
||||
.select(total=f"SUM(gifted_count * ({pointcol}))")
|
||||
.with_no_adapter()
|
||||
)
|
||||
|
||||
total_points += result["total"]
|
||||
|
||||
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(
|
||||
subathoid=self.subathondata.subathon_id
|
||||
)
|
||||
.join(
|
||||
"bits_events",
|
||||
using=("eventid",),
|
||||
join_type=JOINTYPE.INNER,
|
||||
)
|
||||
.select(total="SUM(bits)")
|
||||
.with_no_adapter()
|
||||
)
|
||||
return result["total"]
|
||||
|
||||
|
||||
class SubathonRegistry:
|
||||
def __init__(self, data: SubathonData, profiler: ProfilesRegistry):
|
||||
@@ -144,5 +241,3 @@ class SubathonRegistry:
|
||||
running = await RunningSubathon.fetch(subathondata.subathon_id)
|
||||
subba = ActiveSubathon(subathondata, running)
|
||||
return subba
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user