feat: Add websocket channel
This commit is contained in:
107
hyperfocus/channel.py
Normal file
107
hyperfocus/channel.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from typing import Optional, TypeAlias, TypedDict
|
||||||
|
import json
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from meta.sockets import Channel
|
||||||
|
from modules.profiles.profiles.profiles import ProfilesRegistry
|
||||||
|
|
||||||
|
from . import logger
|
||||||
|
from .data import Hyperfocuser, HyperfocusData
|
||||||
|
|
||||||
|
ISOTimestamp: TypeAlias = str
|
||||||
|
|
||||||
|
|
||||||
|
class HyperfocusedPayload(TypedDict):
|
||||||
|
userid: str
|
||||||
|
user_name: str
|
||||||
|
started_at: ISOTimestamp
|
||||||
|
ends_at: ISOTimestamp
|
||||||
|
|
||||||
|
|
||||||
|
async def prepare_hyperfocuser(profiler: ProfilesRegistry, hyperfocuser: Hyperfocuser):
|
||||||
|
profile = await profiler.get_profile(hyperfocuser.profileid)
|
||||||
|
assert profile is not None
|
||||||
|
|
||||||
|
return HyperfocusedPayload(
|
||||||
|
userid=str(hyperfocuser.profileid),
|
||||||
|
user_name=profile.nickname or "Unknown",
|
||||||
|
started_at=hyperfocuser.started_at.isoformat(),
|
||||||
|
ends_at=hyperfocuser.ends_at.isoformat(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FocusChannel(Channel):
|
||||||
|
name = "HyperFocus"
|
||||||
|
|
||||||
|
def __init__(self, profiler: ProfilesRegistry, focusdata: HyperfocusData, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self.profiler = profiler
|
||||||
|
self.focusdata = focusdata
|
||||||
|
|
||||||
|
# communityid -> listening websockets
|
||||||
|
self.communities = defaultdict(set)
|
||||||
|
|
||||||
|
async def on_connection(self, websocket, event):
|
||||||
|
if not (cidstr := event.get("community")):
|
||||||
|
raise ValueError("Hyperfocus browser missing communityid")
|
||||||
|
elif not cidstr.isdigit():
|
||||||
|
raise ValueError("Community id provided is not an integer")
|
||||||
|
cid = int(cidstr)
|
||||||
|
community = await self.profiler.get_community(cid)
|
||||||
|
if community is None:
|
||||||
|
raise ValueError("Unknown community provided")
|
||||||
|
|
||||||
|
await super().on_connection(websocket, event)
|
||||||
|
self.communities[cid].add(websocket)
|
||||||
|
|
||||||
|
focus_rows = await Hyperfocuser.fetch_where(started_in=cid)
|
||||||
|
await self.send_hyperfocus_put(cid, focus_rows)
|
||||||
|
|
||||||
|
async def del_connection(self, websocket):
|
||||||
|
for wss in self.communities.values():
|
||||||
|
wss.discard(websocket)
|
||||||
|
await super().del_connection(websocket)
|
||||||
|
|
||||||
|
async def send_hyperfocus_patch(
|
||||||
|
self, communityid: int, focuser: Hyperfocuser, websocket=None
|
||||||
|
):
|
||||||
|
for ws in (websocket,) if websocket else self.communities[communityid]:
|
||||||
|
await self.send_event(
|
||||||
|
{
|
||||||
|
"type": "DO",
|
||||||
|
"method": "patchFocus",
|
||||||
|
"args": await prepare_hyperfocuser(self.profiler, focuser),
|
||||||
|
},
|
||||||
|
websocket=ws,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send_hyperfocus_del(self, profileid: int, websocket=None):
|
||||||
|
await self.send_event(
|
||||||
|
{
|
||||||
|
"type": "DO",
|
||||||
|
"method": "delFocus",
|
||||||
|
"args": {"userid": profileid},
|
||||||
|
},
|
||||||
|
websocket=websocket,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send_hyperfocus_put(
|
||||||
|
self, communityid: int, focusers: list[Hyperfocuser], websocket=None
|
||||||
|
):
|
||||||
|
payload = []
|
||||||
|
for focuser in focusers:
|
||||||
|
fpl = await prepare_hyperfocuser(self.profiler, focuser)
|
||||||
|
payload.append(fpl)
|
||||||
|
|
||||||
|
for ws in (websocket,) if websocket else self.communities[communityid]:
|
||||||
|
await self.send_event(
|
||||||
|
{"type": "DO", "method": "putFocus", "args": payload},
|
||||||
|
websocket=ws,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send_event(self, event, **kwargs):
|
||||||
|
logger.info(f"Sending websocket event: {json.dumps(event, indent=1)}")
|
||||||
|
await super().send_event(event, **kwargs)
|
||||||
@@ -11,11 +11,13 @@ from twitchio.ext import commands as cmds
|
|||||||
from botdata import UserAuth
|
from botdata import UserAuth
|
||||||
from meta import Bot
|
from meta import Bot
|
||||||
from meta.logger import log_wrap
|
from meta.logger import log_wrap
|
||||||
|
from meta.sockets import register_channel
|
||||||
from utils.lib import parse_dur, strfdelta, utc_now
|
from utils.lib import parse_dur, strfdelta, utc_now
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
|
|
||||||
from ..data import HyperfocusData, Hyperfocuser
|
from ..data import HyperfocusData, Hyperfocuser
|
||||||
|
from ..channel import FocusChannel
|
||||||
|
|
||||||
|
|
||||||
# Default requested scopes for joining a channel
|
# Default requested scopes for joining a channel
|
||||||
@@ -33,6 +35,9 @@ class FocusComponent(cmds.Component):
|
|||||||
def __init__(self, bot: Bot):
|
def __init__(self, bot: Bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.data = bot.dbconn.load_registry(HyperfocusData())
|
self.data = bot.dbconn.load_registry(HyperfocusData())
|
||||||
|
self.channel = FocusChannel(self.bot.profiles.profiles, self.data)
|
||||||
|
|
||||||
|
register_channel(self.channel.name, self.channel)
|
||||||
|
|
||||||
self._last_deleted: dict[int, datetime] = {}
|
self._last_deleted: dict[int, datetime] = {}
|
||||||
|
|
||||||
@@ -96,22 +101,41 @@ class FocusComponent(cmds.Component):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await self.focus_delete_message(payload)
|
await self.focus_delete_message(payload)
|
||||||
if notify:
|
deleted = True
|
||||||
await payload.broadcaster.send_message(
|
|
||||||
f"@{payload.chatter.name} Stay focused! "
|
|
||||||
"(You are in !hyperfocus, use !unfocus to come back if you need to!)",
|
|
||||||
sender=self.bot.bot_id,
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Failed to delete a hyperfocus message: {payload!r}", exc_info=True
|
f"Failed to delete a hyperfocus message: {payload!r}", exc_info=True
|
||||||
)
|
)
|
||||||
if notify:
|
deleted = False
|
||||||
await payload.broadcaster.send_message(
|
|
||||||
f"@{payload.chatter.name} Stay focused! ",
|
if notify:
|
||||||
sender=self.bot.bot_id,
|
self._last_deleted[profile.profileid] = utc_now()
|
||||||
|
try:
|
||||||
|
if deleted:
|
||||||
|
await payload.broadcaster.send_message(
|
||||||
|
f"@{payload.chatter.name} Stay focused! "
|
||||||
|
"(You are in !hyperfocus, use !unfocus to come back if you need to!)",
|
||||||
|
sender=self.bot.bot_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await payload.broadcaster.send_message(
|
||||||
|
f"@{payload.chatter.name} Stay focused! ",
|
||||||
|
sender=self.bot.bot_id,
|
||||||
|
)
|
||||||
|
except twitchio.exceptions.HTTPException:
|
||||||
|
logger.warning(
|
||||||
|
f"Failed to notify user of hyperfocus deletion: {payload!r}",
|
||||||
|
exc_info=True,
|
||||||
)
|
)
|
||||||
self._last_deleted[profile.profileid] = utc_now()
|
|
||||||
|
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 -----
|
# ------ Commands -----
|
||||||
@cmds.command(
|
@cmds.command(
|
||||||
@@ -150,14 +174,15 @@ class FocusComponent(cmds.Component):
|
|||||||
comm = await self.bot.profiles.fetch_community(ctx.broadcaster, touch=True)
|
comm = await self.bot.profiles.fetch_community(ctx.broadcaster, touch=True)
|
||||||
|
|
||||||
await Hyperfocuser.table.delete_where(profileid=pid)
|
await Hyperfocuser.table.delete_where(profileid=pid)
|
||||||
await Hyperfocuser.create(
|
focuser = await Hyperfocuser.create(
|
||||||
profileid=pid,
|
profileid=pid,
|
||||||
started_at=now,
|
started_at=now,
|
||||||
ends_at=end_at,
|
ends_at=end_at,
|
||||||
started_in=comm.communityid,
|
started_in=comm.communityid,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: Update channel
|
await self.channel.send_hyperfocus_patch(comm.communityid, focuser)
|
||||||
|
|
||||||
minutes = ceil(dur / 60)
|
minutes = ceil(dur / 60)
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
f"{ctx.chatter.name} has gone into HYPERFOCUS! "
|
f"{ctx.chatter.name} has gone into HYPERFOCUS! "
|
||||||
@@ -171,6 +196,7 @@ class FocusComponent(cmds.Component):
|
|||||||
row = await Hyperfocuser.fetch(profile.profileid)
|
row = await Hyperfocuser.fetch(profile.profileid)
|
||||||
if row:
|
if row:
|
||||||
await row.delete()
|
await row.delete()
|
||||||
|
await self.channel.send_hyperfocus_del(profile.profileid)
|
||||||
|
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
"Welcome back from focus, hope it went well!"
|
"Welcome back from focus, hope it went well!"
|
||||||
|
|||||||
Reference in New Issue
Block a user