Compare commits

..

1 Commits

Author SHA1 Message Date
d2050ab2dc feat: Add last_seen cache to dispatch. 2026-01-16 02:35:11 +10:00

View File

@@ -1,4 +1,5 @@
import asyncio import asyncio
from collections import defaultdict
from string import punctuation from string import punctuation
import datetime as dt import datetime as dt
from datetime import datetime, timedelta from datetime import datetime, timedelta
@@ -40,6 +41,7 @@ class FocusComponent(cmds.Component):
register_channel(self.channel.name, self.channel) register_channel(self.channel.name, self.channel)
self._last_seen: dict[int, dict[int, datetime]] = defaultdict(dict)
self._last_deleted: dict[int, datetime] = {} self._last_deleted: dict[int, datetime] = {}
self.hyperfocus_lock = asyncio.Lock() self.hyperfocus_lock = asyncio.Lock()
@@ -47,6 +49,44 @@ class FocusComponent(cmds.Component):
async def component_load(self): async def component_load(self):
await self.data.init() 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: async def get_hyperfocus(self, profileid: int) -> Hyperfocuser | None:
""" """
Get the Hyperfocuser if the user is hyperfocused. Get the Hyperfocuser if the user is hyperfocused.
@@ -96,6 +136,11 @@ class FocusComponent(cmds.Component):
async def handle_message(self, payload: twitchio.ChatMessage): async def handle_message(self, payload: twitchio.ChatMessage):
# Check if chatter is currently hyperfocused # Check if chatter is currently hyperfocused
profile = await self.bot.profiles.fetch_profile(payload.chatter, touch=True) 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) hyperfocused = await self.get_hyperfocus(profile.profileid)
# If they are, check the message content for deletion # 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 # If we need to delete, run delete and send message
notify = ( # notify = ( #
not (last := self._last_deleted.get(profile.profileid)) not (last := self._last_deleted.get(profile.profileid))
or (utc_now() - last).total_seconds() > 30 or (now - last).total_seconds() > 30
) )
try: try:
await self.focus_delete_message(payload) await self.focus_delete_message(payload)
@@ -115,7 +160,7 @@ class FocusComponent(cmds.Component):
deleted = False deleted = False
if notify: if notify:
self._last_deleted[profile.profileid] = utc_now() self._last_deleted[profile.profileid] = now
try: try:
if deleted: if deleted:
await payload.broadcaster.send_message( await payload.broadcaster.send_message(
@@ -136,16 +181,12 @@ class FocusComponent(cmds.Component):
if hyperfocused: if hyperfocused:
# Send an update to the channel # 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) await self.channel.send_hyperfocus_patch(comm.communityid, hyperfocused)
# ------ Commands ----- # ------ Commands -----
@cmds.command( @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): async def hyperfocus_cmd(self, ctx, *, duration: str | None = None):
now = utc_now() now = utc_now()
@@ -188,7 +229,7 @@ class FocusComponent(cmds.Component):
started_in=comm.communityid, 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) minutes = ceil(dur / 60)
await ctx.reply( await ctx.reply(
@@ -197,7 +238,7 @@ class FocusComponent(cmds.Component):
"Use !unfocus to come back if you need to, best of luck! ☘️🍀☘️ " "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): async def unfocus_cmd(self, ctx):
profile = await self.bot.profiles.fetch_profile(ctx.chatter, touch=True) profile = await self.bot.profiles.fetch_profile(ctx.chatter, touch=True)
async with self.hyperfocus_lock: async with self.hyperfocus_lock:
@@ -239,10 +280,11 @@ class FocusComponent(cmds.Component):
) )
@cmds.command(name="focuslist") @cmds.command(name="focuslist")
@cmds.is_moderator()
async def focuslist_cmd(self, ctx): async def focuslist_cmd(self, ctx):
comm = await self.bot.profiles.fetch_community(ctx.broadcaster, touch=True) comm = await self.bot.profiles.fetch_community(ctx.broadcaster, touch=True)
link = ( link = (
f"https://croccyfocus.thewisewolf.dev/widget/?community={comm.communityid}" 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})"
)