Compare commits

...

2 Commits

14 changed files with 108 additions and 71 deletions

View File

@ -14,7 +14,7 @@ class GracefulShutdownException(Exception):
class ShutdownRequest(Event): class ShutdownRequest(Event):
pass _hidden = True
class ChatProcess(Process, ABC): class ChatProcess(Process, ABC):

View File

@ -1,3 +1,3 @@
from .group import cli from .group import cli
from .start import start from .start import start
from .mkevent import mkevent from .websocketutils import websocketutils

View File

@ -1,59 +0,0 @@
import json
import click
import websockets
from .group import cli
from events import Message, Subscription
from events.Message import USER_TYPE
from chats.Twitch.Events import Raid
from utils import make_sync
@make_sync
async def send(data, ws):
async with websockets.connect(ws) as websocket:
await websocket.send(data)
@cli.group()
@click.option('--ws', default='ws://localhost:8080')
@click.option('--via', default="console")
@click.pass_context
def mkevent(ctx, ws, via):
ctx.ensure_object(dict)
ctx.obj['ws'] = ws
ctx.obj['via'] = via
@mkevent.command(help="Make event via JSON")
@click.argument('data_json')
@click.pass_context
def raw(ctx, data_json):
send(data_json, ctx.obj['ws'])
@mkevent.command(help="Make generic chat message")
@click.argument('username')
@click.argument('text')
@click.option('-t', '--type', 'user_type', default=USER_TYPE.SYSTEM.name,
type=click.Choice([enum.name for enum in USER_TYPE]))
@click.pass_context
def msg(ctx, username, text, user_type=None):
user_type = USER_TYPE[user_type]
mockevent = Message(ctx.obj['via'], text, username, username, user_type)
send(mockevent.serialize(), ctx.obj['ws'])
@mkevent.command(help="Make Twitch Raid")
@click.argument('channel')
@click.option('-c', '--count', type=click.IntRange(1, None), default=1)
@click.pass_context
def raid(ctx, channel, count=None):
mockevent = Raid(ctx.obj['via'], channel, channel, count)
send(mockevent.serialize(), ctx.obj['ws'])
@mkevent.command(help="Make subscription")
@click.argument('user')
@click.option('-s', '--streak', type=click.IntRange(0, None), default=0)
@click.option('-g', '--gifted', multiple=True)
@click.pass_context
def sub(ctx, user, streak=None, gifted=None):
mockevent = Subscription(ctx.obj['via'], user, user, streak=streak, gifted_to=gifted)
send(mockevent.serialize(), ctx.obj['ws'])

View File

@ -58,6 +58,7 @@ class ConfigChangeHandler(FileSystemEventHandler):
@click.option('--show-time/--no-time', default=False, help="Show time in logs") @click.option('--show-time/--no-time', default=False, help="Show time in logs")
@click.option('--watch/--no-watch', default=True, help="Automatically reload on config changes") @click.option('--watch/--no-watch', default=True, help="Automatically reload on config changes")
def start(config_file, loglevel, show_time=False, watch=True, port=None, bind=None): def start(config_file, loglevel, show_time=False, watch=True, port=None, bind=None):
"""Start audiencekit server"""
# Set root logger details # Set root logger details
logger = logging.getLogger() logger = logging.getLogger()
logger.setLevel(loglevel) logger.setLevel(loglevel)
@ -70,7 +71,7 @@ def start(config_file, loglevel, show_time=False, watch=True, port=None, bind=No
logging.getLogger('websockets.client').setLevel(logging.INFO) logging.getLogger('websockets.client').setLevel(logging.INFO)
logging.getLogger('asyncio').setLevel(logging.INFO) logging.getLogger('asyncio').setLevel(logging.INFO)
logging.getLogger('urllib3.connectionpool').setLevel(logging.INFO) logging.getLogger('urllib3.connectionpool').setLevel(logging.INFO)
reload_event = Event() reload_event = Event()
if watch: if watch:
handler = ConfigChangeHandler(reload_event) handler = ConfigChangeHandler(reload_event)

88
cli/websocketutils.py Normal file
View File

@ -0,0 +1,88 @@
import json
import importlib
import dataclasses
import click
import websockets
from .group import cli
from events import Event
from utils import make_sync, get_subclasses
@make_sync
async def send(data, ws):
async with websockets.connect(ws) as websocket:
await websocket.send(data)
@click.pass_context
def mkevent_generic(ctx, *args, **kwargs):
mockevent = ctx.obj['event'](ctx.obj['via'], *args, **kwargs)
send(mockevent.serialize(), ctx.obj['ws'])
class EventCommandFactory(click.MultiCommand):
def list_commands(self, ctx):
event_classes = get_subclasses(Event)
return [cls.__name__ for cls in event_classes if cls._hidden != True]
def get_command(self, ctx, name):
target_event = next((cls for cls in get_subclasses(Event) if cls.__name__ == name), None)
ctx.obj['event'] = target_event
params = []
for field in dataclasses.fields(target_event):
# Skip over Event base fields
if field.name in ['id', 'via', 'raw', 'timestamp']:
continue
# Make argument from args, and options from kwargs
required = all(isinstance(default, dataclasses._MISSING_TYPE) for default in [field.default, field.default_factory])
if required:
param = click.Argument([field.name])
else:
param = click.Option(param_decls=[f'--{field.name}'])
params.append(param)
command = click.Command(name, callback=mkevent_generic, params=params, help=target_event.__doc__)
return command
@cli.group(name='ws')
@click.option('--target', '-t', default='ws://localhost:8080', help="Websocket bus of the running server")
@click.option('--chatmod', '-c', multiple=True, help="Load a chat module (makes its specific events accessible)")
@click.option('--pluginmod', '-p', multiple=True, help="Load a plugin module (makes its specific events accessible)")
@click.pass_context
def websocketutils(ctx, target, chatmod=[], pluginmod=[]):
"""Send events to a running server"""
ctx.ensure_object(dict)
ctx.obj['ws'] = target
try:
for module_name in chatmod:
importlib.import_module(f'.{module_name}', package='chats')
for module_name in pluginmod:
importlib.import_module(f'.{module_name}', package='plugins')
except ModuleNotFoundError as e:
option = 'chatmod' if e.name.startswith('chat') else 'pluginmod'
raise click.BadOptionUsage(option, f'Could not import requested module: {e.name}', ctx=ctx)
@websocketutils.command(cls=EventCommandFactory)
@click.option('--via', default="console")
@click.option('--id')
@click.option('--raw')
@click.option('--timestamp')
@click.pass_context
def mkevent(ctx, via, id, raw, timestamp):
"""Create event via CLI"""
ctx.obj['via'] = via
ctx.obj['id'] = id
ctx.obj['raw'] = raw
ctx.obj['timestamp'] = timestamp
@websocketutils.command()
@click.argument('data_json')
@click.pass_context
def raw(ctx, data_json):
"""Create event via JSON"""
send(data_json, ctx.obj['ws'])

View File

@ -6,6 +6,7 @@ import logging
import websockets import websockets
from events import Event from events import Event
from utils import get_subclasses
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -20,13 +21,7 @@ class WebsocketServerProcess(Process):
self._pipe, self._caller_pipe = Pipe() self._pipe, self._caller_pipe = Pipe()
self.clients = set() self.clients = set()
self.update_classes() self._event_classes = get_subclasses(Event)
def update_classes(self):
def all_subclasses(cls):
return set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in all_subclasses(c)])
self._event_classes = all_subclasses(Event)
@property @property
def message_pipe(self): def message_pipe(self):

