Migrate to new plugin architecture.
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
from .subathon import setup as twitch_setup
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from collections import defaultdict
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import random
|
import random
|
||||||
@@ -5,50 +6,55 @@ import twitchio
|
|||||||
from twitchio import PartialUser, Scopes, eventsub
|
from twitchio import PartialUser, Scopes, eventsub
|
||||||
from twitchio.ext import commands as cmds
|
from twitchio.ext import commands as cmds
|
||||||
|
|
||||||
from datamodels import BotChannel, Communities, UserProfile
|
from meta import Bot
|
||||||
from meta import CrocBot
|
from meta.sockets import Channel, register_channel
|
||||||
from utils.lib import utc_now, strfdelta
|
from utils.lib import utc_now, strfdelta
|
||||||
from sockets import Channel, register_channel
|
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
from .data import SubathonData, Subathon, RunningSubathon, SubathonContribution, SubathonGoal
|
from .data import SubathonData, Subathon, RunningSubathon, SubathonContribution, SubathonGoal
|
||||||
|
|
||||||
|
|
||||||
class TimerChannel(Channel):
|
class TimerChannel(Channel):
|
||||||
# TODO: Replace the channel mechanism?
|
name = 'SubTimer'
|
||||||
# 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'
|
|
||||||
|
|
||||||
def __init__(self, cog: 'SubathonComponent', **kwargs):
|
def __init__(self, cog: 'SubathonComponent', **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.cog = cog
|
self.cog = cog
|
||||||
|
|
||||||
# TODO: ...
|
# This is horribly inefficient but will be deprecated by the web update
|
||||||
self.communityid = 1
|
self.communities = defaultdict(set) # Map of communityid -> listening websockets
|
||||||
|
|
||||||
async def on_connection(self, websocket, event):
|
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)
|
await super().on_connection(websocket, event)
|
||||||
|
self.communities[community.communityid].add(websocket)
|
||||||
|
|
||||||
await self.send_set(
|
await self.send_set(
|
||||||
**await self.get_args_for(self.communityid),
|
**await self.get_args_for(community.communityid),
|
||||||
websocket=websocket,
|
websocket=websocket,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def send_updates(self):
|
async def send_updates(self, communityid: int):
|
||||||
|
args = await self.get_args_for(communityid)
|
||||||
|
for ws in self.communities[communityid]:
|
||||||
await self.send_set(
|
await self.send_set(
|
||||||
**await self.get_args_for(self.communityid),
|
**args,
|
||||||
|
websocket=ws
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_args_for(self, channelid):
|
async def get_args_for(self, communityid: int):
|
||||||
active = await self.cog.get_active_subathon(channelid)
|
active = await self.cog.get_active_subathon(communityid)
|
||||||
if active is not None:
|
if active is not None:
|
||||||
ending = utc_now() + timedelta(seconds=await active.get_remaining())
|
ending = utc_now() + timedelta(seconds=await active.get_remaining())
|
||||||
|
# TODO: Next goal info and overall goal progress, maybe last contrib
|
||||||
return {
|
return {
|
||||||
'end_at': ending,
|
'end_at': ending,
|
||||||
'running': active.running
|
'running': active.running
|
||||||
@@ -71,8 +77,6 @@ class TimerChannel(Channel):
|
|||||||
|
|
||||||
|
|
||||||
class ActiveSubathon:
|
class ActiveSubathon:
|
||||||
# TODO: Version check
|
|
||||||
# Relies on event tracker and profiles module as well.
|
|
||||||
def __init__(self, subathondata: Subathon, runningdata: RunningSubathon | None):
|
def __init__(self, subathondata: Subathon, runningdata: RunningSubathon | None):
|
||||||
self.subathondata = subathondata
|
self.subathondata = subathondata
|
||||||
self.runningdata = runningdata
|
self.runningdata = runningdata
|
||||||
@@ -143,28 +147,24 @@ class ActiveSubathon:
|
|||||||
|
|
||||||
|
|
||||||
class SubathonComponent(cmds.Component):
|
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.bot = bot
|
||||||
self.data = bot.dbconn.load_registry(SubathonData())
|
self.data = bot.dbconn.load_registry(SubathonData())
|
||||||
self.channel = TimerChannel(self)
|
self.channel = TimerChannel(self)
|
||||||
register_channel('SubTimer', self.channel)
|
register_channel('SubTimer', self.channel)
|
||||||
|
|
||||||
|
|
||||||
# ----- API -----
|
# ----- API -----
|
||||||
async def component_load(self):
|
async def component_load(self):
|
||||||
# TODO: Setup the websocket
|
|
||||||
await self.data.init()
|
await self.data.init()
|
||||||
|
await self.bot.version_check(*self.data.VERSION)
|
||||||
|
|
||||||
async def component_teardown(self):
|
async def component_teardown(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# ----- Methods -----
|
# ----- 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:
|
async def get_active_subathon(self, communityid: int) -> ActiveSubathon | None:
|
||||||
rows = await Subathon.fetch_where(communityid=communityid, ended_at=None)
|
rows = await Subathon.fetch_where(communityid=communityid, ended_at=None)
|
||||||
if rows:
|
if rows:
|
||||||
@@ -208,15 +208,15 @@ class SubathonComponent(cmds.Component):
|
|||||||
|
|
||||||
contrib_str = f"{name} contributed {score} bit{pl}"
|
contrib_str = f"{name} contributed {score} bit{pl}"
|
||||||
if not await active.check_cap():
|
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:
|
else:
|
||||||
contrib_str += " towards our studython! Thank you holono1Heart"
|
contrib_str += " towards our subathon! Thank you <3"
|
||||||
|
|
||||||
await bits_payload.broadcaster.send_message(
|
await bits_payload.broadcaster.send_message(
|
||||||
contrib_str,
|
contrib_str,
|
||||||
sender=self.bot.bot_id
|
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)
|
await self.goalcheck(active, bits_payload.broadcaster)
|
||||||
# Check goals
|
# Check goals
|
||||||
|
|
||||||
@@ -254,15 +254,15 @@ class SubathonComponent(cmds.Component):
|
|||||||
|
|
||||||
contrib_str = f"{name} contributed {score} sub{pl}"
|
contrib_str = f"{name} contributed {score} sub{pl}"
|
||||||
if not await active.check_cap():
|
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:
|
else:
|
||||||
contrib_str += " towards our studython! Thank you holono1Heart"
|
contrib_str += " towards our subathon! Thank you <3"
|
||||||
|
|
||||||
await sub_payload.broadcaster.send_message(
|
await sub_payload.broadcaster.send_message(
|
||||||
contrib_str,
|
contrib_str,
|
||||||
sender=self.bot.bot_id
|
sender=self.bot.bot_id
|
||||||
)
|
)
|
||||||
await self.channel.send_updates()
|
await self.channel.send_updates(event_row['communityid'])
|
||||||
# Check goals
|
# Check goals
|
||||||
await self.goalcheck(active, sub_payload.broadcaster)
|
await self.goalcheck(active, sub_payload.broadcaster)
|
||||||
|
|
||||||
@@ -296,15 +296,15 @@ class SubathonComponent(cmds.Component):
|
|||||||
|
|
||||||
contrib_str = f"{name} contributed {score} subs"
|
contrib_str = f"{name} contributed {score} subs"
|
||||||
if not await active.check_cap():
|
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:
|
else:
|
||||||
contrib_str += " towards our studython! Thank you holono1Heart"
|
contrib_str += " towards our subathon! Thank you <3"
|
||||||
|
|
||||||
await gift_payload.broadcaster.send_message(
|
await gift_payload.broadcaster.send_message(
|
||||||
contrib_str,
|
contrib_str,
|
||||||
sender=self.bot.bot_id
|
sender=self.bot.bot_id
|
||||||
)
|
)
|
||||||
await self.channel.send_updates()
|
await self.channel.send_updates(event_row['communityid'])
|
||||||
# Check goals
|
# Check goals
|
||||||
await self.goalcheck(active, gift_payload.broadcaster)
|
await self.goalcheck(active, gift_payload.broadcaster)
|
||||||
|
|
||||||
@@ -339,22 +339,22 @@ class SubathonComponent(cmds.Component):
|
|||||||
|
|
||||||
contrib_str = f"{name} contributed {score} sub{pl}"
|
contrib_str = f"{name} contributed {score} sub{pl}"
|
||||||
if not await active.check_cap():
|
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:
|
else:
|
||||||
contrib_str += " towards our studython! Thank you holono1Heart"
|
contrib_str += " towards our subathon! Thank you <3"
|
||||||
|
|
||||||
await sub_payload.broadcaster.send_message(
|
await sub_payload.broadcaster.send_message(
|
||||||
contrib_str,
|
contrib_str,
|
||||||
sender=self.bot.bot_id
|
sender=self.bot.bot_id
|
||||||
)
|
)
|
||||||
await self.channel.send_updates()
|
await self.channel.send_updates(event_row['communityid'])
|
||||||
# Check goals
|
# Check goals
|
||||||
await self.goalcheck(active, sub_payload.broadcaster)
|
await self.goalcheck(active, sub_payload.broadcaster)
|
||||||
|
|
||||||
# end stream => Automatically pause the timer
|
# end stream => Automatically pause the timer
|
||||||
@cmds.Component.listener()
|
@cmds.Component.listener()
|
||||||
async def event_stream_offline(self, payload: twitchio.StreamOffline):
|
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
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is not None:
|
if (active := await self.get_active_subathon(cid)) is not None:
|
||||||
if active.running:
|
if active.running:
|
||||||
@@ -363,13 +363,13 @@ class SubathonComponent(cmds.Component):
|
|||||||
"Paused the subathon timer because the stream went offline!",
|
"Paused the subathon timer because the stream went offline!",
|
||||||
sender=self.bot.bot_id
|
sender=self.bot.bot_id
|
||||||
)
|
)
|
||||||
await self.channel.send_updates()
|
await self.channel.send_updates(cid)
|
||||||
|
|
||||||
# ----- Commands -----
|
# ----- Commands -----
|
||||||
|
|
||||||
@cmds.group(name='subathon', aliases=['studython'], invoke_fallback=True)
|
@cmds.group(name='subathon', aliases=['studython'], invoke_fallback=True)
|
||||||
async def group_subathon(self, ctx: cmds.Context):
|
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
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is not None:
|
if (active := await self.get_active_subathon(cid)) is not None:
|
||||||
score = await active.get_score()
|
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):
|
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:
|
if ctx.broadcaster:
|
||||||
# TODO: Usage. Maybe implement ? commands?
|
# 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
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is not None:
|
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!")
|
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
|
timecap=timecap
|
||||||
)
|
)
|
||||||
await ctx.reply("Setup a new subathon! Use !subathon resume to get the timer running.")
|
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
|
# subathon stop
|
||||||
@group_subathon.command(name='stop')
|
@group_subathon.command(name='stop')
|
||||||
async def cmd_stop(self, ctx: cmds.Context):
|
async def cmd_stop(self, ctx: cmds.Context):
|
||||||
if ctx.broadcaster:
|
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
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is not None:
|
if (active := await self.get_active_subathon(cid)) is not None:
|
||||||
if active.running:
|
if active.running:
|
||||||
await active.pause()
|
await active.pause()
|
||||||
await self.channel.send_updates()
|
await self.channel.send_updates(cid)
|
||||||
await active.subathondata.update(ended_at=utc_now())
|
await active.subathondata.update(ended_at=utc_now())
|
||||||
total = await active.get_score()
|
total = await active.get_score()
|
||||||
dursecs = active.get_duration()
|
dursecs = active.get_duration()
|
||||||
@@ -441,13 +441,13 @@ class SubathonComponent(cmds.Component):
|
|||||||
@group_subathon.command(name='pause')
|
@group_subathon.command(name='pause')
|
||||||
async def cmd_pause(self, ctx: cmds.Context):
|
async def cmd_pause(self, ctx: cmds.Context):
|
||||||
if ctx.broadcaster or ctx.author.moderator:
|
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
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is not None:
|
if (active := await self.get_active_subathon(cid)) is not None:
|
||||||
if active.running:
|
if active.running:
|
||||||
await active.pause()
|
await active.pause()
|
||||||
await ctx.reply("Subathon timer paused!")
|
await ctx.reply("Subathon timer paused!")
|
||||||
await self.channel.send_updates()
|
await self.channel.send_updates(cid)
|
||||||
else:
|
else:
|
||||||
await ctx.reply("Subathon timer already paused!")
|
await ctx.reply("Subathon timer already paused!")
|
||||||
else:
|
else:
|
||||||
@@ -457,13 +457,13 @@ class SubathonComponent(cmds.Component):
|
|||||||
@group_subathon.command(name='resume')
|
@group_subathon.command(name='resume')
|
||||||
async def cmd_resume(self, ctx: cmds.Context):
|
async def cmd_resume(self, ctx: cmds.Context):
|
||||||
if ctx.broadcaster or ctx.author.moderator:
|
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
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is not None:
|
if (active := await self.get_active_subathon(cid)) is not None:
|
||||||
if not active.running:
|
if not active.running:
|
||||||
await active.resume()
|
await active.resume()
|
||||||
await ctx.reply("Subathon timer resumed!")
|
await ctx.reply("Subathon timer resumed!")
|
||||||
await self.channel.send_updates()
|
await self.channel.send_updates(cid)
|
||||||
else:
|
else:
|
||||||
await ctx.reply("Subathon timer already running!")
|
await ctx.reply("Subathon timer already running!")
|
||||||
else:
|
else:
|
||||||
@@ -472,7 +472,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
@cmds.group(name='goals', invoke_fallback=True)
|
@cmds.group(name='goals', invoke_fallback=True)
|
||||||
async def group_goals(self, ctx: cmds.Context):
|
async def group_goals(self, ctx: cmds.Context):
|
||||||
# List the goals
|
# 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
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is not None:
|
if (active := await self.get_active_subathon(cid)) is not None:
|
||||||
goals = await active.get_goals()
|
goals = await active.get_goals()
|
||||||
@@ -492,7 +492,7 @@ class SubathonComponent(cmds.Component):
|
|||||||
@group_goals.command(name='add')
|
@group_goals.command(name='add')
|
||||||
async def cmd_add(self, ctx: cmds.Context, required: int, *, description: str):
|
async def cmd_add(self, ctx: cmds.Context, required: int, *, description: str):
|
||||||
if ctx.broadcaster or ctx.author.moderator:
|
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
|
cid = community.communityid
|
||||||
if (active := await self.get_active_subathon(cid)) is not None:
|
if (active := await self.get_active_subathon(cid)) is not None:
|
||||||
await SubathonGoal.create(
|
await SubathonGoal.create(
|
||||||
@@ -502,5 +502,4 @@ class SubathonComponent(cmds.Component):
|
|||||||
)
|
)
|
||||||
await ctx.reply("Goal added!")
|
await ctx.reply("Goal added!")
|
||||||
else:
|
else:
|
||||||
await ctx.reply("No active subathon to goal!")
|
await ctx.reply("No active subathon to add goal to!")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user