diff --git a/TestBot.py b/TestBot.py index 2ebdca8..be41127 100644 --- a/TestBot.py +++ b/TestBot.py @@ -1,41 +1,48 @@ -import asyncio import yaboli from yaboli.utils import * #class TestBot(Bot): -class TestBot(yaboli.Controller): +class TestBot(yaboli.Bot): def __init__(self, nick): super().__init__(nick=nick) + + self.register_callback("tree", self.command_tree, specific=False) - async def on_send(self, message): - if message.content == "!spawnevil": - bot = TestBot("TestSpawn") - task, reason = await bot.connect("test") - second = await self.room.send("We have " + ("a" if task else "no") + " task. Reason: " + reason, message.message_id) - if task: - await bot.stop() - await self.room.send("Stopped." if task.done() else "Still running (!)", second.message_id) + #async def on_send(self, message): + #if message.content == "!spawnevil": + #bot = TestBot("TestSpawn") + #task, reason = await bot.connect("test") + #second = await self.room.send("We have " + ("a" if task else "no") + " task. Reason: " + reason, message.message_id) + #if task: + #await bot.stop() + #await self.room.send("Stopped." if task.done() else "Still running (!)", second.message_id) - await self.room.send("All's over now.", message.message_id) + #await self.room.send("All's over now.", message.message_id) - elif message.content == "!tree": - messages = [message] + #elif message.content == "!tree": + #messages = [message] + #newmessages = [] + #for i in range(2): + #for m in messages: + #for j in range(2): + #newm = await self.room.send(f"{m.content}.{j}", m.message_id) + #newmessages.append(newm) + #messages = newmessages + #newmessages = [] + + async def command_tree(self, message, args): + messages = [message] + newmessages = [] + for i in range(2): + for m in messages: + for j in range(2): + newm = await self.room.send(f"{message.content}.{j}", m.message_id) + newmessages.append(newm) + messages = newmessages newmessages = [] - for i in range(2): - for m in messages: - for j in range(2): - newm = await self.room.send(f"{m.content}.{j}", m.message_id) - newmessages.append(newm) - messages = newmessages - newmessages = [] - -async def run_bot(): - bot = TestBot("TestSummoner") - task, reason = await bot.connect("test") - if task: - await task if __name__ == "__main__": - asyncio.get_event_loop().run_until_complete(run_bot()) + bot = TestBot("TestSummoner") + run_controller(bot, "test") diff --git a/yaboli/__init__.py b/yaboli/__init__.py index a0717a9..5e86afe 100644 --- a/yaboli/__init__.py +++ b/yaboli/__init__.py @@ -5,9 +5,10 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) +from .bot import * from .connection import * -from .room import * from .controller import * +from .room import * from .utils import * __all__ = connection.__all__ + room.__all__ + controller.__all__ + utils.__all__ diff --git a/yaboli/bot.py b/yaboli/bot.py new file mode 100644 index 0000000..4b787ef --- /dev/null +++ b/yaboli/bot.py @@ -0,0 +1,138 @@ +import asyncio +import logging +import re +from .callbacks import * +from .controller import * + +logger = logging.getLogger(__name__) +__all__ = ["Bot"] + + + +class Bot(Controller): + # ^ and $ not needed since we're doing a re.fullmatch + SPECIFIC_RE = r"!(\S+)\s+@(\S+)([\S\s]*)" + GENERIC_RE = r"!(\S+)([\S\s]*)" + + def __init__(self, nick): + super().__init__(nick) + + self._callbacks = Callbacks() + self.register_default_callbacks() + + def register_callback(self, event, callback, specific=True): + self._callbacks.add((event, specific), callback) + + async def on_send(self, message): + parsed = self.parse_message(message.content) + if not parsed: + return + command, args = parsed + + # general callback (specific set to False) + general = asyncio.ensure_future( + self._callbacks.call((command, False), message, args) + ) + + if len(args) > 0: + mention = args[0] + args = args[1:] + if mention[:1] == "@" and similar(mention[1:], self.nick): + # specific callback (specific set to True) + await self._callbacks.call((command, True), message, args) + + await general + + def parse_message(self, content): + """ + (command, args) = parse_message(content) + + Returns None, not a (None, None) tuple, when message could not be parsed + """ + + match = re.fullmatch(self.GENERIC_RE, content) + if not match: + return None + + command = match.group(1) + argstr = match.group(2) + args = self.parse_args(argstr) + + return command, args + + def parse_args(self, text): + """ + Use single- and double-quotes bash-style to include whitespace in arguments. + A backslash always escapes the next character. + Any non-escaped whitespace separates arguments. + + Returns a list of arguments. + Deals with unclosed quotes and backslashes without crashing. + """ + + escape = False + quote = None + args = [] + arg = "" + + for character in text: + if escape: + arg += character + escape = False + elif character == "\\": + escape = True + elif quote: + if character == quote: + quote = None + else: + arg += character + elif character in "'\"": + quote = character + elif character.isspace() and len(arg) > 0: + args.append(arg) + arg = "" + else: + arg += character + + #if escape or quote: + #return None # syntax error + + if len(arg) > 0: + args.append(arg) + + return args + + def parse_flags(self, arglist): + flags = "" + args = [] + kwargs = {} + + for arg in arglist: + # kwargs (--abc, --foo=bar) + if arg[:2] == "--": + arg = arg[2:] + if "=" in arg: + s = arg.split("=", maxsplit=1) + kwargs[s[0]] = s[1] + else: + kwargs[arg] = None + # flags (-x, -rw) + elif arg[:1] == "-": + arg = arg[1:] + flags += arg + # args (normal arguments) + else: + args.append(arg) + + return flags, args, kwargs + + + + # BOTRULEZ COMMANDS + + def register_default_callbacks(self): + self.register_callback("ping", self.command_ping) + self.register_callback("ping", self.command_ping, specific=False) + + async def command_ping(self, message, args): + await self.room.send("Pong!", message.message_id) diff --git a/yaboli/callbacks.py b/yaboli/callbacks.py index 04c36b8..71902d5 100644 --- a/yaboli/callbacks.py +++ b/yaboli/callbacks.py @@ -1,30 +1,27 @@ +import asyncio + +__all__ = ["Callbacks"] + + + class Callbacks(): """ - Manage callbacks + Manage callbacks asynchronously """ def __init__(self): self._callbacks = {} - def add(self, event, callback, *args, **kwargs): + def add(self, event, callback): """ - add(event, callback, *args, **kwargs) -> None + add(event, callback) -> None Add a function to be called on event. - The function will be called with *args and **kwargs. - Certain arguments might be added, depending on the event. """ if not event in self._callbacks: self._callbacks[event] = [] - - callback_info = { - "callback": callback, - "args": args, - "kwargs": kwargs - } - - self._callbacks[event].append(callback_info) + self._callbacks[event].append(callback) def remove(self, event): """ @@ -36,21 +33,18 @@ class Callbacks(): if event in self._callbacks: del self._callbacks[event] - def call(self, event, *args): + async def call(self, event, *args, **kwargs): """ - call(event) -> None + await call(event) -> None - Call all callbacks subscribed to the event with *args and the arguments specified when the - callback was added. + Call all callbacks subscribed to the event with *args and **kwargs". """ - if event in self._callbacks: - for c_info in self._callbacks[event]: - c = c_info["callback"] - args = c_info["args"] + args - kwargs = c_info["kwargs"] - - c(*args, **kwargs) + tasks = [asyncio.ensure_future(callback(*args, **kwargs)) + for callback in self._callbacks.get(event, [])] + + for task in tasks: + await task def exists(self, event): """ diff --git a/yaboli/room.py b/yaboli/room.py index ad9e32f..3902110 100644 --- a/yaboli/room.py +++ b/yaboli/room.py @@ -1,5 +1,6 @@ import asyncio import logging +from .callbacks import * from .connection import * from .utils import * @@ -33,7 +34,7 @@ class Room: self.pm_with_nick = None self.pm_with_user_id = None - self._callbacks = {} + self._callbacks = Callbacks() self._add_callbacks() self._stopping = False @@ -266,20 +267,20 @@ class Room: # All the private functions for dealing with stuff def _add_callbacks(self): - self._callbacks["bounce-event"] = self._handle_bounce - self._callbacks["disconnect-event"] = self._handle_disconnect - self._callbacks["hello-event"] = self._handle_hello - self._callbacks["join-event"] = self._handle_join - self._callbacks["login-event"] = self._handle_login - self._callbacks["logout-event"] = self._handle_logout - self._callbacks["network-event"] = self._handle_network - self._callbacks["nick-event"] = self._handle_nick - self._callbacks["edit-message-event"] = self._handle_edit_message - self._callbacks["part-event"] = self._handle_part - self._callbacks["ping-event"] = self._handle_ping - self._callbacks["pm-initiate-event"] = self._handle_pm_initiate - self._callbacks["send-event"] = self._handle_send - self._callbacks["snapshot-event"] = self._handle_snapshot + self._callbacks.add("bounce-event", self._handle_bounce) + self._callbacks.add("disconnect-event", self._handle_disconnect) + self._callbacks.add("hello-event", self._handle_hello) + self._callbacks.add("join-event", self._handle_join) + self._callbacks.add("login-event", self._handle_login) + self._callbacks.add("logout-event", self._handle_logout) + self._callbacks.add("network-event", self._handle_network) + self._callbacks.add("nick-event", self._handle_nick) + self._callbacks.add("edit-message-event", self._handle_edit_message) + self._callbacks.add("part-event", self._handle_part) + self._callbacks.add("ping-event", self._handle_ping) + self._callbacks.add("pm-initiate-event", self._handle_pm_initiate) + self._callbacks.add("send-event", self._handle_send) + self._callbacks.add("snapshot-event", self._handle_snapshot) async def _send_packet(self, *args, **kwargs): response = await self._conn.send(*args, **kwargs) @@ -291,12 +292,10 @@ class Room: self._check_for_errors(packet) ptype = packet.get("type") - callback = self._callbacks.get(ptype) - if callback: - try: - await callback(packet) - except asyncio.CancelledError as e: - logger.info(f"&{self.roomname}: Callback of type {ptype!r} cancelled.") + try: + await self._callbacks.call(ptype, packet) + except asyncio.CancelledError as e: + logger.info(f"&{self.roomname}: Callback of type {ptype!r} cancelled.") def _check_for_errors(self, packet): if packet.get("throttled", False): diff --git a/yaboli/utils.py b/yaboli/utils.py index 6cb77a9..eaf8d16 100644 --- a/yaboli/utils.py +++ b/yaboli/utils.py @@ -1,4 +1,7 @@ +import asyncio + __all__ = [ + "run_controller", "mention", "mention_reduced", "similar", "Session", "Listing", "Message", "Log", @@ -7,6 +10,18 @@ __all__ = [ +def run_controller(controller, room): + """ + Helper function to run a singular controller. + """ + + async def run(): + task, reason = await controller.connect(room) + if task: + await task + + asyncio.get_event_loop().run_until_complete(run()) + def mention(nick): return "".join(c for c in nick if c not in ".!?;&<'\"" and not c.isspace())