View File

@ -6,6 +6,7 @@ from events import Event
@dataclass @dataclass
class Control(Event): class Control(Event):
"""Generic inter-bus communication"""
target: str target: str
data: dict = field(default_factory=dict) data: dict = field(default_factory=dict)

View File

@ -5,6 +5,7 @@ from events import Event
@dataclass @dataclass
class Delete(Event): class Delete(Event):
"""Inform clients to remove a specific event"""
target_id: int target_id: int
show_masked: bool = False show_masked: bool = False
reason: str = None reason: str = None

View File

@ -6,6 +6,8 @@ from dataclasses import dataclass, field, asdict
@dataclass @dataclass
class Event: class Event:
# Set to true in your subclass to disable CLI creation
_hidden = False
via: str via: str
id: int = field(default_factory=lambda: random.getrandbits(32), kw_only=True) id: int = field(default_factory=lambda: random.getrandbits(32), kw_only=True)
timestamp: float = field(default_factory=time.time, kw_only=True) timestamp: float = field(default_factory=time.time, kw_only=True)

View File

@ -5,6 +5,7 @@ from events import Event
@dataclass @dataclass
class Follow(Event): class Follow(Event):
"""User has signed up for go-live notifications"""
user_name: str user_name: str
user_id: str user_id: str

View File

@ -16,6 +16,7 @@ class USER_TYPE(str, Enum):
@dataclass @dataclass
class Message(Event): class Message(Event):
"""Chat message"""
text: str text: str
user_name: str user_name: str
user_id: str user_id: str
@ -42,8 +43,8 @@ class Message(Event):
@dataclass @dataclass
class SysMessage(Message): class SysMessage(Message):
"""A Message with user_type, user_name, and user_id set automatically (SYSTEM and Event.via respectfuly)""" """Message with user_type, user_name, and user_id set automatically (SYSTEM and Event.via respectfuly)"""
_hidden = True
user_name: str = field(init=False) user_name: str = field(init=False)
user_id: str = field(init=False) user_id: str = field(init=False)
user_type: USER_TYPE = USER_TYPE.SYSTEM user_type: USER_TYPE = USER_TYPE.SYSTEM

View File

@ -5,6 +5,7 @@ from events import Event
@dataclass @dataclass
class Subscription(Event): class Subscription(Event):
"""User has signed up for a re-ocurring donation"""
user_name: str user_name: str
user_id: str user_id: str
gifted_to: list = None gifted_to: list = None

View File

@ -1,2 +1,3 @@
from .NonBlockingWebsocket import NonBlockingWebsocket from .NonBlockingWebsocket import NonBlockingWebsocket
from .make_sync import make_sync from .make_sync import make_sync
from .get_subclasses import get_subclasses

4
utils/get_subclasses.py Normal file
View File

@ -0,0 +1,4 @@
def get_subclasses(cls):
"""Return a set of all subclasses (recursively) of a given class"""
return set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in get_subclasses(c)])