From 18f57d180042e790376b9f80deaa07c86361b5f2 Mon Sep 17 00:00:00 2001 From: Interitio Date: Mon, 1 Sep 2025 23:24:38 +1000 Subject: [PATCH] Migrate to new plugin architecture. --- __init__.py | 1 + subathon/component.py | 119 +++++++++++++++++++++--------------------- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/__init__.py b/__init__.py index e69de29..e3f1491 100644 --- a/__init__.py +++ b/__init__.py @@ -0,0 +1 @@ +from .subathon import setup as twitch_setup diff --git a/subathon/component.py b/subathon/component.py index 0d83f93..3033041 100644 --- a/subathon/component.py +++ b/subathon/component.py @@ -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!")