Compare commits

...

6 Commits

6 changed files with 115 additions and 88 deletions
+43 -27
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
import operator
@@ -22,9 +32,9 @@ __all__ = (
"ternary_term",
"run_function",
"cascade_term",
'ExecutorTerm',
'LateTerm',
'lazy_term',
"ExecutorTerm",
"LateTerm",
"lazy_term",
)
P = ParamSpec("P")
@@ -37,7 +47,7 @@ T2 = TypeVar("T2")
TermResult = TypeVar("TermResult")
class ExecutorTerm[TermResult]:
class ExecutorTerm(Generic[TermResult]):
# May add more here for initial parsing context
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:
return " ".join(args[argn:])
elif argn >= len(args):
return ''
return ""
else:
return args[argn]
@@ -158,33 +168,36 @@ async def cascade_term(ctx, *terms: ExecutorTerm):
return eval
return ""
@lazy_term
async def command_shell(ctx, component_term: ExecutorTerm):
if isinstance(component_term, ExecutorTerm):
result = await component_term(ctx)
else:
logger.warning(
f"command_shell LazyTerm stringifying non-term {component_term}"
)
logger.warning(f"command_shell LazyTerm stringifying non-term {component_term}")
result = component_term
return str(result)
@lazy_term
async def define_equal(ctx, var: str, value: ExecutorTerm):
ctx.this_scope[var] = await value.execute(ctx)
return None
@lazy_term
async def define_walrus(ctx, var: str, value: ExecutorTerm):
result = await value.execute(ctx)
ctx.this_scope[var] = result
return result
@lazy_term
async def define_term(ctx, var: str, value: ExecutorTerm):
ctx.this_scope[var] = value
return None
@lazy_term
async def run_function(ctx, var: ExecutorTerm, *args: ExecutorTerm):
varlue = await var.execute(ctx)
@@ -196,13 +209,14 @@ async def run_function(ctx, var: ExecutorTerm, *args: ExecutorTerm):
else:
return await varlue(ctx, *argues)
@lazy_term
async def get_var(ctx, root: str | ExecutorTerm, *attrs: str):
if isinstance(root, ExecutorTerm):
value = await root.execute(ctx)
name = [root.text or '']
name = [root.text or ""]
elif root not in ctx.scope:
return ' '.join((root, *attrs))
return " ".join((root, *attrs))
else:
value = ctx.scope[root]
name = [root]
@@ -213,39 +227,41 @@ async def get_var(ctx, root: str | ExecutorTerm, *attrs: str):
for attr in attrs:
try:
value = getattr(value, '_exec_attr_'+attr)
value = getattr(value, "_exec_attr_" + attr)
name.append(attr)
except AttributeError:
raise CommandValueError(f"{'.'.join(name)} has no attribute {attr}")
return value
@lazy_term
async def compare(ctx, term1: ExecutorTerm, op: str, term2: ExecutorTerm):
opmap = {
'==': operator.eq,
'!=': operator.ne,
'>': operator.gt,
'>=': operator.ge,
'<': operator.lt,
'<=': operator.le,
"==": operator.eq,
"!=": operator.ne,
">": operator.gt,
">=": operator.ge,
"<": operator.lt,
"<=": operator.le,
}
value1 = await term1.execute(ctx)
value2 = await term2.execute(ctx)
oppy = opmap[op]
return oppy(value1, value2)
@lazy_term
async def binop(ctx, op: str, *terms):
opmap = {
'||': operator.or_,
'&&': operator.and_,
'+': operator.add,
'-': operator.sub,
'*': operator.mul,
'**': operator.pow,
'/': operator.truediv,
'//': operator.floordiv,
'%': operator.mod,
"||": operator.or_,
"&&": operator.and_,
"+": operator.add,
"-": operator.sub,
"*": operator.mul,
"**": operator.pow,
"/": operator.truediv,
"//": operator.floordiv,
"%": operator.mod,
}
# Left associative
oppy = opmap[op]
+1 -2
View File
@@ -13,7 +13,7 @@ CREATE TABLE customcmds(
cooldown_bucket_dur INTEGER,
permlevel INTEGER,
maskperms BOOLEAN DEFAULT FALSE,
creatorid INTEGER NOT NULL REFERENCES profiles(profileid) ON DELETE CASCADE ON UPDATE CASCADE,
creatorid INTEGER NOT NULL REFERENCES user_profiles(profileid) ON DELETE CASCADE ON UPDATE CASCADE,
description TEXT,
docstring TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
@@ -52,7 +52,6 @@ SELECT
cmds.maskperms,
cmds.creatorid,
cmds.description,
cmds.description,
cmds.docstring,
cmds.created_at,
cmds._timestamp,
+12 -12
View File
@@ -1,11 +1,13 @@
import twitchio
from customcmd.client import ExecClient
from customcmd.data import CustomCmd, CustomCmdData
from meta import Bot
from ..customcmd.client import ExecClient
from ..customcmd.data import CustomCmd, CustomCmdData
from .context import CustomContext, CustomCmdUser
class TwitchCmdClient(ExecClient):
def __init__(self, bot: Bot, data: CustomCmdData):
super().__init__()
@@ -22,12 +24,12 @@ class TwitchCmdClient(ExecClient):
if message.reply:
# Strip the hidden mention prefix from the start of the message
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 prefix := next((p for p in prefixes if content.startswith(p)), None):
# Strip the valid prefix
content = content[len(prefix):].strip()
content = content[len(prefix) :].strip()
if not content:
# Exit early, no command
return
@@ -52,21 +54,19 @@ class TwitchCmdClient(ExecClient):
alias=maybe_command,
used_prefix=prefix,
message=message,
content=message.text,
initial_globals={
'author': CustomCmdUser(message.chatter),
'channel': CustomCmdUser(message.broadcaster),
}
"author": CustomCmdUser(message.chatter),
"channel": CustomCmdUser(message.broadcaster),
},
)
result = await self.run_command(ctx, cmd.response)
if result:
await message.broadcaster.send_message(
result,
sender=self.bot.user
)
await message.broadcaster.send_message(result, sender=self.bot.user)
async def get_prefixes(self, message: twitchio.ChatMessage):
# TODO:
return ('!',)
return ("!",)
async def get_commands_in(self, communityid: int) -> dict[str, CustomCmd]:
"""
+15 -9
View File
@@ -4,19 +4,20 @@ import twitchio
from twitchio import PartialUser
from twitchio.ext import commands as cmds
from customcmd.parser.context import ExecContext
from meta import Bot
from twitch.client import TwitchCmdClient
from utils.lib import utc_now
from . import logger
from ..customcmd.data import CustomCmd, CustomCmdData
from ..customcmd.client import ExecClient
from ..customcmd.parser import ExecContext
from . import logger
from .client import TwitchCmdClient
# TODO: Make an alphacroc or croccyalpha client for testing
# TODO: Fix the tracker not actually guarding the commands properly
# TODO: Fix suffix of result not printing
# TODO When command produces None don't stringify to 'None'
class CustomCmdComponent(cmds.Component):
@@ -46,7 +47,7 @@ class CustomCmdComponent(cmds.Component):
# TODO: Probably attempt to list commands or something here.
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
async def cmd_add(self, ctx: cmds.Context, name: str, *, response: str):
comm = await self.bot.profiles.fetch_community(ctx.broadcaster)
@@ -60,11 +61,16 @@ class CustomCmdComponent(cmds.Component):
# Make sure the command parses (can't guarantee it runs, but at least it can parse)
try:
await self.cmdclient.run_command(ExecContext(args=[], content=''), response)
await self.cmdclient.run_command(
ExecContext(alias="testing", args=[], content=""), response, dryrun=True
)
except Exception:
# TODO: Nicer errors for pinpointing problem
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
)
return
# If everything is good, add it to the table
# TODO: Again, permission resolution
@@ -79,7 +85,7 @@ class CustomCmdComponent(cmds.Component):
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()
async def cmd_del(self, ctx: cmds.Context, name: str):
# Lookup the name to make sure a command does exist
+15 -14
View File
@@ -1,11 +1,11 @@
import twitchio
from customcmd.client import ExecClient
from customcmd.data import CustomCmd, CustomCmdData
from meta import Bot
from utils.lib import utc_now, strfdur
from utils.lib import utc_now, strfdelta
from ..customcmd.parser import ExecContext
from ..customcmd.client import ExecClient
from ..customcmd.data import CustomCmd, CustomCmdData
from .ctxvars import stuff
@@ -25,22 +25,21 @@ class CustomCmdUser:
return self.user.display_name or self.user.name or self.user.id
async def _exec_attr_whisper(self, ctx, message: str):
await self.user.send_whisper(
to_user=self.user,
message=message
)
await ctx.bot.user.send_whisper(to_user=self.user, message=message)
async def _exec_attr_followage(self, ctx, channel: twitchio.PartialUser):
async def _exec_attr_followage(self, ctx, channel: "CustomCmdUser"):
followed_at = None
query = await channel.fetch_followers(user=self.user)
query = await channel.user.fetch_followers(
user=self.user, token_for=ctx.bot.user
)
async for follower in query.followers:
followed_at = follower.followed_at
if followed_at:
durstr = strfdur(utc_now() - followed_at)
resp = f"{self.user.mention} has been following {channel.mention} for {durstr}"
durstr = strfdelta(utc_now() - followed_at)
return durstr
else:
return f"{self.user.mention} is not following {channel.mention}"
return None
@classmethod
def from_user(cls, user: twitchio.PartialUser):
@@ -51,14 +50,16 @@ class CustomContext(ExecContext):
"""
CustomCommand ExecContext instantiated from a Twitch message
"""
def __init__(
self,
bot: Bot,
message: twitchio.ChatMessage,
alias: str,
used_prefix: str,
**kwargs):
super().__init__(**kwargs)
**kwargs,
):
super().__init__(alias=alias, **kwargs)
self.bot = bot
self.message = message
self.alias = alias
+7 -2
View File
@@ -4,23 +4,28 @@ from typing import Optional
stuff = {}
def register_var(name: str | None = None):
def wrapper(coro):
varname = name or coro.__name__
stuff[varname] = coro
return coro
return wrapper
@register_var('random')
@register_var("random")
async def random_func(ctx, a: int, b: Optional[int] = None):
a = int(a)
b = int(b) if b is not None else None
if b is not None:
low, high = a, b
else:
low, high = 0, a
return random.randint(low, high)
@register_var('user')
@register_var("user")
async def user_func(ctx, userstr: int | str):
"""
Attempt to turn the given userstr into a user.