generated from HoloTech/twitch-bot-template
Compare commits
4 Commits
1f60610947
...
c114f1a54a
| Author | SHA1 | Date | |
|---|---|---|---|
| c114f1a54a | |||
| 6667dc7263 | |||
| aba34799e1 | |||
| 68ce8139a9 |
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -1,3 +1,12 @@
|
||||
[submodule "src/data"]
|
||||
path = src/data
|
||||
url = https://git.thewisewolf.dev/HoloTech/psqlmapper.git
|
||||
[submodule "src/modules/subathon"]
|
||||
path = src/modules/subathon
|
||||
url = https://git.thewisewolf.dev/HoloTech/twitch-subathon-plugin.git
|
||||
[submodule "src/modules/tracker"]
|
||||
path = src/modules/tracker
|
||||
url = https://git.thewisewolf.dev/HoloTech/twitch-eventtracker-plugin.git
|
||||
[submodule "src/modules/profiles"]
|
||||
path = src/modules/profiles
|
||||
url = https://git.thewisewolf.dev/HoloTech/profiles-plugin.git
|
||||
|
||||
@@ -2,3 +2,4 @@ twitchio
|
||||
psycopg[pool]
|
||||
cachetools
|
||||
discord.py
|
||||
websockets
|
||||
|
||||
@@ -4,7 +4,7 @@ import websockets
|
||||
|
||||
from twitchio.web import AiohttpAdapter
|
||||
|
||||
from meta import Bot, conf, setup_main_logger, args
|
||||
from meta import Bot, conf, setup_main_logger, args, sockets
|
||||
from data import Database
|
||||
|
||||
from modules import twitch_setup
|
||||
@@ -30,6 +30,7 @@ async def main():
|
||||
setup=twitch_setup,
|
||||
)
|
||||
|
||||
async with websockets.serve(sockets.root_handler, '', conf.wserver.getint('port')):
|
||||
try:
|
||||
await bot.start()
|
||||
finally:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from .args import args
|
||||
from .bot import Bot
|
||||
from .context import Context
|
||||
from .config import Conf, conf
|
||||
from .logger import setup_main_logger, log_context, log_action_stack, log_app, set_logging_context, logging_context, with_log_ctx, persist_task
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, overload
|
||||
|
||||
from twitchio.authentication import UserTokenPayload
|
||||
from twitchio.ext import commands
|
||||
@@ -10,6 +10,10 @@ from botdata import BotData, UserAuth, BotChannel, VersionHistory
|
||||
from constants import BOTUSER_SCOPES, CHANNEL_SCOPES, SCHEMA_VERSIONS
|
||||
|
||||
from .config import Conf
|
||||
from .context import Context
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from modules.profiles.profiles.twitch.component import ProfilesComponent
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -24,6 +28,7 @@ class Bot(commands.Bot):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Whether we should do eventsub via webhooks or websockets
|
||||
if config.bot.get('eventsub_secret', None):
|
||||
self.using_webhooks = True
|
||||
else:
|
||||
@@ -36,6 +41,28 @@ class Bot(commands.Bot):
|
||||
|
||||
self.joined: dict[str, BotChannel] = {}
|
||||
|
||||
# Make the type checker happy about fetching components by name
|
||||
# TODO: Move to stubs
|
||||
|
||||
@property
|
||||
def profiles(self):
|
||||
return self.get_component('ProfilesComponent')
|
||||
|
||||
@overload
|
||||
def get_component(self, name: Literal['ProfilesComponent']) -> 'ProfilesComponent':
|
||||
...
|
||||
|
||||
@overload
|
||||
def get_component(self, name: str) -> Optional[commands.Component]:
|
||||
...
|
||||
|
||||
def get_component(self, name: str) -> Optional[commands.Component]:
|
||||
return super().get_component(name)
|
||||
|
||||
def get_context(self, payload, *, cls: Any = None) -> Context:
|
||||
cls = cls or Context
|
||||
return cls(payload, bot=self)
|
||||
|
||||
async def event_ready(self):
|
||||
# logger.info(f"Logged in as {self.nick}. User id is {self.user_id}")
|
||||
logger.info("Logged in as %s", self.bot_id)
|
||||
|
||||
4
src/meta/context.py
Normal file
4
src/meta/context.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from twitchio.ext import commands as cmds
|
||||
|
||||
class Context(cmds.Context):
|
||||
...
|
||||
68
src/meta/sockets.py
Normal file
68
src/meta/sockets.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from abc import ABC
|
||||
from collections import defaultdict
|
||||
import json
|
||||
from typing import Any
|
||||
import logging
|
||||
|
||||
import websockets
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class Channel(ABC):
|
||||
"""
|
||||
A channel is a stateful connection handler for a group of connected websockets.
|
||||
"""
|
||||
name = "Root Channel"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.connections = set()
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
return not self.connections
|
||||
|
||||
async def on_connection(self, websocket: websockets.WebSocketServerProtocol, event: dict[str, Any]):
|
||||
logger.info(f"Channel '{self.name}' attached new connection {websocket=} {event=}")
|
||||
self.connections.add(websocket)
|
||||
|
||||
async def del_connection(self, websocket: websockets.WebSocketServerProtocol):
|
||||
logger.info(f"Channel '{self.name}' dropped connection {websocket=}")
|
||||
self.connections.discard(websocket)
|
||||
|
||||
async def handle_message(self, websocket: websockets.WebSocketServerProtocol, message):
|
||||
raise NotImplementedError
|
||||
|
||||
async def send_event(self, event, websocket=None):
|
||||
message = json.dumps(event)
|
||||
if not websocket:
|
||||
for ws in self.connections:
|
||||
await ws.send(message)
|
||||
else:
|
||||
await websocket.send(message)
|
||||
|
||||
channels = {}
|
||||
|
||||
def register_channel(name, channel: Channel):
|
||||
channels[name] = channel
|
||||
|
||||
|
||||
async def root_handler(websocket: websockets.WebSocketServerProtocol):
|
||||
message = await websocket.recv()
|
||||
event = json.loads(message)
|
||||
|
||||
if event.get('type', None) != 'init':
|
||||
raise ValueError("Received Websocket connection with no init.")
|
||||
|
||||
if (channel_name := event.get('channel', None)) not in channels:
|
||||
raise ValueError(f"Received Init for unhandled channel {channel_name=}")
|
||||
channel = channels[channel_name]
|
||||
|
||||
try:
|
||||
await channel.on_connection(websocket, event)
|
||||
async for message in websocket:
|
||||
await channel.handle_message(websocket, message)
|
||||
finally:
|
||||
await channel.del_connection(websocket)
|
||||
@@ -6,6 +6,7 @@ if TYPE_CHECKING:
|
||||
|
||||
async def twitch_setup(bot: 'Bot'):
|
||||
# Import and run setup methods from each module
|
||||
# from . import module
|
||||
# await module.twitch_setup(bot)
|
||||
pass
|
||||
from . import profiles, tracker, subathon
|
||||
await profiles.twitch_setup(bot)
|
||||
await tracker.twitch_setup(bot)
|
||||
await subathon.twitch_setup(bot)
|
||||
|
||||
1
src/modules/profiles
Submodule
1
src/modules/profiles
Submodule
Submodule src/modules/profiles added at 543a65b7fb
1
src/modules/subathon
Submodule
1
src/modules/subathon
Submodule
Submodule src/modules/subathon added at 0ea90b5cd2
1
src/modules/tracker
Submodule
1
src/modules/tracker
Submodule
Submodule src/modules/tracker added at 094b1e3c57
Reference in New Issue
Block a user