Compare commits

..

2 Commits

Author SHA1 Message Date
conatum 0d4f9b9a9a fix(twitch): Fix relative imports 2026-05-31 06:32:31 +10:00
conatum 733f3e3ae6 tweak: Use Generic to support 3.11 2026-05-31 06:31:46 +10:00
4 changed files with 92 additions and 76 deletions
+46 -30
View File
@@ -1,4 +1,14 @@
from typing import Any, Awaitable, Callable, Concatenate, ParamSpec, TypeVar, Union, TYPE_CHECKING from typing import (
Any,
Generic,
Awaitable,
Callable,
Concatenate,
ParamSpec,
TypeVar,
Union,
TYPE_CHECKING,
)
from functools import wraps, reduce from functools import wraps, reduce
import operator import operator
@@ -22,9 +32,9 @@ __all__ = (
"ternary_term", "ternary_term",
"run_function", "run_function",
"cascade_term", "cascade_term",
'ExecutorTerm', "ExecutorTerm",
'LateTerm', "LateTerm",
'lazy_term', "lazy_term",
) )
P = ParamSpec("P") P = ParamSpec("P")
@@ -37,7 +47,7 @@ T2 = TypeVar("T2")
TermResult = TypeVar("TermResult") TermResult = TypeVar("TermResult")
class ExecutorTerm[TermResult]: class ExecutorTerm(Generic[TermResult]):
# May add more here for initial parsing context # May add more here for initial parsing context
def __init__(self, *args, node_text: str | None = None, **kwargs): def __init__(self, *args, node_text: str | None = None, **kwargs):
@@ -145,7 +155,7 @@ async def cmdarg_term(ctx, argn: int, greedy: bool) -> str:
if greedy: if greedy:
return " ".join(args[argn:]) return " ".join(args[argn:])
elif argn >= len(args): elif argn >= len(args):
return '' return ""
else: else:
return args[argn] return args[argn]
@@ -158,33 +168,36 @@ async def cascade_term(ctx, *terms: ExecutorTerm):
return eval return eval
return "" return ""
@lazy_term @lazy_term
async def command_shell(ctx, component_term: ExecutorTerm): async def command_shell(ctx, component_term: ExecutorTerm):
if isinstance(component_term, ExecutorTerm): if isinstance(component_term, ExecutorTerm):
result = await component_term(ctx) result = await component_term(ctx)
else: else:
logger.warning( logger.warning(f"command_shell LazyTerm stringifying non-term {component_term}")
f"command_shell LazyTerm stringifying non-term {component_term}"
)
result = component_term result = component_term
return str(result) return str(result)
@lazy_term @lazy_term
async def define_equal(ctx, var: str, value: ExecutorTerm): async def define_equal(ctx, var: str, value: ExecutorTerm):
ctx.this_scope[var] = await value.execute(ctx) ctx.this_scope[var] = await value.execute(ctx)
return None return None
@lazy_term @lazy_term
async def define_walrus(ctx, var: str, value: ExecutorTerm): async def define_walrus(ctx, var: str, value: ExecutorTerm):
result = await value.execute(ctx) result = await value.execute(ctx)
ctx.this_scope[var] = result ctx.this_scope[var] = result
return result return result
@lazy_term
@lazy_term
async def define_term(ctx, var: str, value: ExecutorTerm): async def define_term(ctx, var: str, value: ExecutorTerm):
ctx.this_scope[var] = value ctx.this_scope[var] = value
return None return None
@lazy_term @lazy_term
async def run_function(ctx, var: ExecutorTerm, *args: ExecutorTerm): async def run_function(ctx, var: ExecutorTerm, *args: ExecutorTerm):
varlue = await var.execute(ctx) varlue = await var.execute(ctx)
@@ -196,13 +209,14 @@ async def run_function(ctx, var: ExecutorTerm, *args: ExecutorTerm):
else: else:
return await varlue(ctx, *argues) return await varlue(ctx, *argues)
@lazy_term
@lazy_term
async def get_var(ctx, root: str | ExecutorTerm, *attrs: str): async def get_var(ctx, root: str | ExecutorTerm, *attrs: str):
if isinstance(root, ExecutorTerm): if isinstance(root, ExecutorTerm):
value = await root.execute(ctx) value = await root.execute(ctx)
name = [root.text or ''] name = [root.text or ""]
elif root not in ctx.scope: elif root not in ctx.scope:
return ' '.join((root, *attrs)) return " ".join((root, *attrs))
else: else:
value = ctx.scope[root] value = ctx.scope[root]
name = [root] name = [root]
@@ -213,39 +227,41 @@ async def get_var(ctx, root: str | ExecutorTerm, *attrs: str):
for attr in attrs: for attr in attrs:
try: try:
value = getattr(value, '_exec_attr_'+attr) value = getattr(value, "_exec_attr_" + attr)
name.append(attr) name.append(attr)
except AttributeError: except AttributeError:
raise CommandValueError(f"{'.'.join(name)} has no attribute {attr}") raise CommandValueError(f"{'.'.join(name)} has no attribute {attr}")
return value return value
@lazy_term
@lazy_term
async def compare(ctx, term1: ExecutorTerm, op: str, term2: ExecutorTerm): async def compare(ctx, term1: ExecutorTerm, op: str, term2: ExecutorTerm):
opmap = { opmap = {
'==': operator.eq, "==": operator.eq,
'!=': operator.ne, "!=": operator.ne,
'>': operator.gt, ">": operator.gt,
'>=': operator.ge, ">=": operator.ge,
'<': operator.lt, "<": operator.lt,
'<=': operator.le, "<=": operator.le,
} }
value1 = await term1.execute(ctx) value1 = await term1.execute(ctx)
value2 = await term2.execute(ctx) value2 = await term2.execute(ctx)
oppy = opmap[op] oppy = opmap[op]
return oppy(value1, value2) return oppy(value1, value2)
@lazy_term @lazy_term
async def binop(ctx, op: str, *terms): async def binop(ctx, op: str, *terms):
opmap = { opmap = {
'||': operator.or_, "||": operator.or_,
'&&': operator.and_, "&&": operator.and_,
'+': operator.add, "+": operator.add,
'-': operator.sub, "-": operator.sub,
'*': operator.mul, "*": operator.mul,
'**': operator.pow, "**": operator.pow,
'/': operator.truediv, "/": operator.truediv,
'//': operator.floordiv, "//": operator.floordiv,
'%': operator.mod, "%": operator.mod,
} }
# Left associative # Left associative
oppy = opmap[op] oppy = opmap[op]
+14 -15
View File
@@ -1,11 +1,13 @@
import twitchio import twitchio
from customcmd.client import ExecClient
from customcmd.data import CustomCmd, CustomCmdData
from meta import Bot from meta import Bot
from ..customcmd.client import ExecClient
from ..customcmd.data import CustomCmd, CustomCmdData
from .context import CustomContext, CustomCmdUser from .context import CustomContext, CustomCmdUser
class TwitchCmdClient(ExecClient): class TwitchCmdClient(ExecClient):
def __init__(self, bot: Bot, data: CustomCmdData): def __init__(self, bot: Bot, data: CustomCmdData):
super().__init__() super().__init__()
@@ -22,12 +24,12 @@ class TwitchCmdClient(ExecClient):
if message.reply: if message.reply:
# Strip the hidden mention prefix from the start of the message # Strip the hidden mention prefix from the start of the message
prefix = message.reply.parent_user.mention prefix = message.reply.parent_user.mention
content = content[len(prefix) + 1:] content = content[len(prefix) + 1 :]
# If we start with a valid prefix, parse out a command name # If we start with a valid prefix, parse out a command name
if prefix := next((p for p in prefixes if content.startswith(p)), None): if prefix := next((p for p in prefixes if content.startswith(p)), None):
# Strip the valid prefix # Strip the valid prefix
content = content[len(prefix):].strip() content = content[len(prefix) :].strip()
if not content: if not content:
# Exit early, no command # Exit early, no command
return return
@@ -45,7 +47,7 @@ class TwitchCmdClient(ExecClient):
# Read the arguments, build the context, run the command. # Read the arguments, build the context, run the command.
# TODO: Check the permissions and log etc. # TODO: Check the permissions and log etc.
# TODO: Exception handling, as usual # TODO: Exception handling, as usual
# TODO: Nice string reprs # TODO: Nice string reprs
ctx = CustomContext( ctx = CustomContext(
bot=self.bot, bot=self.bot,
args=args, args=args,
@@ -53,20 +55,17 @@ class TwitchCmdClient(ExecClient):
used_prefix=prefix, used_prefix=prefix,
message=message, message=message,
initial_globals={ initial_globals={
'author': CustomCmdUser(message.chatter), "author": CustomCmdUser(message.chatter),
'channel': CustomCmdUser(message.broadcaster), "channel": CustomCmdUser(message.broadcaster),
} },
) )
result = await self.run_command(ctx, cmd.response) result = await self.run_command(ctx, cmd.response)
if result: if result:
await message.broadcaster.send_message( await message.broadcaster.send_message(result, sender=self.bot.user)
result,
sender=self.bot.user
)
async def get_prefixes(self, message: twitchio.ChatMessage): async def get_prefixes(self, message: twitchio.ChatMessage):
# TODO: # TODO:
return ('!',) return ("!",)
async def get_commands_in(self, communityid: int) -> dict[str, CustomCmd]: async def get_commands_in(self, communityid: int) -> dict[str, CustomCmd]:
""" """
+15 -15
View File
@@ -2,23 +2,21 @@ from typing import Optional
import twitchio import twitchio
from twitchio import PartialUser from twitchio import PartialUser
from twitchio.ext import commands as cmds from twitchio.ext import commands as cmds
from customcmd.parser.context import ExecContext
from meta import Bot from meta import Bot
from twitch.client import TwitchCmdClient from twitch.client import TwitchCmdClient
from utils.lib import utc_now from utils.lib import utc_now
from . import logger from . import logger
from ..customcmd.data import CustomCmd, CustomCmdData from ..customcmd.data import CustomCmd, CustomCmdData
from ..customcmd.client import ExecClient from ..customcmd.client import ExecClient
from ..customcmd.parser import ExecContext
# TODO: Make an alphacroc or croccyalpha client for testing # TODO: Make an alphacroc or croccyalpha client for testing
# TODO: Fix the tracker not actually guarding the commands properly # TODO: Fix the tracker not actually guarding the commands properly
class CustomCmdComponent(cmds.Component): class CustomCmdComponent(cmds.Component):
def __init__(self, bot: Bot): def __init__(self, bot: Bot):
self.bot = bot self.bot = bot
@@ -31,11 +29,11 @@ class CustomCmdComponent(cmds.Component):
await self.bot.version_check(*self.data.VERSION) await self.bot.version_check(*self.data.VERSION)
async def component_teardown(self): async def component_teardown(self):
pass pass
# ----- Methods ----- # ----- Methods -----
# ----- Event handlers ----- # ----- Event handlers -----
@cmds.Component.listener() @cmds.Component.listener()
async def event_message(self, payload: twitchio.ChatMessage): async def event_message(self, payload: twitchio.ChatMessage):
await self.cmdclient.process_message(payload) await self.cmdclient.process_message(payload)
@@ -44,9 +42,9 @@ class CustomCmdComponent(cmds.Component):
@cmds.group(name="commands") @cmds.group(name="commands")
async def cmd_grp(self, ctx: cmds.Context): async def cmd_grp(self, ctx: cmds.Context):
# TODO: Probably attempt to list commands or something here. # TODO: Probably attempt to list commands or something here.
pass pass
@cmd_grp.command(name="add", aliases=('create', 'new')) @cmd_grp.command(name="add", aliases=("create", "new"))
@cmds.is_broadcaster() # TODO: Better command creation perm checks @cmds.is_broadcaster() # TODO: Better command creation perm checks
async def cmd_add(self, ctx: cmds.Context, name: str, *, response: str): async def cmd_add(self, ctx: cmds.Context, name: str, *, response: str):
comm = await self.bot.profiles.fetch_community(ctx.broadcaster) comm = await self.bot.profiles.fetch_community(ctx.broadcaster)
@@ -60,11 +58,13 @@ class CustomCmdComponent(cmds.Component):
# Make sure the command parses (can't guarantee it runs, but at least it can parse) # Make sure the command parses (can't guarantee it runs, but at least it can parse)
try: try:
await self.cmdclient.run_command(ExecContext(args=[], content=''), response) await self.cmdclient.run_command(ExecContext(args=[], content=""), response)
except Exception: except Exception:
# TODO: Nicer errors for pinpointing problem # TODO: Nicer errors for pinpointing problem
await ctx.reply(f"Sorry, could not parse your command!") await ctx.reply(f"Sorry, could not parse your command!")
logger.info(f"Failed to parse attempted customcmd '{response}'", exc_info=True) logger.info(
f"Failed to parse attempted customcmd '{response}'", exc_info=True
)
# If everything is good, add it to the table # If everything is good, add it to the table
# TODO: Again, permission resolution # TODO: Again, permission resolution
@@ -79,10 +79,10 @@ class CustomCmdComponent(cmds.Component):
await ctx.reply(f"Created a new command !{name}") await ctx.reply(f"Created a new command !{name}")
@cmd_grp.command(name="del", aliases=('delete', 'remove', 'rm')) @cmd_grp.command(name="del", aliases=("delete", "remove", "rm"))
@cmds.is_broadcaster() @cmds.is_broadcaster()
async def cmd_del(self, ctx: cmds.Context, name: str): async def cmd_del(self, ctx: cmds.Context, name: str):
# Lookup the name to make sure a command does exist # Lookup the name to make sure a command does exist
# Delete the command (again, consider permissions later) # Delete the command (again, consider permissions later)
# Confirm with user. # Confirm with user.
comm = await self.bot.profiles.fetch_community(ctx.broadcaster) comm = await self.bot.profiles.fetch_community(ctx.broadcaster)
+17 -16
View File
@@ -1,11 +1,11 @@
import twitchio import twitchio
from customcmd.client import ExecClient
from customcmd.data import CustomCmd, CustomCmdData
from meta import Bot from meta import Bot
from utils.lib import utc_now, strfdur from utils.lib import utc_now, strfdur
from ..customcmd.parser import ExecContext from ..customcmd.parser import ExecContext
from ..customcmd.client import ExecClient
from ..customcmd.data import CustomCmd, CustomCmdData
from .ctxvars import stuff from .ctxvars import stuff
@@ -25,10 +25,7 @@ class CustomCmdUser:
return self.user.display_name or self.user.name or self.user.id return self.user.display_name or self.user.name or self.user.id
async def _exec_attr_whisper(self, ctx, message: str): async def _exec_attr_whisper(self, ctx, message: str):
await self.user.send_whisper( await self.user.send_whisper(to_user=self.user, message=message)
to_user=self.user,
message=message
)
async def _exec_attr_followage(self, ctx, channel: twitchio.PartialUser): async def _exec_attr_followage(self, ctx, channel: twitchio.PartialUser):
followed_at = None followed_at = None
@@ -38,7 +35,9 @@ class CustomCmdUser:
if followed_at: if followed_at:
durstr = strfdur(utc_now() - followed_at) durstr = strfdur(utc_now() - followed_at)
resp = f"{self.user.mention} has been following {channel.mention} for {durstr}" resp = (
f"{self.user.mention} has been following {channel.mention} for {durstr}"
)
else: else:
return f"{self.user.mention} is not following {channel.mention}" return f"{self.user.mention} is not following {channel.mention}"
@@ -51,18 +50,20 @@ class CustomContext(ExecContext):
""" """
CustomCommand ExecContext instantiated from a Twitch message CustomCommand ExecContext instantiated from a Twitch message
""" """
def __init__( def __init__(
self, self,
bot: Bot, bot: Bot,
message: twitchio.ChatMessage, message: twitchio.ChatMessage,
alias: str, alias: str,
used_prefix: str, used_prefix: str,
**kwargs): **kwargs,
):
super().__init__(**kwargs) super().__init__(**kwargs)
self.bot = bot self.bot = bot
self.message = message self.message = message
self.alias = alias self.alias = alias
self.used_prefix = used_prefix self.used_prefix = used_prefix
self.enter_scope(stuff) self.enter_scope(stuff)
# TODO: Need a nice way to define some global functions. Registry pattern? # TODO: Need a nice way to define some global functions. Registry pattern?