Compare commits
5 Commits
03d50fbe92
...
subtimer
| Author | SHA1 | Date | |
|---|---|---|---|
| df94877068 | |||
| a52dcade56 | |||
| 2c57d135c9 | |||
| 295ab69fa3 | |||
| bf835e2529 |
+4
-1
@@ -175,4 +175,7 @@ class CrocBot(commands.Bot):
|
|||||||
|
|
||||||
async def load_tokens(self, path: str | None = None):
|
async def load_tokens(self, path: str | None = None):
|
||||||
for row in await UserAuth.fetch_where():
|
for row in await UserAuth.fetch_where():
|
||||||
await self.add_token(row.token, row.refresh_token)
|
try:
|
||||||
|
await self.add_token(row.token, row.refresh_token)
|
||||||
|
except Exception:
|
||||||
|
logger.exception(f"Failed to add token for {row}")
|
||||||
|
|||||||
@@ -8,10 +8,58 @@ from twitchio.ext import commands as cmds
|
|||||||
from datamodels import BotChannel, Communities, UserProfile
|
from datamodels import BotChannel, Communities, UserProfile
|
||||||
from meta import CrocBot
|
from meta import CrocBot
|
||||||
from utils.lib import utc_now, strfdelta
|
from utils.lib import utc_now, strfdelta
|
||||||
|
from sockets import Channel, register_channel
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
from .data import SubathonData, Subathon, RunningSubathon, SubathonContribution, SubathonGoal
|
from .data import SubathonData, Subathon, RunningSubathon, SubathonContribution, SubathonGoal
|
||||||
|
|
||||||
|
|
||||||
|
class TimerChannel(Channel):
|
||||||
|
name = 'Timer'
|
||||||
|
|
||||||
|
def __init__(self, cog: 'SubathonComponent', **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.cog = cog
|
||||||
|
|
||||||
|
self.communityid = 1
|
||||||
|
|
||||||
|
async def on_connection(self, websocket, event):
|
||||||
|
await super().on_connection(websocket, event)
|
||||||
|
await self.send_set(
|
||||||
|
**await self.get_args_for(self.communityid),
|
||||||
|
websocket=websocket,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send_updates(self):
|
||||||
|
await self.send_set(
|
||||||
|
**await self.get_args_for(self.communityid),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_args_for(self, channelid):
|
||||||
|
active = await self.cog.get_active_subathon(channelid)
|
||||||
|
if active is not None:
|
||||||
|
ending = utc_now() + timedelta(seconds=await active.get_remaining())
|
||||||
|
return {
|
||||||
|
'end_at': ending,
|
||||||
|
'running': active.running
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'end_at': utc_now,
|
||||||
|
'running': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def send_set(self, end_at, running, websocket=None):
|
||||||
|
await self.send_event({
|
||||||
|
'type': "DO",
|
||||||
|
'method': 'setTimer',
|
||||||
|
'args': {
|
||||||
|
'end_at': end_at.isoformat(),
|
||||||
|
'running': running,
|
||||||
|
}
|
||||||
|
}, websocket=websocket)
|
||||||
|
|
||||||
|
|
||||||
class ActiveSubathon:
|
class ActiveSubathon:
|
||||||
def __init__(self, subathondata: Subathon, runningdata: RunningSubathon | None):
|
def __init__(self, subathondata: Subathon, runningdata: RunningSubathon | None):
|
||||||
self.subathondata = subathondata
|
self.subathondata = subathondata
|
||||||
@@ -21,6 +69,15 @@ class ActiveSubathon:
|
|||||||
def running(self):
|
def running(self):
|
||||||
return self.runningdata is not None
|
return self.runningdata is not None
|
||||||
|
|
||||||
|
async def check_cap(self):
|
||||||
|
if not (cap := self.subathondata.timecap):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
score = await self.get_score()
|
||||||
|
time_earned = self.get_score_time(score)
|
||||||
|
total_time = self.subathondata.initial_time + time_earned
|
||||||
|
return total_time >= cap
|
||||||
|
|
||||||
async def pause(self):
|
async def pause(self):
|
||||||
if not self.running:
|
if not self.running:
|
||||||
raise ValueError("This subathon is not running!")
|
raise ValueError("This subathon is not running!")
|
||||||
@@ -56,6 +113,8 @@ class ActiveSubathon:
|
|||||||
score = await self.get_score()
|
score = await self.get_score()
|
||||||
time_earned = self.get_score_time(score)
|
time_earned = self.get_score_time(score)
|
||||||
total_time = self.subathondata.initial_time + time_earned
|
total_time = self.subathondata.initial_time + time_earned
|
||||||
|
if cap := self.subathondata.timecap:
|
||||||
|
total_time = min(total_time, cap)
|
||||||
|
|
||||||
return total_time - self.get_duration()
|
return total_time - self.get_duration()
|
||||||
|
|
||||||
@@ -74,6 +133,8 @@ class SubathonComponent(cmds.Component):
|
|||||||
def __init__(self, bot: CrocBot):
|
def __init__(self, bot: CrocBot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.data = bot.dbconn.load_registry(SubathonData())
|
self.data = bot.dbconn.load_registry(SubathonData())
|
||||||
|
self.channel = TimerChannel(self)
|
||||||
|
register_channel('SubTimer', self.channel)
|
||||||
|
|
||||||
|
|
||||||
# ----- API -----
|
# ----- API -----
|
||||||
@@ -131,11 +192,18 @@ class SubathonComponent(cmds.Component):
|
|||||||
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} bit{pl}"
|
||||||
|
if not await active.check_cap():
|
||||||
|
contrib_str += f" and added {added} to the timer! Thank you holono1Heart"
|
||||||
|
else:
|
||||||
|
contrib_str += " towards our studython! Thank you holono1Heart"
|
||||||
|
|
||||||
await bits_payload.broadcaster.send_message(
|
await bits_payload.broadcaster.send_message(
|
||||||
f"{name} contributed {bits_payload.bits} bit{pl} and added {added} to the timer! Thank you <3",
|
contrib_str,
|
||||||
sender=self.bot.bot_id
|
sender=self.bot.bot_id
|
||||||
)
|
)
|
||||||
# TODO: Websocket update
|
await self.channel.send_updates()
|
||||||
await self.goalcheck(active, bits_payload.broadcaster)
|
await self.goalcheck(active, bits_payload.broadcaster)
|
||||||
# Check goals
|
# Check goals
|
||||||
|
|
||||||
@@ -170,11 +238,18 @@ class SubathonComponent(cmds.Component):
|
|||||||
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} sub{pl}"
|
||||||
|
if not await active.check_cap():
|
||||||
|
contrib_str += f" and added {added} to the timer! Thank you holono1Heart"
|
||||||
|
else:
|
||||||
|
contrib_str += " towards our studython! Thank you holono1Heart"
|
||||||
|
|
||||||
await sub_payload.broadcaster.send_message(
|
await sub_payload.broadcaster.send_message(
|
||||||
f"{name} contributed {score} sub{pl} and added {added} to the timer! Thank you <3",
|
contrib_str,
|
||||||
sender=self.bot.bot_id
|
sender=self.bot.bot_id
|
||||||
)
|
)
|
||||||
# TODO: Websocket update
|
await self.channel.send_updates()
|
||||||
# Check goals
|
# Check goals
|
||||||
await self.goalcheck(active, sub_payload.broadcaster)
|
await self.goalcheck(active, sub_payload.broadcaster)
|
||||||
|
|
||||||
@@ -205,14 +280,64 @@ class SubathonComponent(cmds.Component):
|
|||||||
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} subs"
|
||||||
|
if not await active.check_cap():
|
||||||
|
contrib_str += f" and added {added} to the timer! Thank you holono1Heart"
|
||||||
|
else:
|
||||||
|
contrib_str += " towards our studython! Thank you holono1Heart"
|
||||||
|
|
||||||
await gift_payload.broadcaster.send_message(
|
await gift_payload.broadcaster.send_message(
|
||||||
f"{name} contributed {score} subs and added {added} to the timer! Thank you <3",
|
contrib_str,
|
||||||
sender=self.bot.bot_id
|
sender=self.bot.bot_id
|
||||||
)
|
)
|
||||||
# TODO: Websocket update
|
await self.channel.send_updates()
|
||||||
# Check goals
|
# Check goals
|
||||||
await self.goalcheck(active, gift_payload.broadcaster)
|
await self.goalcheck(active, gift_payload.broadcaster)
|
||||||
|
|
||||||
|
@cmds.Component.listener()
|
||||||
|
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:
|
||||||
|
data = active.subathondata
|
||||||
|
# In an active subathon
|
||||||
|
pid = event_row['profileid']
|
||||||
|
tier = int(sub_payload.tier)
|
||||||
|
|
||||||
|
if tier == 1000:
|
||||||
|
mult = data.sub1_score
|
||||||
|
elif tier == 2000:
|
||||||
|
mult = data.sub2_score
|
||||||
|
elif tier == 3000:
|
||||||
|
mult = data.sub3_score
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown sub tier {sub_payload.tier}")
|
||||||
|
|
||||||
|
score = mult * 1
|
||||||
|
|
||||||
|
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 ''
|
||||||
|
|
||||||
|
contrib_str = f"{name} contributed {score} sub{pl}"
|
||||||
|
if not await active.check_cap():
|
||||||
|
contrib_str += f" and added {added} to the timer! Thank you holono1Heart"
|
||||||
|
else:
|
||||||
|
contrib_str += " towards our studython! Thank you holono1Heart"
|
||||||
|
|
||||||
|
await sub_payload.broadcaster.send_message(
|
||||||
|
contrib_str,
|
||||||
|
sender=self.bot.bot_id
|
||||||
|
)
|
||||||
|
await self.channel.send_updates()
|
||||||
|
# Check goals
|
||||||
|
await self.goalcheck(active, sub_payload.broadcaster)
|
||||||
|
|
||||||
# end stream => Automatically pause the timer
|
# end stream => Automatically pause the timer
|
||||||
@cmds.Component.listener()
|
@cmds.Component.listener()
|
||||||
async def event_stream_offline(self, payload: twitchio.StreamOffline):
|
async def event_stream_offline(self, payload: twitchio.StreamOffline):
|
||||||
@@ -225,11 +350,11 @@ class SubathonComponent(cmds.Component):
|
|||||||
"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
|
||||||
)
|
)
|
||||||
# TODO: Websocket update
|
await self.channel.send_updates()
|
||||||
|
|
||||||
# ----- Commands -----
|
# ----- Commands -----
|
||||||
|
|
||||||
@cmds.group(name='subathon', 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):
|
||||||
# TODO: Status
|
# TODO: Status
|
||||||
community = await self.bot.community_fetch(twitchid=ctx.broadcaster.id, name=ctx.broadcaster.name)
|
community = await self.bot.community_fetch(twitchid=ctx.broadcaster.id, name=ctx.broadcaster.name)
|
||||||
@@ -239,7 +364,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
goals = await active.get_goals()
|
goals = await active.get_goals()
|
||||||
total_goals = len(goals)
|
total_goals = len(goals)
|
||||||
donegoals = len([goal for goal in goals if score >= goal.required_score])
|
donegoals = len([goal for goal in goals if score >= goal.required_score])
|
||||||
goalstr = f"{donegoals}/{total_goals} achieved"
|
goalstr = f"{donegoals}/{total_goals} goals achieved"
|
||||||
|
|
||||||
secs = await active.get_remaining()
|
secs = await active.get_remaining()
|
||||||
remaining = strfdelta(timedelta(seconds=secs))
|
remaining = strfdelta(timedelta(seconds=secs))
|
||||||
@@ -256,7 +381,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
|
|
||||||
# subathon start
|
# subathon start
|
||||||
@group_subathon.command(name='setup')
|
@group_subathon.command(name='setup')
|
||||||
async def cmd_setup(self, ctx: cmds.Context, initial_hours: float, sub1: float, sub2: float, sub3: float, bit: float, timescore: int):
|
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):
|
||||||
if ctx.broadcaster:
|
if ctx.broadcaster:
|
||||||
community = await self.bot.community_fetch(twitchid=ctx.broadcaster.id, name=ctx.broadcaster.name)
|
community = await self.bot.community_fetch(twitchid=ctx.broadcaster.id, name=ctx.broadcaster.name)
|
||||||
cid = community.communityid
|
cid = community.communityid
|
||||||
@@ -272,10 +397,11 @@ class SubathonComponent(cmds.Component):
|
|||||||
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
|
||||||
)
|
)
|
||||||
await ctx.reply("Setup a new subathon! Use !subathon resume to get the timer running.")
|
await ctx.reply("Setup a new subathon! Use !subathon resume to get the timer running.")
|
||||||
# TODO: Websocket broadcast
|
await self.channel.send_updates()
|
||||||
|
|
||||||
# subathon stop
|
# subathon stop
|
||||||
@group_subathon.command(name='stop')
|
@group_subathon.command(name='stop')
|
||||||
@@ -286,7 +412,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 active.running:
|
if active.running:
|
||||||
await active.pause()
|
await active.pause()
|
||||||
# TODO: Websocket update
|
await self.channel.send_updates()
|
||||||
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()
|
||||||
@@ -308,7 +434,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
if active.running:
|
if active.running:
|
||||||
await active.pause()
|
await active.pause()
|
||||||
await ctx.reply("Subathon timer paused!")
|
await ctx.reply("Subathon timer paused!")
|
||||||
# TODO: Websocket update
|
await self.channel.send_updates()
|
||||||
else:
|
else:
|
||||||
await ctx.reply("Subathon timer already paused!")
|
await ctx.reply("Subathon timer already paused!")
|
||||||
else:
|
else:
|
||||||
@@ -324,7 +450,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
if not active.running:
|
if not active.running:
|
||||||
await active.resume()
|
await active.resume()
|
||||||
await ctx.reply("Subathon timer resumed!")
|
await ctx.reply("Subathon timer resumed!")
|
||||||
# TODO: Websocket update
|
await self.channel.send_updates()
|
||||||
else:
|
else:
|
||||||
await ctx.reply("Subathon timer already running!")
|
await ctx.reply("Subathon timer already running!")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class Subathon(RowModel):
|
|||||||
score_time = Integer() # Conversion factor score to seconds
|
score_time = Integer() # Conversion factor score to seconds
|
||||||
duration = Integer()
|
duration = Integer()
|
||||||
ended_at = Timestamp()
|
ended_at = Timestamp()
|
||||||
|
timecap = Integer() # Maximum subathon duration in seconds
|
||||||
|
|
||||||
class RunningSubathon(RowModel):
|
class RunningSubathon(RowModel):
|
||||||
_tablename_ = 'running_subathons'
|
_tablename_ = 'running_subathons'
|
||||||
@@ -36,6 +37,7 @@ class SubathonContribution(RowModel):
|
|||||||
profileid = Integer()
|
profileid = Integer()
|
||||||
score = Integer()
|
score = Integer()
|
||||||
event_id = Integer()
|
event_id = Integer()
|
||||||
|
# TODO: Should add a created timestamp here, since not all contributions have event ids
|
||||||
|
|
||||||
class SubathonGoal(RowModel):
|
class SubathonGoal(RowModel):
|
||||||
_tablename_ = 'subathon_goals'
|
_tablename_ = 'subathon_goals'
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ class TrackerComponent(cmds.Component):
|
|||||||
streak_months=payload.streak_months,
|
streak_months=payload.streak_months,
|
||||||
message=payload.text,
|
message=payload.text,
|
||||||
)
|
)
|
||||||
|
self.bot.safe_dispatch('subscription_message', payload=(event_row, detail_row, payload))
|
||||||
|
|
||||||
@cmds.Component.listener()
|
@cmds.Component.listener()
|
||||||
async def event_stream_online(self, payload: twitchio.StreamOnline):
|
async def event_stream_online(self, payload: twitchio.StreamOnline):
|
||||||
|
|||||||
Reference in New Issue
Block a user