Compare commits

..

6 Commits

Author SHA1 Message Date
11c77bcc89 tweak: Add fallback user name 2025-10-30 23:42:56 +10:00
89f9ce3ffa fix: Next and last goal logic 2025-10-30 23:21:11 +10:00
21cf65398c tweak: Add contribution logging. 2025-10-30 22:48:34 +10:00
84791e6b91 (channel): Add basic logging to event 2025-10-30 22:24:12 +10:00
7a9a47f11f fix: More typos in raw data query 2025-10-30 22:23:49 +10:00
0c5250f09e fix: Typo in join column name 2025-10-30 22:07:14 +10:00
3 changed files with 221 additions and 159 deletions

View File

@@ -1,4 +1,5 @@
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
@@ -91,12 +92,11 @@ async def prepare_subathon(
name=goal.description, name=goal.description,
) )
goalps.append(goalp) goalps.append(goalp)
if goal.required_score >= total_score: if goal.required_score <= total_score:
last_goalp = goalp last_goalp = goalp
goals_met += 1 goals_met += 1
else: elif next_goalp is None:
next_goalp = goalp next_goalp = goalp
break
# Build the last contribution information # Build the last contribution information
contribs = await subathon.fetch_contributions().limit(1) contribs = await subathon.fetch_contributions().limit(1)
@@ -106,8 +106,8 @@ async def prepare_subathon(
if contrib.profileid: if contrib.profileid:
profile = await profiler.get_profile(contrib.profileid) profile = await profiler.get_profile(contrib.profileid)
assert profile is not None assert profile is not None
name = profile.nickname name = profile.nickname or "Unknown"
user_id = None user_id = str(profile.profileid)
else: else:
name = "Anonymous" name = "Anonymous"
user_id = None user_id = None
@@ -255,3 +255,7 @@ class TimerChannel(Channel):
}, },
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)

View File

@@ -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.")

View File

@@ -126,12 +126,14 @@ class ActiveSubathon:
async def add_contribution( async def add_contribution(
self, profileid: int | None, score: float, event_id: int | None self, profileid: int | None, score: float, event_id: int | None
) -> SubathonContribution: ) -> SubathonContribution:
return await SubathonContribution.create( row = await SubathonContribution.create(
subathon_id=self.subathondata.subathon_id, subathon_id=self.subathondata.subathon_id,
profileid=profileid, profileid=profileid,
score=score, score=score,
event_id=event_id, 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(
@@ -161,48 +163,48 @@ class ActiveSubathon:
result = await ( result = await (
SubathonContribution.table.select_where( SubathonContribution.table.select_where(
subathoid=self.subathondata.subathon_id subathon_id=self.subathondata.subathon_id
) )
.join( .join(
"subscribe_events", "subscribe_events",
using=("eventid",), using=("event_id",),
join_type=JOINTYPE.INNER, join_type=JOINTYPE.INNER,
) )
.select(total=f"SUM({pointcol})") .select(total=f"SUM({pointcol})")
.with_no_adapter() .with_no_adapter()
) )
total_points += result["total"] total_points += result[0]["total"] or 0
result = await ( result = await (
SubathonContribution.table.select_where( SubathonContribution.table.select_where(
subathoid=self.subathondata.subathon_id subathon_id=self.subathondata.subathon_id
) )
.join( .join(
"subscribe_message_events", "subscribe_message_events",
using=("eventid",), using=("event_id",),
join_type=JOINTYPE.INNER, join_type=JOINTYPE.INNER,
) )
.select(total=f"SUM({pointcol})") .select(total=f"SUM({pointcol})")
.with_no_adapter() .with_no_adapter()
) )
total_points += result["total"] total_points += result[0]["total"] or 0
result = await ( result = await (
SubathonContribution.table.select_where( SubathonContribution.table.select_where(
subathoid=self.subathondata.subathon_id subathon_id=self.subathondata.subathon_id
) )
.join( .join(
"subscribe_gift_events", "gift_events",
using=("eventid",), using=("event_id",),
join_type=JOINTYPE.INNER, join_type=JOINTYPE.INNER,
) )
.select(total=f"SUM(gifted_count * ({pointcol}))") .select(total=f"SUM(gifted_count * ({pointcol}))")
.with_no_adapter() .with_no_adapter()
) )
total_points += result["total"] total_points += result[0]["total"] or 0
return total_points return total_points
@@ -213,17 +215,17 @@ class ActiveSubathon:
""" """
result = await ( result = await (
SubathonContribution.table.select_where( SubathonContribution.table.select_where(
subathoid=self.subathondata.subathon_id subathon_id=self.subathondata.subathon_id
) )
.join( .join(
"bits_events", "bits_events",
using=("eventid",), using=("event_id",),
join_type=JOINTYPE.INNER, join_type=JOINTYPE.INNER,
) )
.select(total="SUM(bits)") .select(total="SUM(bits)")
.with_no_adapter() .with_no_adapter()
) )
return result["total"] return result[0]["total"] or 0
class SubathonRegistry: class SubathonRegistry: