Add Bot class

- Use rewrite-2 callbacks modified for async
- Utility function for running singular controller
- Update TestBot to work with new system
This commit is contained in:
Joscha 2017-09-04 19:56:17 +00:00
parent b8bb75a897
commit 053573e3cb
6 changed files with 227 additions and 73 deletions

View file

@ -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")

View file

@ -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__

138
yaboli/bot.py Normal file
View file

@ -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)

View file

@ -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):
"""

View file

@ -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):

View file

@ -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())