|
|
|
|
@@ -1,3 +1,4 @@
|
|
|
|
|
from collections import defaultdict
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
from typing import Optional
|
|
|
|
|
import random
|
|
|
|
|
@@ -5,50 +6,55 @@ import twitchio
|
|
|
|
|
from twitchio import PartialUser, Scopes, eventsub
|
|
|
|
|
from twitchio.ext import commands as cmds
|
|
|
|
|
|
|
|
|
|
from datamodels import BotChannel, Communities, UserProfile
|
|
|
|
|
from meta import CrocBot
|
|
|
|
|
from meta import Bot
|
|
|
|
|
from meta.sockets import Channel, register_channel
|
|
|
|
|
from utils.lib import utc_now, strfdelta
|
|
|
|
|
from sockets import Channel, register_channel
|
|
|
|
|
|
|
|
|
|
from . import logger
|
|
|
|
|
from .data import SubathonData, Subathon, RunningSubathon, SubathonContribution, SubathonGoal
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TimerChannel(Channel):
|
|
|
|
|
# TODO: Replace the channel mechanism?
|
|
|
|
|
# Or at least allow the subscriber to select the communityid on connection
|
|
|
|
|
# Eventually want to replace with a mechanism where clients subscribe to
|
|
|
|
|
# scoped events (e.g. just subtimer/channel)
|
|
|
|
|
# This subscription can be handled by a dedicated local client which has the registry
|
|
|
|
|
# Or the registry itself
|
|
|
|
|
# And then update hooks send the subscribers the information as a well-defined payload.
|
|
|
|
|
# Subscribers might want to communicate as well..
|
|
|
|
|
# Each module can have a tiny client that handles it? A bit like this channel...
|
|
|
|
|
name = 'Timer'
|
|
|
|
|
name = 'SubTimer'
|
|
|
|
|
|
|
|
|
|
def __init__(self, cog: 'SubathonComponent', **kwargs):
|
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
self.cog = cog
|
|
|
|
|
|
|
|
|
|
# TODO: ...
|
|
|
|
|
self.communityid = 1
|
|
|
|
|
# This is horribly inefficient but will be deprecated by the web update
|
|
|
|
|
self.communities = defaultdict(set) # Map of communityid -> listening websockets
|
|
|
|
|
|
|
|
|
|
async def on_connection(self, websocket, event):
|
|
|
|
|
# TODO: Properly this should be communityid
|
|
|
|
|
# Which is retrieved via API call to the profiles module for the channel
|
|
|
|
|
# This should also be a different error so we can pass it back to the client.
|
|
|
|
|
if not event.get('channel', None):
|
|
|
|
|
raise ValueError("Subtimer connection missing channel!")
|
|
|
|
|
community = await self.cog.bot.profiles.profiles.get_community_twitch(event['channel'])
|
|
|
|
|
if community is None:
|
|
|
|
|
raise ValueError('Requested channel is not registered. Add the bot first.')
|
|
|
|
|
|
|
|
|
|
await super().on_connection(websocket, event)
|
|
|
|
|
self.communities[community.communityid].add(websocket)
|
|
|
|
|
|
|
|
|
|
await self.send_set(
|
|
|
|
|
**await self.get_args_for(self.communityid),
|
|
|
|
|
**await self.get_args_for(community.communityid),
|
|
|
|
|
websocket=websocket,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def send_updates(self):
|
|
|
|
|
await self.send_set(
|
|
|
|
|
**await self.get_args_for(self.communityid),
|
|
|
|
|
)
|
|
|
|
|
async def send_updates(self, communityid: int):
|
|
|
|
|
args = await self.get_args_for(communityid)
|
|
|
|
|
for ws in self.communities[communityid]:
|
|
|
|
|
await self.send_set(
|
|
|
|
|
**args,
|
|
|
|
|
websocket=ws
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def get_args_for(self, channelid):
|
|
|
|
|
active = await self.cog.get_active_subathon(channelid)
|
|
|
|
|
async def get_args_for(self, communityid: int):
|
|
|
|
|
active = await self.cog.get_active_subathon(communityid)
|
|
|
|
|
if active is not None:
|
|
|
|
|
ending = utc_now() + timedelta(seconds=await active.get_remaining())
|
|
|
|
|
# TODO: Next goal info and overall goal progress, maybe last contrib
|
|
|
|
|
return {
|
|
|
|
|
'end_at': ending,
|
|
|
|
|
'running': active.running
|
|
|
|
|
@@ -71,8 +77,6 @@ class TimerChannel(Channel):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ActiveSubathon:
|
|
|
|
|
# TODO: Version check
|
|
|
|
|
# Relies on event tracker and profiles module as well.
|
|
|
|
|
def __init__(self, subathondata: Subathon, runningdata: RunningSubathon | None):
|
|
|
|
|
self.subathondata = subathondata
|
|
|
|
|
self.runningdata = runningdata
|
|
|
|
|
@@ -143,28 +147,24 @@ class ActiveSubathon:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SubathonComponent(cmds.Component):
|
|
|
|
|
def __init__(self, bot: CrocBot):
|
|
|
|
|
# TODO: Add explicit dependencies and version checks
|
|
|
|
|
# for profile and event tracker modules
|
|
|
|
|
|
|
|
|
|
def __init__(self, bot: Bot):
|
|
|
|
|
self.bot = bot
|
|
|
|
|
self.data = bot.dbconn.load_registry(SubathonData())
|
|
|
|
|
self.channel = TimerChannel(self)
|
|
|
|
|
register_channel('SubTimer', self.channel)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ----- API -----
|
|
|
|
|
async def component_load(self):
|
|
|
|
|
# TODO: Setup the websocket
|
|
|
|
|
await self.data.init()
|
|
|
|
|
await self.bot.version_check(*self.data.VERSION)
|
|
|
|
|
|
|
|
|
|
async def component_teardown(self):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# ----- Methods -----
|
|
|
|
|
async def get_community(self, twitchid: str, name: str | None) -> Communities:
|
|
|
|
|
return await self.bot.community_fetch(twitchid=twitchid, name=name)
|
|
|
|
|
|
|
|
|
|
async def get_profile(self, twitchid: str, name: str | None) -> UserProfile:
|
|
|
|
|
return await self.bot.profile_fetch(twitchid=twitchid, name=name)
|
|
|
|
|
|
|
|
|
|
async def get_active_subathon(self, communityid: int) -> ActiveSubathon | None:
|
|
|
|
|
rows = await Subathon.fetch_where(communityid=communityid, ended_at=None)
|
|
|
|
|
if rows:
|
|
|
|
|
@@ -208,15 +208,15 @@ class SubathonComponent(cmds.Component):
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
contrib_str += f" and added {added} to the timer! Thank you <3"
|
|
|
|
|
else:
|
|
|
|
|
contrib_str += " towards our studython! Thank you holono1Heart"
|
|
|
|
|
contrib_str += " towards our subathon! Thank you <3"
|
|
|
|
|
|
|
|
|
|
await bits_payload.broadcaster.send_message(
|
|
|
|
|
contrib_str,
|
|
|
|
|
sender=self.bot.bot_id
|
|
|
|
|
)
|
|
|
|
|
await self.channel.send_updates()
|
|
|
|
|
await self.channel.send_updates(event_row['communityid'])
|
|
|
|
|
await self.goalcheck(active, bits_payload.broadcaster)
|
|
|
|
|
# Check goals
|
|
|
|
|
|
|
|
|
|
@@ -254,15 +254,15 @@ class SubathonComponent(cmds.Component):
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
contrib_str += f" and added {added} to the timer! Thank you <3"
|
|
|
|
|
else:
|
|
|
|
|
contrib_str += " towards our studython! Thank you holono1Heart"
|
|
|
|
|
contrib_str += " towards our subathon! Thank you <3"
|
|
|
|
|
|
|
|
|
|
await sub_payload.broadcaster.send_message(
|
|
|
|
|
contrib_str,
|
|
|
|
|
sender=self.bot.bot_id
|
|
|
|
|
)
|
|
|
|
|
await self.channel.send_updates()
|
|
|
|
|
await self.channel.send_updates(event_row['communityid'])
|
|
|
|
|
# Check goals
|
|
|
|
|
await self.goalcheck(active, sub_payload.broadcaster)
|
|
|
|
|
|
|
|
|
|
@@ -296,15 +296,15 @@ class SubathonComponent(cmds.Component):
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
contrib_str += f" and added {added} to the timer! Thank you <3"
|
|
|
|
|
else:
|
|
|
|
|
contrib_str += " towards our studython! Thank you holono1Heart"
|
|
|
|
|
contrib_str += " towards our subathon! Thank you <3"
|
|
|
|
|
|
|
|
|
|
await gift_payload.broadcaster.send_message(
|
|
|
|
|
contrib_str,
|
|
|
|
|
sender=self.bot.bot_id
|
|
|
|
|
)
|
|
|
|
|
await self.channel.send_updates()
|
|
|
|
|
await self.channel.send_updates(event_row['communityid'])
|
|
|
|
|
# Check goals
|
|
|
|
|
await self.goalcheck(active, gift_payload.broadcaster)
|
|
|
|
|
|
|
|
|
|
@@ -339,22 +339,22 @@ class SubathonComponent(cmds.Component):
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
contrib_str += f" and added {added} to the timer! Thank you <3"
|
|
|
|
|
else:
|
|
|
|
|
contrib_str += " towards our studython! Thank you holono1Heart"
|
|
|
|
|
contrib_str += " towards our subathon! Thank you <3"
|
|
|
|
|
|
|
|
|
|
await sub_payload.broadcaster.send_message(
|
|
|
|
|
contrib_str,
|
|
|
|
|
sender=self.bot.bot_id
|
|
|
|
|
)
|
|
|
|
|
await self.channel.send_updates()
|
|
|
|
|
await self.channel.send_updates(event_row['communityid'])
|
|
|
|
|
# Check goals
|
|
|
|
|
await self.goalcheck(active, sub_payload.broadcaster)
|
|
|
|
|
|
|
|
|
|
# end stream => Automatically pause the timer
|
|
|
|
|
@cmds.Component.listener()
|
|
|
|
|
async def event_stream_offline(self, payload: twitchio.StreamOffline):
|
|
|
|
|
community = await self.bot.community_fetch(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
|
|
|
|
|
community = await self.bot.profiles.fetch_community(payload.broadcaster)
|
|
|
|
|
cid = community.communityid
|
|
|
|
|
if (active := await self.get_active_subathon(cid)) is not None:
|
|
|
|
|
if active.running:
|
|
|
|
|
@@ -363,13 +363,13 @@ class SubathonComponent(cmds.Component):
|
|
|
|
|
"Paused the subathon timer because the stream went offline!",
|
|
|
|
|
sender=self.bot.bot_id
|
|
|
|
|
)
|
|
|
|
|
await self.channel.send_updates()
|
|
|
|
|
await self.channel.send_updates(cid)
|
|
|
|
|
|
|
|
|
|
# ----- Commands -----
|
|
|
|
|
|
|
|
|
|
@cmds.group(name='subathon', aliases=['studython'], invoke_fallback=True)
|
|
|
|
|
async def group_subathon(self, ctx: cmds.Context):
|
|
|
|
|
community = await self.bot.community_fetch(twitchid=ctx.broadcaster.id, name=ctx.broadcaster.name)
|
|
|
|
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
|
|
|
|
cid = community.communityid
|
|
|
|
|
if (active := await self.get_active_subathon(cid)) is not None:
|
|
|
|
|
score = await active.get_score()
|
|
|
|
|
@@ -396,7 +396,7 @@ class SubathonComponent(cmds.Component):
|
|
|
|
|
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:
|
|
|
|
|
# TODO: Usage. Maybe implement ? commands?
|
|
|
|
|
community = await self.bot.community_fetch(twitchid=ctx.broadcaster.id, name=ctx.broadcaster.name)
|
|
|
|
|
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!")
|
|
|
|
|
@@ -414,18 +414,18 @@ class SubathonComponent(cmds.Component):
|
|
|
|
|
timecap=timecap
|
|
|
|
|
)
|
|
|
|
|
await ctx.reply("Setup a new subathon! Use !subathon resume to get the timer running.")
|
|
|
|
|
await self.channel.send_updates()
|
|
|
|
|
await self.channel.send_updates(cid)
|
|
|
|
|
|
|
|
|
|
# subathon stop
|
|
|
|
|
@group_subathon.command(name='stop')
|
|
|
|
|
async def cmd_stop(self, ctx: cmds.Context):
|
|
|
|
|
if ctx.broadcaster:
|
|
|
|
|
community = await self.bot.community_fetch(twitchid=ctx.broadcaster.id, name=ctx.broadcaster.name)
|
|
|
|
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
|
|
|
|
cid = community.communityid
|
|
|
|
|
if (active := await self.get_active_subathon(cid)) is not None:
|
|
|
|
|
if active.running:
|
|
|
|
|
await active.pause()
|
|
|
|
|
await self.channel.send_updates()
|
|
|
|
|
await self.channel.send_updates(cid)
|
|
|
|
|
await active.subathondata.update(ended_at=utc_now())
|
|
|
|
|
total = await active.get_score()
|
|
|
|
|
dursecs = active.get_duration()
|
|
|
|
|
@@ -441,13 +441,13 @@ class SubathonComponent(cmds.Component):
|
|
|
|
|
@group_subathon.command(name='pause')
|
|
|
|
|
async def cmd_pause(self, ctx: cmds.Context):
|
|
|
|
|
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.profiles.fetch_community(ctx.broadcaster)
|
|
|
|
|
cid = community.communityid
|
|
|
|
|
if (active := await self.get_active_subathon(cid)) is not None:
|
|
|
|
|
if active.running:
|
|
|
|
|
await active.pause()
|
|
|
|
|
await ctx.reply("Subathon timer paused!")
|
|
|
|
|
await self.channel.send_updates()
|
|
|
|
|
await self.channel.send_updates(cid)
|
|
|
|
|
else:
|
|
|
|
|
await ctx.reply("Subathon timer already paused!")
|
|
|
|
|
else:
|
|
|
|
|
@@ -457,13 +457,13 @@ class SubathonComponent(cmds.Component):
|
|
|
|
|
@group_subathon.command(name='resume')
|
|
|
|
|
async def cmd_resume(self, ctx: cmds.Context):
|
|
|
|
|
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.profiles.fetch_community(ctx.broadcaster)
|
|
|
|
|
cid = community.communityid
|
|
|
|
|
if (active := await self.get_active_subathon(cid)) is not None:
|
|
|
|
|
if not active.running:
|
|
|
|
|
await active.resume()
|
|
|
|
|
await ctx.reply("Subathon timer resumed!")
|
|
|
|
|
await self.channel.send_updates()
|
|
|
|
|
await self.channel.send_updates(cid)
|
|
|
|
|
else:
|
|
|
|
|
await ctx.reply("Subathon timer already running!")
|
|
|
|
|
else:
|
|
|
|
|
@@ -472,7 +472,7 @@ class SubathonComponent(cmds.Component):
|
|
|
|
|
@cmds.group(name='goals', invoke_fallback=True)
|
|
|
|
|
async def group_goals(self, ctx: cmds.Context):
|
|
|
|
|
# List the goals
|
|
|
|
|
community = await self.bot.community_fetch(twitchid=ctx.broadcaster.id, name=ctx.broadcaster.name)
|
|
|
|
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
|
|
|
|
cid = community.communityid
|
|
|
|
|
if (active := await self.get_active_subathon(cid)) is not None:
|
|
|
|
|
goals = await active.get_goals()
|
|
|
|
|
@@ -492,7 +492,7 @@ class SubathonComponent(cmds.Component):
|
|
|
|
|
@group_goals.command(name='add')
|
|
|
|
|
async def cmd_add(self, ctx: cmds.Context, required: int, *, description: str):
|
|
|
|
|
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.profiles.fetch_community(ctx.broadcaster)
|
|
|
|
|
cid = community.communityid
|
|
|
|
|
if (active := await self.get_active_subathon(cid)) is not None:
|
|
|
|
|
await SubathonGoal.create(
|
|
|
|
|
@@ -502,5 +502,4 @@ class SubathonComponent(cmds.Component):
|
|
|
|
|
)
|
|
|
|
|
await ctx.reply("Goal added!")
|
|
|
|
|
else:
|
|
|
|
|
await ctx.reply("No active subathon to goal!")
|
|
|
|
|
|
|
|
|
|
await ctx.reply("No active subathon to add goal to!")
|
|
|
|
|
|