Migrate to new plugin architecture.

This commit is contained in:
2025-09-01 23:24:38 +10:00
parent 1544e9dbf9
commit 18f57d1800
2 changed files with 60 additions and 60 deletions

View File

@@ -0,0 +1 @@
from .subathon import setup as twitch_setup

View File

@@ -1,3 +1,4 @@
from collections import defaultdict
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional from typing import Optional
import random import random
@@ -5,50 +6,55 @@ 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 datamodels import BotChannel, Communities, UserProfile from meta import Bot
from meta import CrocBot from meta.sockets import Channel, register_channel
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): class TimerChannel(Channel):
# TODO: Replace the channel mechanism? name = 'SubTimer'
# 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'
def __init__(self, cog: 'SubathonComponent', **kwargs): def __init__(self, cog: 'SubathonComponent', **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.cog = cog self.cog = cog
# TODO: ... # This is horribly inefficient but will be deprecated by the web update
self.communityid = 1 self.communities = defaultdict(set) # Map of communityid -> listening websockets
async def on_connection(self, websocket, event): 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) await super().on_connection(websocket, event)
self.communities[community.communityid].add(websocket)
await self.send_set( await self.send_set(
**await self.get_args_for(self.communityid), **await self.get_args_for(community.communityid),
websocket=websocket, websocket=websocket,
) )
async def send_updates(self): async def send_updates(self, communityid: int):
args = await self.get_args_for(communityid)
for ws in self.communities[communityid]:
await self.send_set( await self.send_set(
**await self.get_args_for(self.communityid), **args,
websocket=ws
) )
async def get_args_for(self, channelid): async def get_args_for(self, communityid: int):
active = await self.cog.get_active_subathon(channelid) active = await self.cog.get_active_subathon(communityid)
if active is not None: if active is not None:
ending = utc_now() + timedelta(seconds=await active.get_remaining()) ending = utc_now() + timedelta(seconds=await active.get_remaining())
# TODO: Next goal info and overall goal progress, maybe last contrib
return { return {
'end_at': ending, 'end_at': ending,
'running': active.running 'running': active.running
@@ -71,8 +77,6 @@ class TimerChannel(Channel):
class ActiveSubathon: class ActiveSubathon:
# TODO: Version check
# Relies on event tracker and profiles module as well.
def __init__(self, subathondata: Subathon, runningdata: RunningSubathon | None): def __init__(self, subathondata: Subathon, runningdata: RunningSubathon | None):
self.subathondata = subathondata self.subathondata = subathondata
self.runningdata = runningdata self.runningdata = runningdata
@@ -143,28 +147,24 @@ class ActiveSubathon:
class SubathonComponent(cmds.Component): 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.bot = bot
self.data = bot.dbconn.load_registry(SubathonData()) self.data = bot.dbconn.load_registry(SubathonData())
self.channel = TimerChannel(self) self.channel = TimerChannel(self)
register_channel('SubTimer', self.channel) register_channel('SubTimer', self.channel)
# ----- API ----- # ----- API -----
async def component_load(self): async def component_load(self):
# TODO: Setup the websocket
await self.data.init() await self.data.init()
await self.bot.version_check(*self.data.VERSION)
async def component_teardown(self): async def component_teardown(self):
pass pass
# ----- Methods ----- # ----- 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: async def get_active_subathon(self, communityid: int) -> ActiveSubathon | None:
rows = await Subathon.fetch_where(communityid=communityid, ended_at=None) rows = await Subathon.fetch_where(communityid=communityid, ended_at=None)
if rows: if rows:
@@ -208,15 +208,15 @@ class SubathonComponent(cmds.Component):
contrib_str = f"{name} contributed {score} bit{pl}" contrib_str = f"{name} contributed {score} bit{pl}"
if not await active.check_cap(): 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: else:
contrib_str += " towards our studython! Thank you holono1Heart" contrib_str += " towards our subathon! Thank you <3"
await bits_payload.broadcaster.send_message( await bits_payload.broadcaster.send_message(
contrib_str, contrib_str,
sender=self.bot.bot_id 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) await self.goalcheck(active, bits_payload.broadcaster)
# Check goals # Check goals
@@ -254,15 +254,15 @@ class SubathonComponent(cmds.Component):
contrib_str = f"{name} contributed {score} sub{pl}" contrib_str = f"{name} contributed {score} sub{pl}"
if not await active.check_cap(): 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: else:
contrib_str += " towards our studython! Thank you holono1Heart" contrib_str += " towards our subathon! Thank you <3"
await sub_payload.broadcaster.send_message( await sub_payload.broadcaster.send_message(
contrib_str, contrib_str,
sender=self.bot.bot_id sender=self.bot.bot_id
) )
await self.channel.send_updates() await self.channel.send_updates(event_row['communityid'])
# Check goals # Check goals
await self.goalcheck(active, sub_payload.broadcaster) await self.goalcheck(active, sub_payload.broadcaster)
@@ -296,15 +296,15 @@ class SubathonComponent(cmds.Component):
contrib_str = f"{name} contributed {score} subs" contrib_str = f"{name} contributed {score} subs"
if not await active.check_cap(): 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: else:
contrib_str += " towards our studython! Thank you holono1Heart" contrib_str += " towards our subathon! Thank you <3"
await gift_payload.broadcaster.send_message( await gift_payload.broadcaster.send_message(
contrib_str, contrib_str,
sender=self.bot.bot_id sender=self.bot.bot_id
) )
await self.channel.send_updates() await self.channel.send_updates(event_row['communityid'])
# Check goals # Check goals
await self.goalcheck(active, gift_payload.broadcaster) await self.goalcheck(active, gift_payload.broadcaster)
@@ -339,22 +339,22 @@ class SubathonComponent(cmds.Component):
contrib_str = f"{name} contributed {score} sub{pl}" contrib_str = f"{name} contributed {score} sub{pl}"
if not await active.check_cap(): 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: else:
contrib_str += " towards our studython! Thank you holono1Heart" contrib_str += " towards our subathon! Thank you <3"
await sub_payload.broadcaster.send_message( await sub_payload.broadcaster.send_message(
contrib_str, contrib_str,
sender=self.bot.bot_id sender=self.bot.bot_id
) )
await self.channel.send_updates() await self.channel.send_updates(event_row['communityid'])
# Check goals # Check goals
await self.goalcheck(active, sub_payload.broadcaster) 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):
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 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:
@@ -363,13 +363,13 @@ 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
) )
await self.channel.send_updates() await self.channel.send_updates(cid)
# ----- Commands ----- # ----- 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): 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 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:
score = await active.get_score() 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): 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:
# TODO: Usage. Maybe implement ? commands? # 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 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 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!")
@@ -414,18 +414,18 @@ class SubathonComponent(cmds.Component):
timecap=timecap 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.")
await self.channel.send_updates() await self.channel.send_updates(cid)
# subathon stop # subathon stop
@group_subathon.command(name='stop') @group_subathon.command(name='stop')
async def cmd_stop(self, ctx: cmds.Context): async def cmd_stop(self, ctx: cmds.Context):
if ctx.broadcaster: 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 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() 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()
@@ -441,13 +441,13 @@ class SubathonComponent(cmds.Component):
@group_subathon.command(name='pause') @group_subathon.command(name='pause')
async def cmd_pause(self, ctx: cmds.Context): async def cmd_pause(self, ctx: cmds.Context):
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.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("Subathon timer paused!") await ctx.reply("Subathon timer paused!")
await self.channel.send_updates() await self.channel.send_updates(cid)
else: else:
await ctx.reply("Subathon timer already paused!") await ctx.reply("Subathon timer already paused!")
else: else:
@@ -457,13 +457,13 @@ class SubathonComponent(cmds.Component):
@group_subathon.command(name='resume') @group_subathon.command(name='resume')
async def cmd_resume(self, ctx: cmds.Context): async def cmd_resume(self, ctx: cmds.Context):
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.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("Subathon timer resumed!") await ctx.reply("Subathon timer resumed!")
await self.channel.send_updates() await self.channel.send_updates(cid)
else: else:
await ctx.reply("Subathon timer already running!") await ctx.reply("Subathon timer already running!")
else: else:
@@ -472,7 +472,7 @@ class SubathonComponent(cmds.Component):
@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
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 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:
goals = await active.get_goals() goals = await active.get_goals()
@@ -492,7 +492,7 @@ class SubathonComponent(cmds.Component):
@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.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(
@@ -502,5 +502,4 @@ class SubathonComponent(cmds.Component):
) )
await ctx.reply("Goal added!") await ctx.reply("Goal added!")
else: else:
await ctx.reply("No active subathon to goal!") await ctx.reply("No active subathon to add goal to!")