Compare commits
6 Commits
41ec25cd21
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 93c17112e3 | |||
| 26dc684851 | |||
| 1bd2e9607a | |||
| a14cc4056e | |||
| 3885710689 | |||
| 737871d696 |
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": str(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)
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
from string import punctuation
|
||||
import datetime as dt
|
||||
from datetime import datetime, timedelta
|
||||
@@ -11,11 +12,13 @@ from twitchio.ext import commands as cmds
|
||||
from botdata import UserAuth
|
||||
from meta import Bot
|
||||
from meta.logger import log_wrap
|
||||
from meta.sockets import register_channel
|
||||
from utils.lib import parse_dur, strfdelta, utc_now
|
||||
|
||||
from . import logger
|
||||
|
||||
from ..data import HyperfocusData, Hyperfocuser
|
||||
from ..channel import FocusChannel
|
||||
|
||||
|
||||
# Default requested scopes for joining a channel
|
||||
@@ -33,8 +36,12 @@ class FocusComponent(cmds.Component):
|
||||
def __init__(self, bot: Bot):
|
||||
self.bot = bot
|
||||
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.hyperfocus_lock = asyncio.Lock()
|
||||
|
||||
# ----- API -----
|
||||
async def component_load(self):
|
||||
@@ -65,7 +72,7 @@ class FocusComponent(cmds.Component):
|
||||
|
||||
This amounts to whether it starts with a punctuation symbol, or it is only emotes and mentions.
|
||||
"""
|
||||
allowed = message.text.startswith(tuple(punctuation))
|
||||
allowed = message.text.startswith(tuple("!*#%|?><."))
|
||||
|
||||
if not allowed:
|
||||
allowed = True
|
||||
@@ -83,6 +90,10 @@ class FocusComponent(cmds.Component):
|
||||
|
||||
@cmds.Component.listener()
|
||||
async def event_message(self, payload: twitchio.ChatMessage):
|
||||
async with self.hyperfocus_lock:
|
||||
await self.handle_message(payload)
|
||||
|
||||
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)
|
||||
hyperfocused = await self.get_hyperfocus(profile.profileid)
|
||||
@@ -96,22 +107,41 @@ class FocusComponent(cmds.Component):
|
||||
)
|
||||
try:
|
||||
await self.focus_delete_message(payload)
|
||||
if notify:
|
||||
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,
|
||||
)
|
||||
deleted = True
|
||||
except Exception:
|
||||
logger.warning(
|
||||
f"Failed to delete a hyperfocus message: {payload!r}", exc_info=True
|
||||
)
|
||||
if notify:
|
||||
await payload.broadcaster.send_message(
|
||||
f"@{payload.chatter.name} Stay focused! ",
|
||||
sender=self.bot.bot_id,
|
||||
deleted = False
|
||||
|
||||
if notify:
|
||||
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 -----
|
||||
@cmds.command(
|
||||
@@ -149,33 +179,37 @@ class FocusComponent(cmds.Component):
|
||||
pid = profile.profileid
|
||||
comm = await self.bot.profiles.fetch_community(ctx.broadcaster, touch=True)
|
||||
|
||||
await Hyperfocuser.table.delete_where(profileid=pid)
|
||||
await Hyperfocuser.create(
|
||||
profileid=pid,
|
||||
started_at=now,
|
||||
ends_at=end_at,
|
||||
started_in=comm.communityid,
|
||||
)
|
||||
async with self.hyperfocus_lock:
|
||||
await Hyperfocuser.table.delete_where(profileid=pid)
|
||||
focuser = await Hyperfocuser.create(
|
||||
profileid=pid,
|
||||
started_at=now,
|
||||
ends_at=end_at,
|
||||
started_in=comm.communityid,
|
||||
)
|
||||
|
||||
# TODO: Update channel
|
||||
minutes = ceil(dur / 60)
|
||||
await ctx.reply(
|
||||
f"{ctx.chatter.name} has gone into HYPERFOCUS! "
|
||||
f"They will be in emote and command only mode for the next {minutes} minutes! "
|
||||
"Use !unfocus to come back if you need to, best of luck! ☘️🍀☘️ "
|
||||
)
|
||||
await self.channel.send_hyperfocus_patch(comm.communityid, focuser)
|
||||
|
||||
minutes = ceil(dur / 60)
|
||||
await ctx.reply(
|
||||
f"{ctx.chatter.name} has gone into HYPERFOCUS! "
|
||||
f"They will be in emote and command only mode for the next {minutes} minutes! "
|
||||
"Use !unfocus to come back if you need to, best of luck! ☘️🍀☘️ "
|
||||
)
|
||||
|
||||
@cmds.command(name="unfocus")
|
||||
async def unfocus_cmd(self, ctx):
|
||||
profile = await self.bot.profiles.fetch_profile(ctx.chatter, touch=True)
|
||||
row = await Hyperfocuser.fetch(profile.profileid)
|
||||
if row:
|
||||
await row.delete()
|
||||
async with self.hyperfocus_lock:
|
||||
row = await Hyperfocuser.fetch(profile.profileid)
|
||||
if row:
|
||||
await row.delete()
|
||||
await self.channel.send_hyperfocus_del(profile.profileid)
|
||||
|
||||
await ctx.reply(
|
||||
"Welcome back from focus, hope it went well!"
|
||||
" Remember to have a sip and stretch if you need it~"
|
||||
)
|
||||
await ctx.reply(
|
||||
"Welcome back from focus, hope it went well!"
|
||||
" Remember to have a sip and stretch if you need it~"
|
||||
)
|
||||
|
||||
@cmds.command(name="hyperfocused")
|
||||
async def hyperfocused_cmd(self, ctx, user: twitchio.User | None = None):
|
||||
@@ -183,16 +217,32 @@ class FocusComponent(cmds.Component):
|
||||
|
||||
profile = await self.bot.profiles.fetch_profile(user, touch=False)
|
||||
|
||||
if hyper := (await self.get_hyperfocus(profile.profileid)):
|
||||
durstr = strfdelta(hyper.ends_at - utc_now())
|
||||
await ctx.reply(
|
||||
f"{user.name} is in HYPERFOCUS for another {durstr}! "
|
||||
"They can only write emojis and commands in this time. Good luck!"
|
||||
)
|
||||
elif own:
|
||||
await ctx.reply(
|
||||
"You are not hyperfocused!"
|
||||
" Enter HYPERFOCUS mode for e.g. 10 minutes with '!hyperfocus 10'"
|
||||
)
|
||||
else:
|
||||
await ctx.reply(f"{user.name} is not hyperfocused!")
|
||||
async with self.hyperfocus_lock:
|
||||
if hyper := (await self.get_hyperfocus(profile.profileid)):
|
||||
durstr = strfdelta(hyper.ends_at - utc_now())
|
||||
await ctx.reply(
|
||||
f"{user.name} is in HYPERFOCUS for another {durstr}! "
|
||||
"They can only write emojis and commands in this time. Good luck!"
|
||||
)
|
||||
elif own:
|
||||
await ctx.reply(
|
||||
"You are not hyperfocused!"
|
||||
" Enter HYPERFOCUS mode for e.g. 10 minutes with '!hyperfocus 10'"
|
||||
)
|
||||
else:
|
||||
await ctx.reply(f"{user.name} is not hyperfocused!")
|
||||
|
||||
@cmds.command(name="addfocus")
|
||||
async def addfocus_cmd(self, ctx):
|
||||
await ctx.reply(
|
||||
"Add HYPERFOCUS to your channel by authorising me here: https://croccyfocus.thewisewolf.dev/invite"
|
||||
)
|
||||
|
||||
@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}")
|
||||
|
||||
Reference in New Issue
Block a user