Compare commits

..

3 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
3 changed files with 203 additions and 146 deletions

View File

@@ -92,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)
@@ -107,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

View File

@@ -13,24 +13,30 @@ 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
class SubathonComponent(cmds.Component): class SubathonComponent(cmds.Component):
""" """
!subathon !subathon
!subathon setup <initial_hours> <t1score> <t2score> <t3score> <bitscore> <timescore> [cap] !subathon setup <initial_hours> <t1score> <t2score> <t3score> <bitscore> <timescore> [cap]
!subathon stop !subathon stop
!subathon link !subathon link
!subathon pause !subathon pause
!subathon resume !subathon resume
!subathon adjust <amount> [@user] !subathon adjust <amount> [@user]
!subathon config !subathon config
!subathon config name [new name] !subathon config name [new name]
!subathon config cap [new cap] !subathon config cap [new cap]
!subathon config scores [<t1score> <t2score> <t3score> <bitscore>] !subathon config scores [<t1score> <t2score> <t3score> <bitscore>]
@@ -38,17 +44,18 @@ class SubathonComponent(cmds.Component):
!subathon lb !subathon lb
!goals !goals
!goals remaining !goals remaining
!goals add <points> <description> !goals add <points> <description>
!goals remove <points> !goals remove <points>
Command: Command:
Permissions: Permissions:
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):
@@ -89,8 +96,8 @@ class SubathonComponent(cmds.Component):
# Should probably wait on a central messaging slow-lock bucketed by channel instead # Should probably wait on a central messaging slow-lock bucketed by channel instead
await asyncio.sleep(1) await asyncio.sleep(1)
await channel.send_message( await channel.send_message(
f"We have reached Goal #{i+1}: {goal.description} !! Thank you everyone for your support <3", f"We have reached Goal #{i + 1}: {goal.description} !! Thank you everyone for your support <3",
sender=self.bot.bot_id, sender=self.bot.bot_id,
) )
await goal.update(notified=True) await goal.update(notified=True)
@@ -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,16 +179,18 @@ 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:
mult = data.sub1_score mult = data.sub1_score
elif tier == 2000: elif tier == 2000:
mult = data.sub2_score mult = data.sub2_score
elif tier == 3000: elif tier == 3000:
mult = data.sub3_score mult = data.sub3_score
else: else:
@@ -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,16 +226,18 @@ 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:
mult = data.sub1_score mult = data.sub1_score
elif tier == 2000: elif tier == 2000:
mult = data.sub2_score mult = data.sub2_score
elif tier == 3000: elif tier == 3000:
mult = data.sub3_score mult = data.sub3_score
else: else:
@@ -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,16 +272,18 @@ 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:
mult = data.sub1_score mult = data.sub1_score
elif tier == 2000: elif tier == 2000:
mult = data.sub2_score mult = data.sub2_score
elif tier == 3000: elif tier == 3000:
mult = data.sub3_score mult = data.sub3_score
else: else:
@@ -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,31 +353,37 @@ 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:
await ctx.reply("No active subathon running!") await ctx.reply("No active subathon running!")
# 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]
Arguments: Arguments:
initial_hours = number of hours to start the timer with initial_hours = number of hours to start the timer with
sub1_points = points per T1 sub sub1_points = points per T1 sub
sub2_points = points per T2 sub sub2_points = points per T2 sub
sub3_points = points per T3 sub sub3_points = points per T3 sub
bit_points = points per bit bit_points = points per bit
timepoints = seconds to be added to the timer per point timepoints = seconds to be added to the timer per point
timecap (optional) = number of hours to cap the timer at timecap (optional) = number of hours to cap the timer at
@@ -357,11 +391,13 @@ 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
subdata = await Subathon.create( subdata = await Subathon.create(
name="Subathon", name="Subathon",
communityid=cid, communityid=cid,
@@ -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)
@@ -436,8 +470,8 @@ class SubathonComponent(cmds.Component):
else: else:
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,8 +512,8 @@ 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
name = None name = 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,24 +644,30 @@ 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(
return "Provided timer cap must be an integer number of hours!"
)
return
new_cap = int(args) new_cap = int(args)
if new_cap <= 0: if new_cap <= 0:
# 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:
@@ -667,10 +711,13 @@ class SubathonComponent(cmds.Component):
await self.dispatch_update(active) await self.dispatch_update(active)
await ctx.reply("Successfully updated subathon score table.") await ctx.reply("Successfully updated subathon score table.")
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:
@@ -693,7 +742,7 @@ class SubathonComponent(cmds.Component):
# Setting path # Setting path
if not args.isdigit(): if not args.isdigit():
await ctx.reply("Time score (seconds per point) must be an integer.") await ctx.reply("Time score (seconds per point) must be an integer.")
return return
await active.subathondata.update(score_time=int(args)) await active.subathondata.update(score_time=int(args))
await self.dispatch_update(active) await self.dispatch_update(active)
await ctx.reply("Subathon time score updated (NOTE: This is retroactive).") await ctx.reply("Subathon time score updated (NOTE: This is retroactive).")
@@ -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)
@@ -822,8 +879,8 @@ class SubathonComponent(cmds.Component):
else: else:
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):
""" """
@@ -831,7 +888,7 @@ class SubathonComponent(cmds.Component):
Remove any goal(s) set at the given score. Remove any goal(s) set at the given score.
""" """
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:
results = await SubathonGoal.table.delete_where( results = await SubathonGoal.table.delete_where(
subathon_id=active.subathondata.subathon_id, subathon_id=active.subathondata.subathon_id,
@@ -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(