Compare commits
6 Commits
32b84625df
...
89f9ce3ffa
| Author | SHA1 | Date | |
|---|---|---|---|
| 89f9ce3ffa | |||
| 21cf65398c | |||
| 84791e6b91 | |||
| 7a9a47f11f | |||
| 0c5250f09e | |||
| aa87fd6f9f |
@@ -1,15 +1,23 @@
|
|||||||
from typing import Optional, TypeAlias, TypedDict
|
from typing import Optional, TypeAlias, TypedDict
|
||||||
|
import json
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from data.queries import JOINTYPE, ORDER
|
||||||
from meta.sockets import Channel
|
from meta.sockets import Channel
|
||||||
from utils.lib import utc_now
|
from utils.lib import utc_now
|
||||||
from modules.profiles.profiles.profiles import ProfilesRegistry
|
from modules.profiles.profiles.profiles import ProfilesRegistry
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
from .data import SubathonData, Subathon, RunningSubathon, SubathonContribution, SubathonGoal
|
from .data import (
|
||||||
|
SubathonData,
|
||||||
|
Subathon,
|
||||||
|
RunningSubathon,
|
||||||
|
SubathonContribution,
|
||||||
|
SubathonGoal,
|
||||||
|
)
|
||||||
from .subathon import ActiveSubathon, SubathonRegistry
|
from .subathon import ActiveSubathon, SubathonRegistry
|
||||||
|
|
||||||
|
|
||||||
@@ -31,6 +39,12 @@ class ContributionPayload(TypedDict):
|
|||||||
timestamp: ISOTimestamp
|
timestamp: ISOTimestamp
|
||||||
|
|
||||||
|
|
||||||
|
class LeaderboardItemPayload(TypedDict):
|
||||||
|
user_name: str
|
||||||
|
user_id: str | None
|
||||||
|
amount: float
|
||||||
|
|
||||||
|
|
||||||
class ScoreTablePayload(TypedDict):
|
class ScoreTablePayload(TypedDict):
|
||||||
bit_score: float # points per bit
|
bit_score: float # points per bit
|
||||||
t1_score: float # points per T1 sub
|
t1_score: float # points per T1 sub
|
||||||
@@ -47,31 +61,44 @@ class SubathonPayload(TypedDict):
|
|||||||
name: str
|
name: str
|
||||||
|
|
||||||
total_contribution: float
|
total_contribution: float
|
||||||
|
total_subpoints: int
|
||||||
|
total_bits: int
|
||||||
|
|
||||||
goals_met: int
|
goals_met: int
|
||||||
goals_total: int
|
goals_total: int
|
||||||
|
|
||||||
last_goal: GoalPayload | None
|
last_goal: GoalPayload | None
|
||||||
next_goal: GoalPayload | None
|
next_goal: GoalPayload | None
|
||||||
|
goals: list[GoalPayload]
|
||||||
|
|
||||||
last_contribution: ContributionPayload | None
|
last_contribution: ContributionPayload | None
|
||||||
|
leaderboard: list[LeaderboardItemPayload]
|
||||||
|
|
||||||
|
|
||||||
async def prepare_subathon(profiler: ProfilesRegistry, subathon: ActiveSubathon) -> SubathonPayload:
|
async def prepare_subathon(
|
||||||
now = utc_now()
|
profiler: ProfilesRegistry, subathon: ActiveSubathon
|
||||||
|
) -> SubathonPayload:
|
||||||
total_score = await subathon.get_score()
|
total_score = await subathon.get_score()
|
||||||
|
|
||||||
|
# Build the goal data
|
||||||
goals = await subathon.get_goals()
|
goals = await subathon.get_goals()
|
||||||
goals_met = 0
|
goals_met = 0
|
||||||
last_goal = None
|
last_goalp = None
|
||||||
next_goal = None
|
next_goalp = None
|
||||||
|
goalps = []
|
||||||
total_goals = len(goals)
|
total_goals = len(goals)
|
||||||
for goal in goals:
|
for goal in goals:
|
||||||
if goal.required_score >= total_score:
|
goalp = GoalPayload(
|
||||||
last_goal = goal
|
required=float(goal.required_score),
|
||||||
|
name=goal.description,
|
||||||
|
)
|
||||||
|
goalps.append(goalp)
|
||||||
|
if goal.required_score <= total_score:
|
||||||
|
last_goalp = goalp
|
||||||
goals_met += 1
|
goals_met += 1
|
||||||
else:
|
elif next_goalp is None:
|
||||||
next_goal = goal
|
next_goalp = goalp
|
||||||
break
|
|
||||||
|
|
||||||
|
# Build the last contribution information
|
||||||
contribs = await subathon.fetch_contributions().limit(1)
|
contribs = await subathon.fetch_contributions().limit(1)
|
||||||
last_contrib: ContributionPayload | None = None
|
last_contrib: ContributionPayload | None = None
|
||||||
if contribs:
|
if contribs:
|
||||||
@@ -82,16 +109,17 @@ async def prepare_subathon(profiler: ProfilesRegistry, subathon: ActiveSubathon)
|
|||||||
name = profile.nickname
|
name = profile.nickname
|
||||||
user_id = None
|
user_id = None
|
||||||
else:
|
else:
|
||||||
name = 'Anonymous'
|
name = "Anonymous"
|
||||||
user_id = None
|
user_id = None
|
||||||
last_contrib = ContributionPayload(
|
last_contrib = ContributionPayload(
|
||||||
user_name=name,
|
user_name=name,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
amount=float(contrib.score),
|
amount=float(contrib.score),
|
||||||
seconds_added=subathon.get_score_time(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(
|
score_table = ScoreTablePayload(
|
||||||
bit_score=float(subathon.subathondata.bit_score),
|
bit_score=float(subathon.subathondata.bit_score),
|
||||||
t1_score=float(subathon.subathondata.sub1_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),
|
t3_score=float(subathon.subathondata.sub3_score),
|
||||||
score_time=int(subathon.subathondata.score_time),
|
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(
|
payload = SubathonPayload(
|
||||||
name=subathon.subathondata.name,
|
name=subathon.subathondata.name,
|
||||||
end_at=(await subathon.get_ending()).isoformat(),
|
end_at=(await subathon.get_ending()).isoformat(),
|
||||||
is_running=(subathon.running),
|
is_running=(subathon.running),
|
||||||
score_table=score_table,
|
score_table=score_table,
|
||||||
total_contribution=float(await subathon.get_score()),
|
total_contribution=float(await subathon.get_score()),
|
||||||
|
total_subpoints=subpoints,
|
||||||
|
total_bits=bits,
|
||||||
goals_met=goals_met,
|
goals_met=goals_met,
|
||||||
goals_total=total_goals,
|
goals_total=total_goals,
|
||||||
last_goal=GoalPayload(
|
last_goal=last_goalp,
|
||||||
required=float(last_goal.required_score),
|
next_goal=next_goalp,
|
||||||
name=last_goal.description,
|
goals=goalps,
|
||||||
) if last_goal is not None else None,
|
last_contribution=last_contrib,
|
||||||
next_goal=GoalPayload(
|
leaderboard=leaderboard,
|
||||||
required=float(next_goal.required_score),
|
|
||||||
name=next_goal.description,
|
|
||||||
) if next_goal is not None else None,
|
|
||||||
last_contribution=last_contrib
|
|
||||||
)
|
)
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
class TimerChannel(Channel):
|
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)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.profiler: ProfilesRegistry = profiler
|
self.profiler: ProfilesRegistry = profiler
|
||||||
self.subathons: SubathonRegistry = subathons
|
self.subathons: SubathonRegistry = subathons
|
||||||
|
|
||||||
# Map of communities to webhooks listening for this community
|
# Map of communities to webhooks listening for this community
|
||||||
self.communities = defaultdict(set) # Map of communityid -> listening websockets
|
self.communities = defaultdict(
|
||||||
|
set
|
||||||
|
) # Map of communityid -> listening websockets
|
||||||
|
|
||||||
async def on_connection(self, websocket, event):
|
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")
|
raise ValueError("Subathon timer connection missing communityid")
|
||||||
elif not cidstr.isdigit():
|
elif not cidstr.isdigit():
|
||||||
raise ValueError("Community id provided is not an integer")
|
raise ValueError("Community id provided is not an integer")
|
||||||
@@ -157,35 +219,43 @@ class TimerChannel(Channel):
|
|||||||
wss.discard(websocket)
|
wss.discard(websocket)
|
||||||
await super().del_connection(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]:
|
for ws in (websocket,) if websocket else self.communities[communityid]:
|
||||||
await self.send_event(
|
await self.send_event(
|
||||||
{
|
{
|
||||||
'type': "DO",
|
"type": "DO",
|
||||||
'method': "setTimer",
|
"method": "setTimer",
|
||||||
'args': payload,
|
"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]:
|
for ws in (websocket,) if websocket else self.communities[communityid]:
|
||||||
await self.send_event(
|
await self.send_event(
|
||||||
{
|
{
|
||||||
'type': "DO",
|
"type": "DO",
|
||||||
'method': "endTimer",
|
"method": "endTimer",
|
||||||
'args': payload,
|
"args": payload,
|
||||||
},
|
},
|
||||||
websocket=ws
|
websocket=ws,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def send_no_subathon(self, communityid: int, websocket=None):
|
async def send_no_subathon(self, communityid: int, websocket=None):
|
||||||
for ws in (websocket,) if websocket else self.communities[communityid]:
|
for ws in (websocket,) if websocket else self.communities[communityid]:
|
||||||
await self.send_event(
|
await self.send_event(
|
||||||
{
|
{
|
||||||
'type': "DO",
|
"type": "DO",
|
||||||
'method': "noTimer",
|
"method": "noTimer",
|
||||||
'args': {},
|
"args": {},
|
||||||
},
|
},
|
||||||
websocket=ws
|
websocket=ws,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def send_event(self, event, **kwargs):
|
||||||
|
logger.info(f"Sending websocket event: {json.dumps(event, indent=1)}")
|
||||||
|
await super().send_event(event, **kwargs)
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ from meta.sockets import Channel, register_channel
|
|||||||
from utils.lib import utc_now, strfdelta
|
from utils.lib import utc_now, strfdelta
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
from .data import SubathonData, Subathon, RunningSubathon, SubathonContribution, SubathonGoal
|
from .data import (
|
||||||
|
SubathonData,
|
||||||
|
Subathon,
|
||||||
|
RunningSubathon,
|
||||||
|
SubathonContribution,
|
||||||
|
SubathonGoal,
|
||||||
|
)
|
||||||
from .subathon import ActiveSubathon, SubathonRegistry
|
from .subathon import ActiveSubathon, SubathonRegistry
|
||||||
from .channel import SubathonPayload, prepare_subathon, TimerChannel
|
from .channel import SubathonPayload, prepare_subathon, TimerChannel
|
||||||
|
|
||||||
@@ -49,6 +55,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
Description:
|
Description:
|
||||||
Examples:
|
Examples:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO: Add explicit dependencies and version checks
|
# TODO: Add explicit dependencies and version checks
|
||||||
# for profile and event tracker modules
|
# for profile and event tracker modules
|
||||||
|
|
||||||
@@ -58,7 +65,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
self.subathons = SubathonRegistry(self.data, bot.profiles.profiles)
|
self.subathons = SubathonRegistry(self.data, bot.profiles.profiles)
|
||||||
self.channel = TimerChannel(self.bot.profiles.profiles, self.subathons)
|
self.channel = TimerChannel(self.bot.profiles.profiles, self.subathons)
|
||||||
|
|
||||||
register_channel('SubTimer', self.channel)
|
register_channel("SubTimer", self.channel)
|
||||||
|
|
||||||
# ----- API -----
|
# ----- API -----
|
||||||
async def component_load(self):
|
async def component_load(self):
|
||||||
@@ -98,20 +105,24 @@ class SubathonComponent(cmds.Component):
|
|||||||
# Blerp handler
|
# Blerp handler
|
||||||
@cmds.Component.listener()
|
@cmds.Component.listener()
|
||||||
async def event_message(self, message: twitchio.ChatMessage):
|
async def event_message(self, message: twitchio.ChatMessage):
|
||||||
if message.chatter.id not in ('253326823', '1361913054'):
|
if message.chatter.id not in ("253326823", "1361913054"):
|
||||||
return
|
return
|
||||||
if 'bits' not in message.text:
|
if "bits" not in message.text:
|
||||||
return
|
return
|
||||||
community = await self.bot.profiles.fetch_community(message.broadcaster)
|
community = await self.bot.profiles.fetch_community(message.broadcaster)
|
||||||
cid = community.communityid
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is not None and not await active.check_finished():
|
if (
|
||||||
|
active := await self.get_active_subathon(cid)
|
||||||
|
) is not None and not await active.check_finished():
|
||||||
# Extract count and user
|
# Extract count and user
|
||||||
match = re.match(r"(?P<name>\w+) used (?P<amount>\d+)", message.text)
|
match = re.match(r"(?P<name>\w+) used (?P<amount>\d+)", message.text)
|
||||||
if not match:
|
if not match:
|
||||||
match = re.match(r"!For (?P<amount>\d+) bits, (?P<name>\w+)", message.text)
|
match = re.match(
|
||||||
|
r"!For (?P<amount>\d+) bits, (?P<name>\w+)", message.text
|
||||||
|
)
|
||||||
if match:
|
if match:
|
||||||
amount = int(match['amount'])
|
amount = int(match["amount"])
|
||||||
name = match['name']
|
name = match["name"]
|
||||||
# This is for giving the contribution to the calling user
|
# This is for giving the contribution to the calling user
|
||||||
# user = await self.bot.fetch_user(login=name.lower())
|
# user = await self.bot.fetch_user(login=name.lower())
|
||||||
user = await message.chatter.user()
|
user = await message.chatter.user()
|
||||||
@@ -128,12 +139,15 @@ class SubathonComponent(cmds.Component):
|
|||||||
@cmds.Component.listener()
|
@cmds.Component.listener()
|
||||||
async def event_safe_bits_use(self, payload):
|
async def event_safe_bits_use(self, payload):
|
||||||
event_row, detail_row, bits_payload = payload
|
event_row, detail_row, bits_payload = payload
|
||||||
if (active := await self.get_active_subathon(event_row['communityid'])) is not None and not await active.check_finished():
|
if (
|
||||||
|
active := await self.get_active_subathon(event_row["communityid"])
|
||||||
|
) is not None and not await active.check_finished():
|
||||||
# In an active subathon
|
# In an active subathon
|
||||||
pid = event_row['profileid']
|
pid = event_row["profileid"]
|
||||||
uid = event_row['user_id']
|
uid = event_row["user_id"]
|
||||||
score = detail_row['bits'] * active.subathondata.bit_score
|
score = detail_row["bits"] * active.subathondata.bit_score
|
||||||
await active.add_contribution(pid, score, event_row['event_id'])
|
logger.info(f"Adding subathon contribution from bits payload {payload}")
|
||||||
|
await active.add_contribution(pid, score, event_row["event_id"])
|
||||||
|
|
||||||
# Send message to channel
|
# Send message to channel
|
||||||
sec = active.get_score_time(score)
|
sec = active.get_score_time(score)
|
||||||
@@ -143,7 +157,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
else:
|
else:
|
||||||
added = f"{sec} seconds"
|
added = f"{sec} seconds"
|
||||||
name = bits_payload.user.name
|
name = bits_payload.user.name
|
||||||
pl = 's' if bits_payload.bits != 1 else ''
|
pl = "s" if bits_payload.bits != 1 else ""
|
||||||
|
|
||||||
contrib_str = f"{name} contributed {score} point{pl}"
|
contrib_str = f"{name} contributed {score} point{pl}"
|
||||||
if not await active.check_cap():
|
if not await active.check_cap():
|
||||||
@@ -152,8 +166,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
contrib_str += " towards our subathon! Thank you <3"
|
contrib_str += " towards our subathon! Thank you <3"
|
||||||
|
|
||||||
await bits_payload.broadcaster.send_message(
|
await bits_payload.broadcaster.send_message(
|
||||||
contrib_str,
|
contrib_str, sender=self.bot.bot_id
|
||||||
sender=self.bot.bot_id
|
|
||||||
)
|
)
|
||||||
await self.dispatch_update(active)
|
await self.dispatch_update(active)
|
||||||
await self.goalcheck(active, bits_payload.broadcaster)
|
await self.goalcheck(active, bits_payload.broadcaster)
|
||||||
@@ -166,10 +179,12 @@ class SubathonComponent(cmds.Component):
|
|||||||
# Ignore gifted here
|
# Ignore gifted here
|
||||||
return
|
return
|
||||||
|
|
||||||
if (active := await self.get_active_subathon(event_row['communityid'])) is not None and not await active.check_finished():
|
if (
|
||||||
|
active := await self.get_active_subathon(event_row["communityid"])
|
||||||
|
) is not None and not await active.check_finished():
|
||||||
data = active.subathondata
|
data = active.subathondata
|
||||||
# In an active subathon
|
# In an active subathon
|
||||||
pid = event_row['profileid']
|
pid = event_row["profileid"]
|
||||||
tier = int(sub_payload.tier)
|
tier = int(sub_payload.tier)
|
||||||
|
|
||||||
if tier == 1000:
|
if tier == 1000:
|
||||||
@@ -183,13 +198,16 @@ class SubathonComponent(cmds.Component):
|
|||||||
|
|
||||||
score = mult * 1
|
score = mult * 1
|
||||||
|
|
||||||
await active.add_contribution(pid, score, event_row['event_id'])
|
logger.info(
|
||||||
|
f"Adding subathon contribution from subscription payload {payload}"
|
||||||
|
)
|
||||||
|
await active.add_contribution(pid, score, event_row["event_id"])
|
||||||
|
|
||||||
# Send message to channel
|
# Send message to channel
|
||||||
added_min = int(active.get_score_time(score) // 60)
|
added_min = int(active.get_score_time(score) // 60)
|
||||||
added = f"{added_min} minutes"
|
added = f"{added_min} minutes"
|
||||||
name = sub_payload.user.name
|
name = sub_payload.user.name
|
||||||
pl = 's' if score > 1 else ''
|
pl = "s" if score > 1 else ""
|
||||||
|
|
||||||
contrib_str = f"{name} contributed {score} point{pl}"
|
contrib_str = f"{name} contributed {score} point{pl}"
|
||||||
if not await active.check_cap():
|
if not await active.check_cap():
|
||||||
@@ -198,8 +216,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
contrib_str += " towards our subathon! Thank you <3"
|
contrib_str += " towards our subathon! Thank you <3"
|
||||||
|
|
||||||
await sub_payload.broadcaster.send_message(
|
await sub_payload.broadcaster.send_message(
|
||||||
contrib_str,
|
contrib_str, sender=self.bot.bot_id
|
||||||
sender=self.bot.bot_id
|
|
||||||
)
|
)
|
||||||
await self.dispatch_update(active)
|
await self.dispatch_update(active)
|
||||||
# Check goals
|
# Check goals
|
||||||
@@ -209,10 +226,12 @@ class SubathonComponent(cmds.Component):
|
|||||||
async def event_safe_subscription_gift(self, payload):
|
async def event_safe_subscription_gift(self, payload):
|
||||||
event_row, detail_row, gift_payload = payload
|
event_row, detail_row, gift_payload = payload
|
||||||
|
|
||||||
if (active := await self.get_active_subathon(event_row['communityid'])) is not None and not await active.check_finished():
|
if (
|
||||||
|
active := await self.get_active_subathon(event_row["communityid"])
|
||||||
|
) is not None and not await active.check_finished():
|
||||||
data = active.subathondata
|
data = active.subathondata
|
||||||
# In an active subathon
|
# In an active subathon
|
||||||
pid = event_row['profileid']
|
pid = event_row["profileid"]
|
||||||
|
|
||||||
tier = int(gift_payload.tier)
|
tier = int(gift_payload.tier)
|
||||||
if tier == 1000:
|
if tier == 1000:
|
||||||
@@ -226,12 +245,15 @@ class SubathonComponent(cmds.Component):
|
|||||||
|
|
||||||
score = mult * gift_payload.total
|
score = mult * gift_payload.total
|
||||||
|
|
||||||
await active.add_contribution(pid, score, event_row['event_id'])
|
logger.info(
|
||||||
|
f"Adding subathon contribution from subscription gift payload {payload}"
|
||||||
|
)
|
||||||
|
await active.add_contribution(pid, score, event_row["event_id"])
|
||||||
|
|
||||||
# Send message to channel
|
# Send message to channel
|
||||||
added_min = int(active.get_score_time(score) // 60)
|
added_min = int(active.get_score_time(score) // 60)
|
||||||
added = f"{added_min} minutes"
|
added = f"{added_min} minutes"
|
||||||
name = gift_payload.user.name if gift_payload.user else 'Anonymous'
|
name = gift_payload.user.name if gift_payload.user else "Anonymous"
|
||||||
|
|
||||||
contrib_str = f"{name} contributed {score} points"
|
contrib_str = f"{name} contributed {score} points"
|
||||||
if not await active.check_cap():
|
if not await active.check_cap():
|
||||||
@@ -240,8 +262,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
contrib_str += " towards our subathon! Thank you <3"
|
contrib_str += " towards our subathon! Thank you <3"
|
||||||
|
|
||||||
await gift_payload.broadcaster.send_message(
|
await gift_payload.broadcaster.send_message(
|
||||||
contrib_str,
|
contrib_str, sender=self.bot.bot_id
|
||||||
sender=self.bot.bot_id
|
|
||||||
)
|
)
|
||||||
await self.dispatch_update(active)
|
await self.dispatch_update(active)
|
||||||
# Check goals
|
# Check goals
|
||||||
@@ -251,10 +272,12 @@ class SubathonComponent(cmds.Component):
|
|||||||
async def event_safe_subscription_message(self, payload):
|
async def event_safe_subscription_message(self, payload):
|
||||||
event_row, detail_row, sub_payload = payload
|
event_row, detail_row, sub_payload = payload
|
||||||
|
|
||||||
if (active := await self.get_active_subathon(event_row['communityid'])) is not None and not await active.check_finished():
|
if (
|
||||||
|
active := await self.get_active_subathon(event_row["communityid"])
|
||||||
|
) is not None and not await active.check_finished():
|
||||||
data = active.subathondata
|
data = active.subathondata
|
||||||
# In an active subathon
|
# In an active subathon
|
||||||
pid = event_row['profileid']
|
pid = event_row["profileid"]
|
||||||
tier = int(sub_payload.tier)
|
tier = int(sub_payload.tier)
|
||||||
|
|
||||||
if tier == 1000:
|
if tier == 1000:
|
||||||
@@ -268,13 +291,16 @@ class SubathonComponent(cmds.Component):
|
|||||||
|
|
||||||
score = mult * 1
|
score = mult * 1
|
||||||
|
|
||||||
await active.add_contribution(pid, score, event_row['event_id'])
|
logger.info(
|
||||||
|
f"Adding subathon contribution from subscription message payload {payload}"
|
||||||
|
)
|
||||||
|
await active.add_contribution(pid, score, event_row["event_id"])
|
||||||
|
|
||||||
# Send message to channel
|
# Send message to channel
|
||||||
added_min = int(active.get_score_time(score) // 60)
|
added_min = int(active.get_score_time(score) // 60)
|
||||||
added = f"{added_min} minutes"
|
added = f"{added_min} minutes"
|
||||||
name = sub_payload.user.name
|
name = sub_payload.user.name
|
||||||
pl = 's' if score > 1 else ''
|
pl = "s" if score > 1 else ""
|
||||||
|
|
||||||
contrib_str = f"{name} contributed {score} points{pl}"
|
contrib_str = f"{name} contributed {score} points{pl}"
|
||||||
if not await active.check_cap():
|
if not await active.check_cap():
|
||||||
@@ -283,8 +309,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
contrib_str += " towards our subathon! Thank you <3"
|
contrib_str += " towards our subathon! Thank you <3"
|
||||||
|
|
||||||
await sub_payload.broadcaster.send_message(
|
await sub_payload.broadcaster.send_message(
|
||||||
contrib_str,
|
contrib_str, sender=self.bot.bot_id
|
||||||
sender=self.bot.bot_id
|
|
||||||
)
|
)
|
||||||
await self.dispatch_update(active)
|
await self.dispatch_update(active)
|
||||||
# Check goals
|
# Check goals
|
||||||
@@ -297,17 +322,20 @@ class SubathonComponent(cmds.Component):
|
|||||||
cid = community.communityid
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is not None:
|
if (active := await self.get_active_subathon(cid)) is not None:
|
||||||
if active.running:
|
if active.running:
|
||||||
|
logger.info(
|
||||||
|
f"Automatically paused suabathon {active.subathondata!r} from stream offline."
|
||||||
|
)
|
||||||
await active.pause()
|
await active.pause()
|
||||||
if not await active.check_finished():
|
if not await active.check_finished():
|
||||||
await payload.broadcaster.send_message(
|
await payload.broadcaster.send_message(
|
||||||
"Paused the subathon timer because the stream went offline!",
|
"Paused the subathon timer because the stream went offline!",
|
||||||
sender=self.bot.bot_id
|
sender=self.bot.bot_id,
|
||||||
)
|
)
|
||||||
await self.dispatch_update(active)
|
await self.dispatch_update(active)
|
||||||
|
|
||||||
# ----- Commands -----
|
# ----- Commands -----
|
||||||
|
|
||||||
@cmds.group(name='subathon', aliases=['studython'], invoke_fallback=True)
|
@cmds.group(name="subathon", aliases=["studython"], invoke_fallback=True)
|
||||||
async def group_subathon(self, ctx: cmds.Context):
|
async def group_subathon(self, ctx: cmds.Context):
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
cid = community.communityid
|
cid = community.communityid
|
||||||
@@ -325,13 +353,9 @@ class SubathonComponent(cmds.Component):
|
|||||||
if secs > 0:
|
if secs > 0:
|
||||||
remaining = strfdelta(timedelta(seconds=secs))
|
remaining = strfdelta(timedelta(seconds=secs))
|
||||||
|
|
||||||
text = (
|
text = f"{active.name} running for {duration}! {score} points recieved, {goalstr}, and {remaining} left on the timer"
|
||||||
f"{active.name} running for {duration}! {score} points recieved, {goalstr}, and {remaining} left on the timer"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
text = (
|
text = f"{active.name} completed after {duration} with a total of {score} points, and {goalstr}!"
|
||||||
f"{active.name} completed after {duration} with a total of {score} points, and {goalstr}!"
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.reply(text)
|
await ctx.reply(text)
|
||||||
else:
|
else:
|
||||||
@@ -339,9 +363,19 @@ class SubathonComponent(cmds.Component):
|
|||||||
|
|
||||||
# subathon start
|
# subathon start
|
||||||
# TODO: Usage line on command error
|
# TODO: Usage line on command error
|
||||||
@group_subathon.command(name='setup', alias='start')
|
@group_subathon.command(name="setup", alias="start")
|
||||||
@cmds.is_broadcaster()
|
@cmds.is_broadcaster()
|
||||||
async def cmd_setup(self, ctx: cmds.Context, initial_hours: float, sub1: float, sub2: float, sub3: float, bit: float, timescore: int, timecap: Optional[int]=None):
|
async def cmd_setup(
|
||||||
|
self,
|
||||||
|
ctx: cmds.Context,
|
||||||
|
initial_hours: float,
|
||||||
|
sub1: float,
|
||||||
|
sub2: float,
|
||||||
|
sub3: float,
|
||||||
|
bit: float,
|
||||||
|
timescore: int,
|
||||||
|
timecap: Optional[int] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Creates a new subathon.
|
Creates a new subathon.
|
||||||
USAGE: {prefix}subathon setup <initial_hours> <sub1_points> <sub2_points> <sub3_points> <bit_points> <timepoints> [timecap]
|
USAGE: {prefix}subathon setup <initial_hours> <sub1_points> <sub2_points> <sub3_points> <bit_points> <timepoints> [timecap]
|
||||||
@@ -357,7 +391,9 @@ class SubathonComponent(cmds.Component):
|
|||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
cid = community.communityid
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is not None:
|
if (active := await self.get_active_subathon(cid)) is not None:
|
||||||
await ctx.reply("There is already an active subathon running! Use !subathon stop to stop it!")
|
await ctx.reply(
|
||||||
|
"There is already an active subathon running! Use !subathon stop to stop it!"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
initial_time = initial_hours * 60 * 60
|
initial_time = initial_hours * 60 * 60
|
||||||
timecap_seconds = timecap * 60 * 60 if timecap else None
|
timecap_seconds = timecap * 60 * 60 if timecap else None
|
||||||
@@ -371,9 +407,9 @@ class SubathonComponent(cmds.Component):
|
|||||||
sub3_score=sub3,
|
sub3_score=sub3,
|
||||||
bit_score=bit,
|
bit_score=bit,
|
||||||
score_time=timescore,
|
score_time=timescore,
|
||||||
timecap=timecap_seconds
|
timecap=timecap_seconds,
|
||||||
)
|
)
|
||||||
base_timer_url = self.bot.config.subathon['timer_url']
|
base_timer_url = self.bot.config.subathon["timer_url"]
|
||||||
timer_link = f"{base_timer_url}?community={cid}"
|
timer_link = f"{base_timer_url}?community={cid}"
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
f"Setup your subathon! "
|
f"Setup your subathon! "
|
||||||
@@ -385,7 +421,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
await self.dispatch_update(active)
|
await self.dispatch_update(active)
|
||||||
|
|
||||||
# subathon stop
|
# subathon stop
|
||||||
@group_subathon.command(name='stop')
|
@group_subathon.command(name="stop")
|
||||||
@cmds.is_broadcaster()
|
@cmds.is_broadcaster()
|
||||||
async def cmd_stop(self, ctx: cmds.Context):
|
async def cmd_stop(self, ctx: cmds.Context):
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
@@ -406,22 +442,20 @@ class SubathonComponent(cmds.Component):
|
|||||||
await ctx.reply("No active subathon to stop.")
|
await ctx.reply("No active subathon to stop.")
|
||||||
|
|
||||||
# subathon link
|
# subathon link
|
||||||
@group_subathon.command(name='link')
|
@group_subathon.command(name="link")
|
||||||
@cmds.is_moderator()
|
@cmds.is_moderator()
|
||||||
async def cmd_link(self, ctx: cmds.Context):
|
async def cmd_link(self, ctx: cmds.Context):
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
cid = community.communityid
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is not None:
|
if (active := await self.get_active_subathon(cid)) is not None:
|
||||||
base_timer_url = self.bot.config.subathon['timer_url']
|
base_timer_url = self.bot.config.subathon["timer_url"]
|
||||||
timer_link = f"{base_timer_url}?community={cid}"
|
timer_link = f"{base_timer_url}?community={cid}"
|
||||||
await ctx.reply(
|
await ctx.reply(timer_link)
|
||||||
timer_link
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await ctx.reply("No active subathon to stop.")
|
await ctx.reply("No active subathon to stop.")
|
||||||
|
|
||||||
# subathon pause
|
# subathon pause
|
||||||
@group_subathon.command(name='pause')
|
@group_subathon.command(name="pause")
|
||||||
@cmds.is_moderator()
|
@cmds.is_moderator()
|
||||||
async def cmd_pause(self, ctx: cmds.Context):
|
async def cmd_pause(self, ctx: cmds.Context):
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
@@ -437,7 +471,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
await ctx.reply("No active subathon to pause")
|
await ctx.reply("No active subathon to pause")
|
||||||
|
|
||||||
# subathon resume
|
# subathon resume
|
||||||
@group_subathon.command(name='resume')
|
@group_subathon.command(name="resume")
|
||||||
@cmds.is_moderator()
|
@cmds.is_moderator()
|
||||||
async def cmd_resume(self, ctx: cmds.Context):
|
async def cmd_resume(self, ctx: cmds.Context):
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
@@ -455,9 +489,11 @@ class SubathonComponent(cmds.Component):
|
|||||||
await ctx.reply("No active subathon to resume")
|
await ctx.reply("No active subathon to resume")
|
||||||
|
|
||||||
# subathon adjust
|
# subathon adjust
|
||||||
@group_subathon.command(name='adjust')
|
@group_subathon.command(name="adjust")
|
||||||
@cmds.is_moderator()
|
@cmds.is_moderator()
|
||||||
async def cmd_subathon_adjust(self, ctx: Context, amount: int, *, user: Optional[twitchio.User] = None):
|
async def cmd_subathon_adjust(
|
||||||
|
self, ctx: Context, amount: int, *, user: Optional[twitchio.User] = None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Directly add or remove points from the subathon.
|
Directly add or remove points from the subathon.
|
||||||
If a user is provided, will adjust their contributed amount.
|
If a user is provided, will adjust their contributed amount.
|
||||||
@@ -476,7 +512,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
if (active := await self.get_active_subathon(cid)) is not None:
|
if (active := await self.get_active_subathon(cid)) is not None:
|
||||||
if user is not None:
|
if user is not None:
|
||||||
profile = await self.bot.profiles.fetch_profile(user)
|
profile = await self.bot.profiles.fetch_profile(user)
|
||||||
name = user.display_name or profile.nickname or 'Unknown'
|
name = user.display_name or profile.nickname or "Unknown"
|
||||||
pid = profile.profileid
|
pid = profile.profileid
|
||||||
else:
|
else:
|
||||||
profile = None
|
profile = None
|
||||||
@@ -524,7 +560,8 @@ class SubathonComponent(cmds.Component):
|
|||||||
WARNING: This setting is retroactive and should generally not be changed.
|
WARNING: This setting is retroactive and should generally not be changed.
|
||||||
Accepts an integer. Example: !subathon config timecap 10
|
Accepts an integer. Example: !subathon config timecap 10
|
||||||
"""
|
"""
|
||||||
@group_subathon.group(name='config', aliases=('option',), invoke_fallback=True)
|
|
||||||
|
@group_subathon.group(name="config", aliases=("option",), invoke_fallback=True)
|
||||||
@cmds.is_moderator()
|
@cmds.is_moderator()
|
||||||
async def subathon_config_grp(self, ctx: Context, *, args: Optional[str] = None):
|
async def subathon_config_grp(self, ctx: Context, *, args: Optional[str] = None):
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
@@ -544,38 +581,35 @@ class SubathonComponent(cmds.Component):
|
|||||||
sdata = active.subathondata
|
sdata = active.subathondata
|
||||||
|
|
||||||
# name
|
# name
|
||||||
parts.append(
|
parts.append(f'name="{sdata.name}"')
|
||||||
f"name=\"{sdata.name}\""
|
|
||||||
)
|
|
||||||
|
|
||||||
# timecamp
|
# timecamp
|
||||||
cap = sdata.timecap or 0
|
cap = sdata.timecap or 0
|
||||||
caph = int(cap / 3600)
|
caph = int(cap / 3600)
|
||||||
parts.append(
|
parts.append(f"cap={caph} (hours, 0 means no cap)")
|
||||||
f"cap={caph} (hours, 0 means no cap)"
|
|
||||||
)
|
|
||||||
|
|
||||||
# scores
|
# scores
|
||||||
scores = map(float, (sdata.sub1_score, sdata.sub2_score, sdata.sub3_score, sdata.bit_score))
|
scores = map(
|
||||||
scorestr = ' '.join(map(str, scores))
|
float,
|
||||||
parts.append(
|
(sdata.sub1_score, sdata.sub2_score, sdata.sub3_score, sdata.bit_score),
|
||||||
f"scores={scorestr} (t1, t2, t3, and 1 bit point scores)"
|
|
||||||
)
|
)
|
||||||
|
scorestr = " ".join(map(str, scores))
|
||||||
|
parts.append(f"scores={scorestr} (t1, t2, t3, and 1 bit point scores)")
|
||||||
|
|
||||||
# timescore
|
# timescore
|
||||||
parts.append(
|
parts.append(f"timescore={sdata.score_time} (seconds per point)")
|
||||||
f"timescore={sdata.score_time} (seconds per point)"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Combine
|
# Combine
|
||||||
partstr = ' ; '.join(parts)
|
partstr = " ; ".join(parts)
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
f"{partstr} ; Use {ctx.prefix}subathon config <option> [value] to see or set each option!"
|
f"{partstr} ; Use {ctx.prefix}subathon config <option> [value] to see or set each option!"
|
||||||
)
|
)
|
||||||
|
|
||||||
@subathon_config_grp.command(name='name')
|
@subathon_config_grp.command(name="name")
|
||||||
@cmds.is_moderator()
|
@cmds.is_moderator()
|
||||||
async def subathon_config_name_cmd(self, ctx: Context, *, args: Optional[str] = None):
|
async def subathon_config_name_cmd(
|
||||||
|
self, ctx: Context, *, args: Optional[str] = None
|
||||||
|
):
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
cid = community.communityid
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is None:
|
if (active := await self.get_active_subathon(cid)) is None:
|
||||||
@@ -586,20 +620,22 @@ class SubathonComponent(cmds.Component):
|
|||||||
# Setting path
|
# Setting path
|
||||||
await active.subathondata.update(name=args)
|
await active.subathondata.update(name=args)
|
||||||
await self.dispatch_update(active)
|
await self.dispatch_update(active)
|
||||||
await ctx.reply(f"Updated the subathon name to \"{args}\"")
|
await ctx.reply(f'Updated the subathon name to "{args}"')
|
||||||
else:
|
else:
|
||||||
# Display path
|
# Display path
|
||||||
name = active.subathondata.name
|
name = active.subathondata.name
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
"Name of the subathon, used whenever the subathon is mentioned. "
|
"Name of the subathon, used whenever the subathon is mentioned. "
|
||||||
"Accepts any string. "
|
"Accepts any string. "
|
||||||
f"Currently: \"{name}\" "
|
f'Currently: "{name}" '
|
||||||
f"Example: {ctx.prefix}subathon config name Birthday Subathon"
|
f"Example: {ctx.prefix}subathon config name Birthday Subathon"
|
||||||
)
|
)
|
||||||
|
|
||||||
@subathon_config_grp.command(name='cap')
|
@subathon_config_grp.command(name="cap")
|
||||||
@cmds.is_moderator()
|
@cmds.is_moderator()
|
||||||
async def subathon_config_cap_cmd(self, ctx: Context, *, args: Optional[str] = None):
|
async def subathon_config_cap_cmd(
|
||||||
|
self, ctx: Context, *, args: Optional[str] = None
|
||||||
|
):
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
cid = community.communityid
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is None:
|
if (active := await self.get_active_subathon(cid)) is None:
|
||||||
@@ -608,11 +644,13 @@ class SubathonComponent(cmds.Component):
|
|||||||
|
|
||||||
if args:
|
if args:
|
||||||
# Setting path
|
# Setting path
|
||||||
if args.lower() in ('none',):
|
if args.lower() in ("none",):
|
||||||
args = '0'
|
args = "0"
|
||||||
|
|
||||||
if not args.isdigit():
|
if not args.isdigit():
|
||||||
await ctx.reply("Provided timer cap must be an integer number of hours!")
|
await ctx.reply(
|
||||||
|
"Provided timer cap must be an integer number of hours!"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
new_cap = int(args)
|
new_cap = int(args)
|
||||||
@@ -620,12 +658,16 @@ class SubathonComponent(cmds.Component):
|
|||||||
# Unset the cap
|
# Unset the cap
|
||||||
await active.subathondata.update(timecap=None)
|
await active.subathondata.update(timecap=None)
|
||||||
await self.dispatch_update(active)
|
await self.dispatch_update(active)
|
||||||
await ctx.reply("The timer cap has been removed! To infinity and beyond!")
|
await ctx.reply(
|
||||||
|
"The timer cap has been removed! To infinity and beyond!"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Set the cap
|
# Set the cap
|
||||||
await active.subathondata.update(timecap=int(new_cap * 3600))
|
await active.subathondata.update(timecap=int(new_cap * 3600))
|
||||||
await self.dispatch_update(active)
|
await self.dispatch_update(active)
|
||||||
await ctx.reply(f"The subathon timer has been capped to {new_cap} hours.")
|
await ctx.reply(
|
||||||
|
f"The subathon timer has been capped to {new_cap} hours."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Display path
|
# Display path
|
||||||
current_cap = active.subathondata.timecap or 0
|
current_cap = active.subathondata.timecap or 0
|
||||||
@@ -639,9 +681,11 @@ class SubathonComponent(cmds.Component):
|
|||||||
f"Example: {ctx.prefix}subathon config cap 24"
|
f"Example: {ctx.prefix}subathon config cap 24"
|
||||||
)
|
)
|
||||||
|
|
||||||
@subathon_config_grp.command(name='scores')
|
@subathon_config_grp.command(name="scores")
|
||||||
@cmds.is_moderator()
|
@cmds.is_moderator()
|
||||||
async def subathon_config_scores_cmd(self, ctx: Context, *, args: Optional[str] = None):
|
async def subathon_config_scores_cmd(
|
||||||
|
self, ctx: Context, *, args: Optional[str] = None
|
||||||
|
):
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
cid = community.communityid
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is None:
|
if (active := await self.get_active_subathon(cid)) is None:
|
||||||
@@ -669,8 +713,11 @@ class SubathonComponent(cmds.Component):
|
|||||||
else:
|
else:
|
||||||
# Display path
|
# Display path
|
||||||
sdata = active.subathondata
|
sdata = active.subathondata
|
||||||
scores = map(float, (sdata.sub1_score, sdata.sub2_score, sdata.sub3_score, sdata.bit_score))
|
scores = map(
|
||||||
scorestr = ' '.join(map(str, scores))
|
float,
|
||||||
|
(sdata.sub1_score, sdata.sub2_score, sdata.sub3_score, sdata.bit_score),
|
||||||
|
)
|
||||||
|
scorestr = " ".join(map(str, scores))
|
||||||
|
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
"The number of points each type of contribution (t1 sub, t2 sub, t3 sub, and 1 bit) "
|
"The number of points each type of contribution (t1 sub, t2 sub, t3 sub, and 1 bit) "
|
||||||
@@ -680,9 +727,11 @@ class SubathonComponent(cmds.Component):
|
|||||||
f"Example: {ctx.prefix}subathon config scores 5 10 20 0.1"
|
f"Example: {ctx.prefix}subathon config scores 5 10 20 0.1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@subathon_config_grp.command(name='timescore')
|
@subathon_config_grp.command(name="timescore")
|
||||||
@cmds.is_moderator()
|
@cmds.is_moderator()
|
||||||
async def subathon_config_timescore_cmd(self, ctx: Context, *, args: Optional[str] = None):
|
async def subathon_config_timescore_cmd(
|
||||||
|
self, ctx: Context, *, args: Optional[str] = None
|
||||||
|
):
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
cid = community.communityid
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is None:
|
if (active := await self.get_active_subathon(cid)) is None:
|
||||||
@@ -707,7 +756,13 @@ class SubathonComponent(cmds.Component):
|
|||||||
f"Example: {ctx.prefix}subathon config timescore 10"
|
f"Example: {ctx.prefix}subathon config timescore 10"
|
||||||
)
|
)
|
||||||
|
|
||||||
@group_subathon.command(name='leaderboard', aliases=('top', 'lb',))
|
@group_subathon.command(
|
||||||
|
name="leaderboard",
|
||||||
|
aliases=(
|
||||||
|
"top",
|
||||||
|
"lb",
|
||||||
|
),
|
||||||
|
)
|
||||||
async def cmd_subathon_lb(self, ctx: Context):
|
async def cmd_subathon_lb(self, ctx: Context):
|
||||||
"""
|
"""
|
||||||
Display top contributors by points contributed, up to 10.
|
Display top contributors by points contributed, up to 10.
|
||||||
@@ -722,34 +777,36 @@ class SubathonComponent(cmds.Component):
|
|||||||
|
|
||||||
if (active := await self.get_active_subathon(cid)) is not None:
|
if (active := await self.get_active_subathon(cid)) is not None:
|
||||||
# Get totals for all contributors
|
# Get totals for all contributors
|
||||||
query = self.data.subathon_contributions.select_where(subathon_id=active.subathondata.subathon_id)
|
query = self.data.subathon_contributions.select_where(
|
||||||
query.join('user_profiles', using=('profileid',), join_type=JOINTYPE.LEFT)
|
subathon_id=active.subathondata.subathon_id
|
||||||
query.select('subathon_id', 'profileid', total="SUM(score)")
|
)
|
||||||
query.order_by('total', direction=ORDER.DESC)
|
query.join("user_profiles", using=("profileid",), join_type=JOINTYPE.LEFT)
|
||||||
query.group_by('subathon_id', 'profileid')
|
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()
|
query.with_no_adapter()
|
||||||
results = await query
|
results = await query
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
caller_idx = None
|
caller_idx = None
|
||||||
for i, row in enumerate(results):
|
for i, row in enumerate(results):
|
||||||
if pid := row['profileid']:
|
if pid := row["profileid"]:
|
||||||
profile = await self.bot.profiles.profiles.get_profile(pid)
|
profile = await self.bot.profiles.profiles.get_profile(pid)
|
||||||
name = (profile.nickname if profile else None) or 'Unknown'
|
name = (profile.nickname if profile else None) or "Unknown"
|
||||||
if pid == caller_pid:
|
if pid == caller_pid:
|
||||||
caller_idx = i
|
caller_idx = i
|
||||||
else:
|
else:
|
||||||
name = 'Anonymous'
|
name = "Anonymous"
|
||||||
score = row['total']
|
score = row["total"]
|
||||||
part = f"{name}: {score} points"
|
part = f"{name}: {score} points"
|
||||||
parts.append(part)
|
parts.append(part)
|
||||||
|
|
||||||
header = ""
|
header = ""
|
||||||
leaderboard = ', '.join(parts[:10])
|
leaderboard = ", ".join(parts[:10])
|
||||||
footer = ""
|
footer = ""
|
||||||
if len(parts) > 10:
|
if len(parts) > 10:
|
||||||
header = f"{active.name} top 10 leaderboard: "
|
header = f"{active.name} top 10 leaderboard: "
|
||||||
leaderboard = ', '.join(parts[:10])
|
leaderboard = ", ".join(parts[:10])
|
||||||
if caller_idx is not None and caller_idx >= 10:
|
if caller_idx is not None and caller_idx >= 10:
|
||||||
caller_part = parts[caller_idx]
|
caller_part = parts[caller_idx]
|
||||||
footer = f" ... {caller_part}"
|
footer = f" ... {caller_part}"
|
||||||
@@ -764,7 +821,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
await ctx.reply("No active subathon to show leaderboard of!")
|
await ctx.reply("No active subathon to show leaderboard of!")
|
||||||
|
|
||||||
# Subathon goals
|
# Subathon goals
|
||||||
@cmds.group(name='goals', invoke_fallback=True)
|
@cmds.group(name="goals", invoke_fallback=True)
|
||||||
async def group_goals(self, ctx: cmds.Context):
|
async def group_goals(self, ctx: cmds.Context):
|
||||||
# List the goals
|
# List the goals
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
@@ -777,14 +834,14 @@ class SubathonComponent(cmds.Component):
|
|||||||
goalstrs.append(line)
|
goalstrs.append(line)
|
||||||
|
|
||||||
if goals:
|
if goals:
|
||||||
text = ', '.join(goalstrs)
|
text = ", ".join(goalstrs)
|
||||||
await ctx.reply(f"{active.name} Goals! -- {text}")
|
await ctx.reply(f"{active.name} Goals! -- {text}")
|
||||||
else:
|
else:
|
||||||
await ctx.reply("No goals have been configured!")
|
await ctx.reply("No goals have been configured!")
|
||||||
else:
|
else:
|
||||||
await ctx.reply("No active subathon running!")
|
await ctx.reply("No active subathon running!")
|
||||||
|
|
||||||
@group_goals.command(name='remaining', aliases=('left',))
|
@group_goals.command(name="remaining", aliases=("left",))
|
||||||
async def cmd_goals_remaining(self, ctx: cmds.Context):
|
async def cmd_goals_remaining(self, ctx: cmds.Context):
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
cid = community.communityid
|
cid = community.communityid
|
||||||
@@ -798,7 +855,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
goalstrs.append(line)
|
goalstrs.append(line)
|
||||||
|
|
||||||
if goalstrs:
|
if goalstrs:
|
||||||
text = ', '.join(goalstrs)
|
text = ", ".join(goalstrs)
|
||||||
await ctx.reply(f"{active.name} Goals Remaining -- {text}")
|
await ctx.reply(f"{active.name} Goals Remaining -- {text}")
|
||||||
elif goals:
|
elif goals:
|
||||||
await ctx.reply("All goals completed, congratulations!")
|
await ctx.reply("All goals completed, congratulations!")
|
||||||
@@ -807,7 +864,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
else:
|
else:
|
||||||
await ctx.reply("No active subathon running!")
|
await ctx.reply("No active subathon running!")
|
||||||
|
|
||||||
@group_goals.command(name='add')
|
@group_goals.command(name="add")
|
||||||
@cmds.is_moderator()
|
@cmds.is_moderator()
|
||||||
async def cmd_add(self, ctx: cmds.Context, required: int, *, description: str):
|
async def cmd_add(self, ctx: cmds.Context, required: int, *, description: str):
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
@@ -823,7 +880,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
await ctx.reply("No active subathon to add goal to!")
|
await ctx.reply("No active subathon to add goal to!")
|
||||||
|
|
||||||
# remove
|
# remove
|
||||||
@group_goals.command(name='remove', aliases=['del', 'delete', 'rm'])
|
@group_goals.command(name="remove", aliases=["del", "delete", "rm"])
|
||||||
@cmds.is_moderator()
|
@cmds.is_moderator()
|
||||||
async def cmd_goals_remove(self, ctx: cmds.Context, required: int):
|
async def cmd_goals_remove(self, ctx: cmds.Context, required: int):
|
||||||
"""
|
"""
|
||||||
@@ -841,4 +898,3 @@ class SubathonComponent(cmds.Component):
|
|||||||
await ctx.reply(f"Score {required} goal(s) removed!")
|
await ctx.reply(f"Score {required} goal(s) removed!")
|
||||||
else:
|
else:
|
||||||
await ctx.reply(f"No goal set at {required} score to remove.")
|
await ctx.reply(f"No goal set at {required} score to remove.")
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
from asyncio import Event
|
from asyncio import Event
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from data.queries import JOINTYPE
|
||||||
from utils.lib import utc_now, strfdelta
|
from utils.lib import utc_now, strfdelta
|
||||||
from modules.profiles.profiles.profiles import ProfilesRegistry
|
from modules.profiles.profiles.profiles import ProfilesRegistry
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
from .data import SubathonData, Subathon, RunningSubathon, SubathonContribution, SubathonGoal
|
from .data import (
|
||||||
|
SubathonData,
|
||||||
|
Subathon,
|
||||||
|
RunningSubathon,
|
||||||
|
SubathonContribution,
|
||||||
|
SubathonGoal,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ActiveSubathon:
|
class ActiveSubathon:
|
||||||
def __init__(self, subathondata: Subathon, runningdata: RunningSubathon | None):
|
def __init__(self, subathondata: Subathon, runningdata: RunningSubathon | None):
|
||||||
@@ -17,7 +25,7 @@ class ActiveSubathon:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return self.subathondata.name or 'Subathon'
|
return self.subathondata.name or "Subathon"
|
||||||
|
|
||||||
async def check_cap(self):
|
async def check_cap(self):
|
||||||
if not (cap := self.subathondata.timecap):
|
if not (cap := self.subathondata.timecap):
|
||||||
@@ -32,7 +40,7 @@ class ActiveSubathon:
|
|||||||
"""
|
"""
|
||||||
Return True if the subathon duration has exceeded its earned time.
|
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):
|
async def pause(self):
|
||||||
"""
|
"""
|
||||||
@@ -53,10 +61,14 @@ class ActiveSubathon:
|
|||||||
async def resume(self):
|
async def resume(self):
|
||||||
if self.running:
|
if self.running:
|
||||||
raise ValueError("This subathon is already 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:
|
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)
|
return sum(row.score for row in rows)
|
||||||
|
|
||||||
def get_score_time(self, score: float) -> int:
|
def get_score_time(self, score: float) -> int:
|
||||||
@@ -111,23 +123,110 @@ class ActiveSubathon:
|
|||||||
remaining = await self.get_remaining()
|
remaining = await self.get_remaining()
|
||||||
return now + timedelta(seconds=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(
|
||||||
return await SubathonContribution.create(
|
self, profileid: int | None, score: float, event_id: int | None
|
||||||
|
) -> SubathonContribution:
|
||||||
|
row = await SubathonContribution.create(
|
||||||
subathon_id=self.subathondata.subathon_id,
|
subathon_id=self.subathondata.subathon_id,
|
||||||
profileid=profileid, score=score, event_id=event_id
|
profileid=profileid,
|
||||||
|
score=score,
|
||||||
|
event_id=event_id,
|
||||||
)
|
)
|
||||||
|
logger.info(f"Added contribution {row!r} to subathon {self.subathondata!r}")
|
||||||
|
return row
|
||||||
|
|
||||||
def fetch_contributions(self, **kwargs):
|
def fetch_contributions(self, **kwargs):
|
||||||
query = SubathonContribution.fetch_where(
|
query = SubathonContribution.fetch_where(
|
||||||
subathon_id=self.subathondata.subathon_id,
|
subathon_id=self.subathondata.subathon_id, **kwargs
|
||||||
**kwargs
|
).order_by("created_at")
|
||||||
).order_by('created_at')
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
async def get_goals(self) -> list[SubathonGoal]:
|
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
|
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:
|
class SubathonRegistry:
|
||||||
def __init__(self, data: SubathonData, profiler: ProfilesRegistry):
|
def __init__(self, data: SubathonData, profiler: ProfilesRegistry):
|
||||||
@@ -144,5 +243,3 @@ class SubathonRegistry:
|
|||||||
running = await RunningSubathon.fetch(subathondata.subathon_id)
|
running = await RunningSubathon.fetch(subathondata.subathon_id)
|
||||||
subba = ActiveSubathon(subathondata, running)
|
subba = ActiveSubathon(subathondata, running)
|
||||||
return subba
|
return subba
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user