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
import json
from collections import defaultdict
from datetime import datetime, timedelta
@@ -91,12 +92,11 @@ async def prepare_subathon(
name=goal.description,
)
goalps.append(goalp)
if goal.required_score >= total_score:
if goal.required_score <= total_score:
last_goalp = goalp
goals_met += 1
else:
elif next_goalp is None:
next_goalp = goalp
break
# Build the last contribution information
contribs = await subathon.fetch_contributions().limit(1)
@@ -106,8 +106,8 @@ async def prepare_subathon(
if contrib.profileid:
profile = await profiler.get_profile(contrib.profileid)
assert profile is not None
name = profile.nickname
user_id = None
name = profile.nickname or "Unknown"
user_id = str(profile.profileid)
else:
name = "Anonymous"
user_id = None
@@ -255,3 +255,7 @@ class TimerChannel(Channel):
},
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 . import logger
from .data import SubathonData, Subathon, RunningSubathon, SubathonContribution, SubathonGoal
from .data import (
SubathonData,
Subathon,
RunningSubathon,
SubathonContribution,
SubathonGoal,
)
from .subathon import ActiveSubathon, SubathonRegistry
from .channel import SubathonPayload, prepare_subathon, TimerChannel
@@ -49,6 +55,7 @@ class SubathonComponent(cmds.Component):
Description:
Examples:
"""
# TODO: Add explicit dependencies and version checks
# for profile and event tracker modules
@@ -58,7 +65,7 @@ class SubathonComponent(cmds.Component):
self.subathons = SubathonRegistry(self.data, bot.profiles.profiles)
self.channel = TimerChannel(self.bot.profiles.profiles, self.subathons)
register_channel('SubTimer', self.channel)
register_channel("SubTimer", self.channel)
# ----- API -----
async def component_load(self):
@@ -89,7 +96,7 @@ class SubathonComponent(cmds.Component):
# Should probably wait on a central messaging slow-lock bucketed by channel instead
await asyncio.sleep(1)
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,
)
await goal.update(notified=True)
@@ -98,20 +105,24 @@ class SubathonComponent(cmds.Component):
# Blerp handler
@cmds.Component.listener()
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
if 'bits' not in message.text:
if "bits" not in message.text:
return
community = await self.bot.profiles.fetch_community(message.broadcaster)
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
match = re.match(r"(?P<name>\w+) used (?P<amount>\d+)", message.text)
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:
amount = int(match['amount'])
name = match['name']
amount = int(match["amount"])
name = match["name"]
# This is for giving the contribution to the calling user
# user = await self.bot.fetch_user(login=name.lower())
user = await message.chatter.user()
@@ -128,12 +139,15 @@ class SubathonComponent(cmds.Component):
@cmds.Component.listener()
async def event_safe_bits_use(self, 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
pid = event_row['profileid']
uid = event_row['user_id']
score = detail_row['bits'] * active.subathondata.bit_score
await active.add_contribution(pid, score, event_row['event_id'])
pid = event_row["profileid"]
uid = event_row["user_id"]
score = detail_row["bits"] * active.subathondata.bit_score
logger.info(f"Adding subathon contribution from bits payload {payload}")
await active.add_contribution(pid, score, event_row["event_id"])
# Send message to channel
sec = active.get_score_time(score)
@@ -143,7 +157,7 @@ class SubathonComponent(cmds.Component):
else:
added = f"{sec} seconds"
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}"
if not await active.check_cap():
@@ -152,8 +166,7 @@ class SubathonComponent(cmds.Component):
contrib_str += " towards our subathon! Thank you <3"
await bits_payload.broadcaster.send_message(
contrib_str,
sender=self.bot.bot_id
contrib_str, sender=self.bot.bot_id
)
await self.dispatch_update(active)
await self.goalcheck(active, bits_payload.broadcaster)
@@ -166,10 +179,12 @@ class SubathonComponent(cmds.Component):
# Ignore gifted here
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
# In an active subathon
pid = event_row['profileid']
pid = event_row["profileid"]
tier = int(sub_payload.tier)
if tier == 1000:
@@ -183,13 +198,16 @@ class SubathonComponent(cmds.Component):
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
added_min = int(active.get_score_time(score) // 60)
added = f"{added_min} minutes"
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}"
if not await active.check_cap():
@@ -198,8 +216,7 @@ class SubathonComponent(cmds.Component):
contrib_str += " towards our subathon! Thank you <3"
await sub_payload.broadcaster.send_message(
contrib_str,
sender=self.bot.bot_id
contrib_str, sender=self.bot.bot_id
)
await self.dispatch_update(active)
# Check goals
@@ -209,10 +226,12 @@ class SubathonComponent(cmds.Component):
async def event_safe_subscription_gift(self, 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
# In an active subathon
pid = event_row['profileid']
pid = event_row["profileid"]
tier = int(gift_payload.tier)
if tier == 1000:
@@ -226,12 +245,15 @@ class SubathonComponent(cmds.Component):
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
added_min = int(active.get_score_time(score) // 60)
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"
if not await active.check_cap():
@@ -240,8 +262,7 @@ class SubathonComponent(cmds.Component):
contrib_str += " towards our subathon! Thank you <3"
await gift_payload.broadcaster.send_message(
contrib_str,
sender=self.bot.bot_id
contrib_str, sender=self.bot.bot_id
)
await self.dispatch_update(active)
# Check goals
@@ -251,10 +272,12 @@ class SubathonComponent(cmds.Component):
async def event_safe_subscription_message(self, 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
# In an active subathon
pid = event_row['profileid']
pid = event_row["profileid"]
tier = int(sub_payload.tier)
if tier == 1000:
@@ -268,13 +291,16 @@ class SubathonComponent(cmds.Component):
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
added_min = int(active.get_score_time(score) // 60)
added = f"{added_min} minutes"
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}"
if not await active.check_cap():
@@ -283,8 +309,7 @@ class SubathonComponent(cmds.Component):
contrib_str += " towards our subathon! Thank you <3"
await sub_payload.broadcaster.send_message(
contrib_str,
sender=self.bot.bot_id
contrib_str, sender=self.bot.bot_id
)
await self.dispatch_update(active)
# Check goals
@@ -297,17 +322,20 @@ class SubathonComponent(cmds.Component):
cid = community.communityid
if (active := await self.get_active_subathon(cid)) is not None:
if active.running:
logger.info(
f"Automatically paused suabathon {active.subathondata!r} from stream offline."
)
await active.pause()
if not await active.check_finished():
await payload.broadcaster.send_message(
"Paused the subathon timer because the stream went offline!",
sender=self.bot.bot_id
sender=self.bot.bot_id,
)
await self.dispatch_update(active)
# ----- 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):
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
cid = community.communityid
@@ -325,13 +353,9 @@ class SubathonComponent(cmds.Component):
if secs > 0:
remaining = strfdelta(timedelta(seconds=secs))
text = (
f"{active.name} running for {duration}! {score} points recieved, {goalstr}, and {remaining} left on the timer"
)
text = f"{active.name} running for {duration}! {score} points recieved, {goalstr}, and {remaining} left on the timer"
else:
text = (
f"{active.name} completed after {duration} with a total of {score} points, and {goalstr}!"
)
text = f"{active.name} completed after {duration} with a total of {score} points, and {goalstr}!"
await ctx.reply(text)
else:
@@ -339,9 +363,19 @@ class SubathonComponent(cmds.Component):
# subathon start
# TODO: Usage line on command error
@group_subathon.command(name='setup', alias='start')
@group_subathon.command(name="setup", alias="start")
@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.
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)
cid = community.communityid
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
initial_time = initial_hours * 60 * 60
timecap_seconds = timecap * 60 * 60 if timecap else None
@@ -371,9 +407,9 @@ class SubathonComponent(cmds.Component):
sub3_score=sub3,
bit_score=bit,
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}"
await ctx.reply(
f"Setup your subathon! "
@@ -385,7 +421,7 @@ class SubathonComponent(cmds.Component):
await self.dispatch_update(active)
# subathon stop
@group_subathon.command(name='stop')
@group_subathon.command(name="stop")
@cmds.is_broadcaster()
async def cmd_stop(self, ctx: cmds.Context):
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.")
# subathon link
@group_subathon.command(name='link')
@group_subathon.command(name="link")
@cmds.is_moderator()
async def cmd_link(self, ctx: cmds.Context):
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
cid = community.communityid
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}"
await ctx.reply(
timer_link
)
await ctx.reply(timer_link)
else:
await ctx.reply("No active subathon to stop.")
# subathon pause
@group_subathon.command(name='pause')
@group_subathon.command(name="pause")
@cmds.is_moderator()
async def cmd_pause(self, ctx: cmds.Context):
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")
# subathon resume
@group_subathon.command(name='resume')
@group_subathon.command(name="resume")
@cmds.is_moderator()
async def cmd_resume(self, ctx: cmds.Context):
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")
# subathon adjust
@group_subathon.command(name='adjust')
@group_subathon.command(name="adjust")
@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.
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 user is not None:
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
else:
profile = None
@@ -524,7 +560,8 @@ class SubathonComponent(cmds.Component):
WARNING: This setting is retroactive and should generally not be changed.
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()
async def subathon_config_grp(self, ctx: Context, *, args: Optional[str] = None):
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
@@ -544,38 +581,35 @@ class SubathonComponent(cmds.Component):
sdata = active.subathondata
# name
parts.append(
f"name=\"{sdata.name}\""
)
parts.append(f'name="{sdata.name}"')
# timecamp
cap = sdata.timecap or 0
caph = int(cap / 3600)
parts.append(
f"cap={caph} (hours, 0 means no cap)"
)
parts.append(f"cap={caph} (hours, 0 means no cap)")
# scores
scores = map(float, (sdata.sub1_score, sdata.sub2_score, sdata.sub3_score, sdata.bit_score))
scorestr = ' '.join(map(str, scores))
parts.append(
f"scores={scorestr} (t1, t2, t3, and 1 bit point scores)"
scores = map(
float,
(sdata.sub1_score, sdata.sub2_score, sdata.sub3_score, sdata.bit_score),
)
scorestr = " ".join(map(str, scores))
parts.append(f"scores={scorestr} (t1, t2, t3, and 1 bit point scores)")
# timescore
parts.append(
f"timescore={sdata.score_time} (seconds per point)"
)
parts.append(f"timescore={sdata.score_time} (seconds per point)")
# Combine
partstr = ' ; '.join(parts)
partstr = " ; ".join(parts)
await ctx.reply(
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()
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)
cid = community.communityid
if (active := await self.get_active_subathon(cid)) is None:
@@ -586,20 +620,22 @@ class SubathonComponent(cmds.Component):
# Setting path
await active.subathondata.update(name=args)
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:
# Display path
name = active.subathondata.name
await ctx.reply(
"Name of the subathon, used whenever the subathon is mentioned. "
"Accepts any string. "
f"Currently: \"{name}\" "
f'Currently: "{name}" '
f"Example: {ctx.prefix}subathon config name Birthday Subathon"
)
@subathon_config_grp.command(name='cap')
@subathon_config_grp.command(name="cap")
@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)
cid = community.communityid
if (active := await self.get_active_subathon(cid)) is None:
@@ -608,11 +644,13 @@ class SubathonComponent(cmds.Component):
if args:
# Setting path
if args.lower() in ('none',):
args = '0'
if args.lower() in ("none",):
args = "0"
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
new_cap = int(args)
@@ -620,12 +658,16 @@ class SubathonComponent(cmds.Component):
# Unset the cap
await active.subathondata.update(timecap=None)
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:
# Set the cap
await active.subathondata.update(timecap=int(new_cap * 3600))
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:
# Display path
current_cap = active.subathondata.timecap or 0
@@ -639,9 +681,11 @@ class SubathonComponent(cmds.Component):
f"Example: {ctx.prefix}subathon config cap 24"
)
@subathon_config_grp.command(name='scores')
@subathon_config_grp.command(name="scores")
@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)
cid = community.communityid
if (active := await self.get_active_subathon(cid)) is None:
@@ -669,8 +713,11 @@ class SubathonComponent(cmds.Component):
else:
# Display path
sdata = active.subathondata
scores = map(float, (sdata.sub1_score, sdata.sub2_score, sdata.sub3_score, sdata.bit_score))
scorestr = ' '.join(map(str, scores))
scores = map(
float,
(sdata.sub1_score, sdata.sub2_score, sdata.sub3_score, sdata.bit_score),
)
scorestr = " ".join(map(str, scores))
await ctx.reply(
"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"
)
@subathon_config_grp.command(name='timescore')
@subathon_config_grp.command(name="timescore")
@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)
cid = community.communityid
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"
)
@group_subathon.command(name='leaderboard', aliases=('top', 'lb',))
@group_subathon.command(
name="leaderboard",
aliases=(
"top",
"lb",
),
)
async def cmd_subathon_lb(self, ctx: Context):
"""
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:
# Get totals for all contributors
query = self.data.subathon_contributions.select_where(subathon_id=active.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 = self.data.subathon_contributions.select_where(
subathon_id=active.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
parts = []
caller_idx = None
for i, row in enumerate(results):
if pid := row['profileid']:
if pid := row["profileid"]:
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:
caller_idx = i
else:
name = 'Anonymous'
score = row['total']
name = "Anonymous"
score = row["total"]
part = f"{name}: {score} points"
parts.append(part)
header = ""
leaderboard = ', '.join(parts[:10])
leaderboard = ", ".join(parts[:10])
footer = ""
if len(parts) > 10:
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:
caller_part = parts[caller_idx]
footer = f" ... {caller_part}"
@@ -764,7 +821,7 @@ class SubathonComponent(cmds.Component):
await ctx.reply("No active subathon to show leaderboard of!")
# Subathon goals
@cmds.group(name='goals', invoke_fallback=True)
@cmds.group(name="goals", invoke_fallback=True)
async def group_goals(self, ctx: cmds.Context):
# List the goals
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
@@ -777,14 +834,14 @@ class SubathonComponent(cmds.Component):
goalstrs.append(line)
if goals:
text = ', '.join(goalstrs)
text = ", ".join(goalstrs)
await ctx.reply(f"{active.name} Goals! -- {text}")
else:
await ctx.reply("No goals have been configured!")
else:
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):
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
cid = community.communityid
@@ -798,7 +855,7 @@ class SubathonComponent(cmds.Component):
goalstrs.append(line)
if goalstrs:
text = ', '.join(goalstrs)
text = ", ".join(goalstrs)
await ctx.reply(f"{active.name} Goals Remaining -- {text}")
elif goals:
await ctx.reply("All goals completed, congratulations!")
@@ -807,7 +864,7 @@ class SubathonComponent(cmds.Component):
else:
await ctx.reply("No active subathon running!")
@group_goals.command(name='add')
@group_goals.command(name="add")
@cmds.is_moderator()
async def cmd_add(self, ctx: cmds.Context, required: int, *, description: str):
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!")
# remove
@group_goals.command(name='remove', aliases=['del', 'delete', 'rm'])
@group_goals.command(name="remove", aliases=["del", "delete", "rm"])
@cmds.is_moderator()
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!")
else:
await ctx.reply(f"No goal set at {required} score to remove.")

View File

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