MessengerAssistant
I just wrote a simple plugin driven Messenger Assistant. It's a bot that listens to your messages when you chat on Messenger and can execute commands based on that. It uses fbchat package from pypi. The simplest example would be:
- Command time, which gives prints to us current time.
In prototype version, it was just big if/elif statement to decide if the message is a command and if we need to reply to it. I wanted to do something more powerful and modular.
Plugin driven MessengerAssistant
I started off by doing a base for plugin, some template that every plugin has to follow to be compatible with my bot. If it was C# or Java, we would use interfaces which would enforce existence of some methods that we could later use. Python does not have an idea of interfaces, but it does provide module called abc(Abstract Base Classes).
Our AbstractPluginBase could look like this:
import abc
class AbstractPluginBase:
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def __init__(self):
pass
@abc.abstractmethod
def check_pattern(self, message):
# Check if we should handle this message
return False
@abc.abstractmethod
def handle_message(self, message):
# Handle the message
return ""
abc works by using metaclass, class that sole purpose is to create classes(don't mix that with instances of classes). This use of abc works like interfaces, classes dervied from AbstractPluginBase must implement those three methods(__init__ exists by default so we won't see it implemented in concrete plugins, I added it so it is impossible to change initialization, which will be helpful later on).
Then in plugins/ python package(which means that there is __init__.py file inside) I wrote some example plugins like the time one you saw above
import time
from plugin_base import AbstractPluginBase
class TimePlugin(AbstractPluginBase):
def check_pattern(self, message):
if message == "time":
return True
else:
return False
def handle_message(self, message):
return time.ctime()
As simple as that, the last missing piece is loading all the modules in plugins/ directory, for that I needed to bind all of the files inside plugins/ to plugins module. So __init__.py contains:
import glob
from os.path import dirname, basename, isfile
modules = glob.glob(dirname(__file__) + "/*.py")
__all__ = [basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]
Iterating over all files in directory, adding them to the package.
At this points we have plugins package that we can load from, last thing to do is to load all of the modules and get instances of them, in doing that we have to remember to only load subclasses of AbstractPluginBase, cause some plugins can use other classes for their implementations.
We had to use some reflection here, to check types of members of modules, so that we only get classes that satisfy three conditions: object is a class, object is a subclass of AbstractPluginBase and object is not AbstractPluginBase itself.
Then if we want to use the plugins, we just iterate over them and check if given plugin is able to handle given message.
At this points we have plugins package that we can load from, last thing to do is to load all of the modules and get instances of them, in doing that we have to remember to only load subclasses of AbstractPluginBase, cause some plugins can use other classes for their implementations.
import inspect
import importlib
import plugins
class PluginLoader:
@staticmethod
def get_all_plugins():
plugins_classes = []
# Load all modules
for module in [importlib.import_module("plugins." + x) for x in plugins.__all__]:
# Get only subclasses of AbstractPluginBase
plugins_classes += inspect.getmembers(module, PluginLoader.is_plugin)
# Return instances of the classes
return [(x[0], x[1]()) for x in plugins_classes]
@staticmethod
def is_plugin(object):
return inspect.isclass(object) and issubclass(object, AbstractPluginBase) and object is not AbstractPluginBase
We had to use some reflection here, to check types of members of modules, so that we only get classes that satisfy three conditions: object is a class, object is a subclass of AbstractPluginBase and object is not AbstractPluginBase itself.
Then if we want to use the plugins, we just iterate over them and check if given plugin is able to handle given message.
for plugin in self.plugins:
name, plugin_inst = plugin
if plugin_inst.check_pattern(message):
if self.debug:
print("%s is handling the message" % name)
output = plugin_inst.handle_message(message)
self.send(send_id, output, is_user=is_user)
It wasn't so hard, was it? Python is really powerful when it comes to reflection and inspecting itself at runtime which gives us a lot of control when we know what we are doing.
I added some plugins like getting current value of my cryptocurrencies if I were to sell them at the current price at the exchange.
Full source code: https://github.com/hub2/Messenger-Assistant
I added some plugins like getting current value of my cryptocurrencies if I were to sell them at the current price at the exchange.
Full source code: https://github.com/hub2/Messenger-Assistant

0 comments:
Post a Comment