diff --git a/hyperfocus/twitch/component.py b/hyperfocus/twitch/component.py index cf0ca4d..34f2518 100644 --- a/hyperfocus/twitch/component.py +++ b/hyperfocus/twitch/component.py @@ -1,4 +1,5 @@ import asyncio +from collections import defaultdict from string import punctuation import datetime as dt from datetime import datetime, timedelta @@ -40,6 +41,7 @@ class FocusComponent(cmds.Component): register_channel(self.channel.name, self.channel) + self._last_seen: dict[int, dict[int, datetime]] = defaultdict(dict) self._last_deleted: dict[int, datetime] = {} self.hyperfocus_lock = asyncio.Lock() @@ -47,6 +49,44 @@ class FocusComponent(cmds.Component): async def component_load(self): await self.data.init() + async def dispatch_focuser_update( + self, + profileid: int, + communityid: int | None = None, + focuser: Hyperfocuser | None = None, + ): + """ + Dispatch the given profile's hyperfocus status along the channel. + + If the communityid is given, ensures that community receives the update. + """ + if focuser is None: + focuser = await self.get_hyperfocus(profileid) + elif focuser.profileid != profileid: + raise ValueError("Mis-matching profileid and focuser provided") + + targets = set() + if communityid is not None: + targets.add(communityid) + + now = utc_now() + + if focuser is not None: + # If we are sending active, send to all last seens newer than a certain date, and delete any old ones + last_seen_cids = self._last_seen[profileid].items() + for cid, last_seen in last_seen_cids: + if (now - last_seen).total_seconds() < 4 * 3600: + targets.add(cid) + for cid in targets: + await self.channel.send_hyperfocus_patch(cid, focuser) + else: + # If we are deleting, send to *all* last seens, and then delete any old ones + # targets.update(self._last_seen[profileid].keys()) + # for cid in targets: + await self.channel.send_hyperfocus_del(profileid) + # TODO: Cleanup old entries in last_seen + # TODO: Would prefer to use stream time and database member last seen instead + async def get_hyperfocus(self, profileid: int) -> Hyperfocuser | None: """ Get the Hyperfocuser if the user is hyperfocused. @@ -96,6 +136,11 @@ class FocusComponent(cmds.Component): async def handle_message(self, payload: twitchio.ChatMessage): # Check if chatter is currently hyperfocused profile = await self.bot.profiles.fetch_profile(payload.chatter, touch=True) + comm = await self.bot.profiles.fetch_community(payload.broadcaster, touch=True) + + now = utc_now() + self._last_seen[profile.profileid][comm.communityid] = now + hyperfocused = await self.get_hyperfocus(profile.profileid) # If they are, check the message content for deletion @@ -103,7 +148,7 @@ class FocusComponent(cmds.Component): # If we need to delete, run delete and send message notify = ( # not (last := self._last_deleted.get(profile.profileid)) - or (utc_now() - last).total_seconds() > 30 + or (now - last).total_seconds() > 30 ) try: await self.focus_delete_message(payload) @@ -115,7 +160,7 @@ class FocusComponent(cmds.Component): deleted = False if notify: - self._last_deleted[profile.profileid] = utc_now() + self._last_deleted[profile.profileid] = now try: if deleted: await payload.broadcaster.send_message( @@ -136,16 +181,12 @@ class FocusComponent(cmds.Component): if hyperfocused: # Send an update to the channel - # TODO: Nicer and more efficient caching of which channel has which users - # TODO: Possible race condition with the commands. Should use locks. - comm = await self.bot.profiles.fetch_community( - payload.broadcaster, touch=True - ) await self.channel.send_hyperfocus_patch(comm.communityid, hyperfocused) # ------ Commands ----- @cmds.command( - name="hyperfocus", aliases=["hyperf", "hyper", "hypercrocus", "hyperofcus"] + name="hyperfocus", + aliases=["hfocus", "hyperf", "hyper", "hypercrocus", "hyperofcus"], ) async def hyperfocus_cmd(self, ctx, *, duration: str | None = None): now = utc_now() @@ -188,7 +229,7 @@ class FocusComponent(cmds.Component): started_in=comm.communityid, ) - await self.channel.send_hyperfocus_patch(comm.communityid, focuser) + await self.dispatch_focuser_update(pid, comm.communityid, focuser) minutes = ceil(dur / 60) await ctx.reply( @@ -197,7 +238,7 @@ class FocusComponent(cmds.Component): "Use !unfocus to come back if you need to, best of luck! ☘️🍀☘️ " ) - @cmds.command(name="unfocus") + @cmds.command(name="unfocus", aliases=["uncrocus"]) async def unfocus_cmd(self, ctx): profile = await self.bot.profiles.fetch_profile(ctx.chatter, touch=True) async with self.hyperfocus_lock: @@ -239,10 +280,11 @@ class FocusComponent(cmds.Component): ) @cmds.command(name="focuslist") - @cmds.is_moderator() async def focuslist_cmd(self, ctx): comm = await self.bot.profiles.fetch_community(ctx.broadcaster, touch=True) link = ( f"https://croccyfocus.thewisewolf.dev/widget/?community={comm.communityid}" ) - await ctx.reply(f"Browser source link for your channel's hyperfocus: {link}") + await ctx.reply( + f"Browser source link for your channel's hyperfocus: {link} (For troubleshooting: your community id is {comm.communityid})" + )