Compare commits
2 Commits
main
...
feat/mkeve
Author | SHA1 | Date |
---|---|---|
Derek | a0487033af | |
Derek | 56f57f22b6 |
|
@ -14,7 +14,7 @@ class GracefulShutdownException(Exception):
|
||||||
|
|
||||||
|
|
||||||
class ShutdownRequest(Event):
|
class ShutdownRequest(Event):
|
||||||
pass
|
_hidden = True
|
||||||
|
|
||||||
|
|
||||||
class ChatProcess(Process, ABC):
|
class ChatProcess(Process, ABC):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'])
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'])
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)])
|
Loading…
Reference in New Issue