|
|
|
@@ -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,12 +192,19 @@ 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()
|
|
|
|
await self.goalcheck(active, bits_payload.broadcaster)
|
|
|
|
# Check goals
|
|
|
|
# Check goals
|
|
|
|
|
|
|
|
|
|
|
|
@cmds.Component.listener()
|
|
|
|
@cmds.Component.listener()
|
|
|
|
@@ -170,13 +238,20 @@ 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()
|
|
|
|
await self.goalcheck(active, sub_payload.broadcaster)
|
|
|
|
|
|
|
|
|
|
|
|
@cmds.Component.listener()
|
|
|
|
@cmds.Component.listener()
|
|
|
|
async def event_safe_subscription_gift(self, payload):
|
|
|
|
async def event_safe_subscription_gift(self, payload):
|
|
|
|
@@ -205,13 +280,63 @@ 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()
|
|
|
|
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()
|
|
|
|
@@ -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))
|
|
|
|
@@ -248,7 +373,7 @@ class SubathonComponent(cmds.Component):
|
|
|
|
duration = strfdelta(timedelta(seconds=secs))
|
|
|
|
duration = strfdelta(timedelta(seconds=secs))
|
|
|
|
|
|
|
|
|
|
|
|
text = (
|
|
|
|
text = (
|
|
|
|
f"Subathon running for {duration}! {score} (equivalent) subscriptions recieved, {goals}, and {remaining} left on the timer"
|
|
|
|
f"Subathon running for {duration}! {score} (equivalent) subscriptions recieved, {goalstr}, and {remaining} left on the timer"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
await ctx.reply(text)
|
|
|
|
await ctx.reply(text)
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
@@ -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:
|
|
|
|
@@ -351,7 +477,7 @@ 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')
|
|
|
|
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:
|
|
|
|
if ctx.broadcaster or ctx.author.moderator:
|
|
|
|
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
|
|
|
|
|