feat: Add lb and adjust commands.
This commit is contained in:
@@ -6,7 +6,7 @@ VALUES ('SUBATHON', 0, 1, 'Initial Creation');
|
|||||||
|
|
||||||
CREATE TABLE subathons(
|
CREATE TABLE subathons(
|
||||||
subathon_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
subathon_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
communityid INTEGER NOT NULL REFERENCES communities(communityid),
|
communityid INTEGER NOT NULL REFERENCES communities(communityid) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
initial_time INTEGER NOT NULL,
|
initial_time INTEGER NOT NULL,
|
||||||
name TEXT,
|
name TEXT,
|
||||||
@@ -27,8 +27,8 @@ CREATE TABLE running_subathons(
|
|||||||
|
|
||||||
CREATE TABLE subathon_contributions(
|
CREATE TABLE subathon_contributions(
|
||||||
contribution_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
contribution_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
subathon_id INTEGER NOT NULL REFERENCES subathons(subathon_id),
|
subathon_id INTEGER NOT NULL REFERENCES subathons(subathon_id) ON DELETE CASCADE,
|
||||||
profileid INTEGER REFERENCES user_profiles(profileid),
|
profileid INTEGER REFERENCES user_profiles(profileid) ON DELETE SET NULL,
|
||||||
score NUMERIC NOT NULL,
|
score NUMERIC NOT NULL,
|
||||||
event_id INTEGER REFERENCES events(event_id),
|
event_id INTEGER REFERENCES events(event_id),
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
@@ -36,7 +36,7 @@ CREATE TABLE subathon_contributions(
|
|||||||
|
|
||||||
CREATE TABLE subathon_goals(
|
CREATE TABLE subathon_goals(
|
||||||
goal_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
goal_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
subathon_id INTEGER NOT NULL REFERENCES subathons(subathon_id),
|
subathon_id INTEGER NOT NULL REFERENCES subathons(subathon_id) ON DELETE CASCADE,
|
||||||
required_score NUMERIC NOT NULL,
|
required_score NUMERIC NOT NULL,
|
||||||
description TEXT NOT NULL,
|
description TEXT NOT NULL,
|
||||||
notified BOOLEAN DEFAULT false,
|
notified BOOLEAN DEFAULT false,
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import twitchio
|
|||||||
from twitchio import PartialUser, Scopes, eventsub
|
from twitchio import PartialUser, Scopes, eventsub
|
||||||
from twitchio.ext import commands as cmds
|
from twitchio.ext import commands as cmds
|
||||||
|
|
||||||
from meta import Bot
|
from data.queries import ORDER
|
||||||
|
from meta import Bot, Context
|
||||||
from meta.sockets import Channel, register_channel
|
from meta.sockets import Channel, register_channel
|
||||||
from utils.lib import utc_now, strfdelta
|
from utils.lib import utc_now, strfdelta
|
||||||
|
|
||||||
@@ -42,6 +43,11 @@ class TimerChannel(Channel):
|
|||||||
websocket=websocket,
|
websocket=websocket,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def del_connection(self, websocket):
|
||||||
|
for wss in self.communities.values():
|
||||||
|
wss.discard(websocket)
|
||||||
|
await super().del_connection(websocket)
|
||||||
|
|
||||||
async def send_updates(self, communityid: int):
|
async def send_updates(self, communityid: int):
|
||||||
args = await self.get_args_for(communityid)
|
args = await self.get_args_for(communityid)
|
||||||
for ws in self.communities[communityid]:
|
for ws in self.communities[communityid]:
|
||||||
@@ -396,85 +402,197 @@ class SubathonComponent(cmds.Component):
|
|||||||
await ctx.reply("No active subathon running!")
|
await ctx.reply("No active subathon running!")
|
||||||
|
|
||||||
# subathon start
|
# subathon start
|
||||||
@group_subathon.command(name='setup')
|
@group_subathon.command(name='setup', alias='start')
|
||||||
|
@cmds.is_broadcaster()
|
||||||
async def cmd_setup(self, ctx: cmds.Context, name: str, 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, name: str, initial_hours: float, sub1: float, sub2: float, sub3: float, bit: float, timescore: int, timecap: Optional[int]=None):
|
||||||
if ctx.broadcaster:
|
"""
|
||||||
# TODO: Usage. Maybe implement ? commands?
|
Creates a new subathon.
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
USAGE: {prefix}subathon setup <initial_hours> <sub1_points> <sub2_points> <sub3_points> <bit_points> <timepoints> [timecap]
|
||||||
cid = community.communityid
|
Arguments:
|
||||||
if (active := await self.get_active_subathon(cid)) is not None:
|
initial_hours = number of hours to start the timer with
|
||||||
await ctx.reply("There is already an active subathon running! Use !subathon stop to stop it!")
|
sub1_points = points per T1 sub
|
||||||
return
|
sub2_points = points per T2 sub
|
||||||
initial_time = initial_hours * 60 * 60
|
sub3_points = points per T3 sub
|
||||||
|
bit_points = points per bit
|
||||||
|
timepoints = seconds to be added to the timer per point
|
||||||
|
timecap (optional) = number of seconds to cap the timer at.
|
||||||
|
"""
|
||||||
|
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!")
|
||||||
|
return
|
||||||
|
initial_time = initial_hours * 60 * 60
|
||||||
|
|
||||||
active = await Subathon.create(
|
active = await Subathon.create(
|
||||||
communityid=cid,
|
communityid=cid,
|
||||||
name=name,
|
name=name,
|
||||||
initial_time=initial_time,
|
initial_time=initial_time,
|
||||||
sub1_score=sub1,
|
sub1_score=sub1,
|
||||||
sub2_score=sub2,
|
sub2_score=sub2,
|
||||||
sub3_score=sub3,
|
sub3_score=sub3,
|
||||||
bit_score=bit,
|
bit_score=bit,
|
||||||
score_time=timescore,
|
score_time=timescore,
|
||||||
timecap=timecap
|
timecap=timecap
|
||||||
)
|
)
|
||||||
timer_link = f"https://izashi.thewisewolf.dev/tracker/timer?channelid={ctx.channel.id}"
|
# TODO: Add this to config not hardcode
|
||||||
await ctx.reply(f"Setup your {name}! Use !subathon resume to get the timer running. Your timer link: {timer_link}")
|
timer_link = f"https://izashi.thewisewolf.dev/tracker/timer?channelid={ctx.channel.id}"
|
||||||
await self.channel.send_updates(cid)
|
await ctx.reply(f"Setup your {name}! Use !subathon resume to get the timer running. Your timer link: {timer_link}")
|
||||||
|
await self.channel.send_updates(cid)
|
||||||
|
|
||||||
# subathon stop
|
# subathon stop
|
||||||
@group_subathon.command(name='stop')
|
@group_subathon.command(name='stop')
|
||||||
|
@cmds.is_broadcaster()
|
||||||
async def cmd_stop(self, ctx: cmds.Context):
|
async def cmd_stop(self, ctx: cmds.Context):
|
||||||
if ctx.broadcaster:
|
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:
|
if active.running:
|
||||||
if active.running:
|
await active.pause()
|
||||||
await active.pause()
|
await self.channel.send_updates(cid)
|
||||||
await self.channel.send_updates(cid)
|
await active.subathondata.update(ended_at=utc_now())
|
||||||
await active.subathondata.update(ended_at=utc_now())
|
total = await active.get_score()
|
||||||
total = await active.get_score()
|
dursecs = active.get_duration()
|
||||||
dursecs = active.get_duration()
|
dur = strfdelta(timedelta(seconds=dursecs))
|
||||||
dur = strfdelta(timedelta(seconds=dursecs))
|
|
||||||
|
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
f"{active.name} complete after {dur} with a total of {total} points, congratulations!"
|
f"{active.name} complete after {dur} with a total of {total} points, congratulations!"
|
||||||
)
|
)
|
||||||
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()
|
||||||
async def cmd_pause(self, ctx: cmds.Context):
|
async def cmd_pause(self, ctx: cmds.Context):
|
||||||
if ctx.broadcaster or ctx.author.moderator:
|
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:
|
if active.running:
|
||||||
if active.running:
|
await active.pause()
|
||||||
await active.pause()
|
await ctx.reply(f"{active.name} timer paused!")
|
||||||
await ctx.reply(f"{active.name} timer paused!")
|
await self.channel.send_updates(cid)
|
||||||
await self.channel.send_updates(cid)
|
|
||||||
else:
|
|
||||||
await ctx.reply(f"{active.name} timer already paused!")
|
|
||||||
else:
|
else:
|
||||||
await ctx.reply("No active subathon to pause")
|
await ctx.reply(f"{active.name} timer already paused!")
|
||||||
|
else:
|
||||||
|
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()
|
||||||
async def cmd_resume(self, ctx: cmds.Context):
|
async def cmd_resume(self, ctx: cmds.Context):
|
||||||
if ctx.broadcaster or ctx.author.moderator:
|
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:
|
if not active.running:
|
||||||
if not active.running:
|
await active.resume()
|
||||||
await active.resume()
|
await ctx.reply(f"{active.name} timer resumed!")
|
||||||
await ctx.reply(f"{active.name} timer resumed!")
|
await self.channel.send_updates(cid)
|
||||||
await self.channel.send_updates(cid)
|
|
||||||
else:
|
|
||||||
await ctx.reply(f"{active.name} timer already running!")
|
|
||||||
else:
|
else:
|
||||||
await ctx.reply("No active subathon to resume")
|
await ctx.reply(f"{active.name} timer already running!")
|
||||||
|
else:
|
||||||
|
await ctx.reply("No active subathon to resume")
|
||||||
|
|
||||||
|
# subathon adjust
|
||||||
|
@group_subathon.command(name='adjust')
|
||||||
|
@cmds.is_moderator()
|
||||||
|
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.
|
||||||
|
USAGE:
|
||||||
|
{prefix}subathon adjust <amount> [@user]
|
||||||
|
Arguments:
|
||||||
|
'amount' is an integer number of points to adjust by.
|
||||||
|
It may be negative to remove points.
|
||||||
|
'@user' is an optional user name or mention to adjust for.
|
||||||
|
Examples:
|
||||||
|
'{prefix}subathon adjust 10'
|
||||||
|
'{prefix}subathon adjust -10 @machinestalkerwolfie'
|
||||||
|
"""
|
||||||
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
|
cid = community.communityid
|
||||||
|
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'
|
||||||
|
pid = profile.profileid
|
||||||
|
else:
|
||||||
|
profile = None
|
||||||
|
name = None
|
||||||
|
pid = None
|
||||||
|
await active.add_contribution(pid, amount, None)
|
||||||
|
|
||||||
|
# Build message
|
||||||
|
if amount > 0:
|
||||||
|
amountstr = f"Added {amount} point(s) to the timer"
|
||||||
|
elif amount < 0:
|
||||||
|
amountstr = f"Removed {amount} point(s) from the timer"
|
||||||
|
else:
|
||||||
|
amountstr = "Did nothing to the timer"
|
||||||
|
|
||||||
|
namestr = f"on behalf of {name}" if profile else ""
|
||||||
|
message = f"{amountstr} {namestr}"
|
||||||
|
await ctx.reply(message)
|
||||||
|
else:
|
||||||
|
await ctx.reply("No active subathon to adjust")
|
||||||
|
|
||||||
|
@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.
|
||||||
|
Anonymous contributions are gathered as one user.
|
||||||
|
"""
|
||||||
|
# TODO: Might want to extend stats to select/show last subathon as well
|
||||||
|
# IDEA: We could also offer full export as csv via web
|
||||||
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
|
cid = community.communityid
|
||||||
|
caller_profile = await self.bot.profiles.fetch_profile(ctx.chatter)
|
||||||
|
caller_pid = caller_profile.profileid
|
||||||
|
|
||||||
|
if (active := await self.get_active_subathon(cid)) is not None:
|
||||||
|
# Get totals for all contributors
|
||||||
|
query = self.data.subathon_contributions.select_where(subathonid=active.subathondata.subathon_id)
|
||||||
|
query.join('user_profiles', using=('profileid',))
|
||||||
|
query.select('profileid', total="SUM(score)")
|
||||||
|
query.order_by('total', direction=ORDER.DESC)
|
||||||
|
query.with_no_adapter()
|
||||||
|
results = await query
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
caller_idx = None
|
||||||
|
for i, row in enumerate(results):
|
||||||
|
if pid := row['profileid']:
|
||||||
|
profile = await self.bot.profiles.profiles.get_profile(pid)
|
||||||
|
name = (profile.nickname if profile else None) or 'Unknown'
|
||||||
|
if pid == caller_pid:
|
||||||
|
caller_idx = i
|
||||||
|
else:
|
||||||
|
name = 'Anonymous'
|
||||||
|
score = row['total']
|
||||||
|
part = f"{name}: {score} points"
|
||||||
|
parts.append(part)
|
||||||
|
|
||||||
|
header = ""
|
||||||
|
leaderboard = ', '.join(parts[:10])
|
||||||
|
footer = ""
|
||||||
|
if len(parts) > 10:
|
||||||
|
header = f"{active.name} top 10 leaderboard: "
|
||||||
|
leaderboard = ', '.join(parts[:10])
|
||||||
|
if caller_idx is not None and caller_idx >= 10:
|
||||||
|
caller_part = parts[caller_idx]
|
||||||
|
footer = f" ... {caller_part}"
|
||||||
|
elif parts:
|
||||||
|
header = f"{active.name} contribution leaderboard: "
|
||||||
|
else:
|
||||||
|
header = "No contributions to show yet!"
|
||||||
|
|
||||||
|
message = f"{header} {leaderboard} {footer}"
|
||||||
|
await ctx.reply(message)
|
||||||
|
else:
|
||||||
|
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):
|
async def group_goals(self, ctx: cmds.Context):
|
||||||
# List the goals
|
# List the goals
|
||||||
@@ -496,18 +614,17 @@ class SubathonComponent(cmds.Component):
|
|||||||
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()
|
||||||
async def cmd_add(self, ctx: cmds.Context, required: int, *, description: str):
|
async def cmd_add(self, ctx: cmds.Context, required: int, *, description: str):
|
||||||
if ctx.broadcaster or ctx.author.moderator:
|
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 SubathonGoal.create(
|
||||||
await SubathonGoal.create(
|
subathon_id=active.subathondata.subathon_id,
|
||||||
subathon_id=active.subathondata.subathon_id,
|
required_score=required,
|
||||||
required_score=required,
|
description=description,
|
||||||
description=description,
|
)
|
||||||
)
|
await ctx.reply("Goal added!")
|
||||||
await ctx.reply("Goal added!")
|
else:
|
||||||
else:
|
await ctx.reply("No active subathon to add goal to!")
|
||||||
await ctx.reply("No active subathon to add goal to!")
|
|
||||||
|
|
||||||
# TODO:
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
from data import Registry, RowModel, Table
|
from data import Registry, RowModel, Table
|
||||||
from data.columns import String, Timestamp, Integer, Bool
|
from data.columns import String, Timestamp, Integer, Bool
|
||||||
|
|
||||||
# User contributed {} subs and added {}:{} to the timer! Thank you :love:
|
|
||||||
# We have reached goal #5: Karaoke !! Thank you everyone for your support <3
|
|
||||||
|
|
||||||
class Subathon(RowModel):
|
class Subathon(RowModel):
|
||||||
_tablename_ = 'subathons'
|
_tablename_ = 'subathons'
|
||||||
|
|||||||
Reference in New Issue
Block a user