commit 20a3aa0ce3efcfc76ea489ae5561502f818f4525 Author: Alexander PapaTutuWawa Date: Wed Mar 31 00:31:29 2021 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7fe916 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +**/*~ +build/ +dist/ +venv/ +**/__pycache__/ +*.egg-info +*.toml \ No newline at end of file diff --git a/mira/__init__.py b/mira/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mira/base.py b/mira/base.py new file mode 100644 index 0000000..0e9355e --- /dev/null +++ b/mira/base.py @@ -0,0 +1,97 @@ +import importlib +import asyncio + +import aioxmpp +import toml + +from mira.subscription import SubscriptionManager + +def message_wrapper(to, body): + msg = aioxmpp.Message( + type_=aioxmpp.MessageType.CHAT, + to=to) + msg.body[None] = body + return msg + +class MiraBot: + def __init__(self): + # Bot specific settings + self._jid = "" + self._password = "" + self._client = None + self._avatar = None + + self._modules = {} # Module name -> module + self._subscription_manager = SubscriptionManager() + + def load_config(self, path): + data = toml.load(path) + + self._jid = aioxmpp.JID.fromstr(data['jid']) + self._password = data['password'] + self._avatar = data.get('avatar', None) + + for module in data['modules']: + mod = importlib.import_module(module['name']) + self._modules[mod.NAME] = mod.get_instance(self, module) + self._modules[mod.NAME].set_name(mod.NAME) + + async def connect(self): + self._client = aioxmpp.PresenceManagedClient( + self._jid, + aioxmpp.make_security_layer(self._password)) + async with self._client.connected(): + self._client.stream.register_message_callback( + aioxmpp.MessageType.CHAT, + None, + self._on_message) + + if self._avatar: + with open(self._avatar, 'rb') as avatar_file: + data = avatar_file.read() + avatar_set = aioxmpp.avatar.AvatarSet() + # TODO: Detect MIME type + avatar_set.add_avatar_image('image/png', image_bytes=data) + avatar = self._client.summon(aioxmpp.avatar.AvatarService) + await avatar.publish_avatar_set(avatar_set) + + while True: + await asyncio.sleep(1) + + def _on_message(self, message): + # Automatically handles sending a message receipt and dealing + # with unwanted messages + if (message.type_ != aioxmpp.MessageType.CHAT or + not message.body): + return + + cmd = str(message.body.any()).split(' ') + + receipt = aioxmpp.mdr.compose_receipt(message) + self._client.enqueue(receipt) + + if not cmd[0] in self._modules: + self._client.enqueue(message_wrapper(message.from_, "Unbekannter Befehl")) + return + + self._modules[cmd[0]]._on_command(cmd[1:], message) + + # Module Function: Send message + def send_message(self, message): + self._client.enqueue(message) + + # Module Function: Send a message to @to with @body + def send_message_wrapper(self, to, body): + self.send_message(message_wrapper(to, body)) + + # Module Function + def get_subscription_manager(self): + return self._subscription_manager + +def main(): + bot = MiraBot() + bot.load_config("./config.toml") + + loop = asyncio.get_event_loop() + loop.run_until_complete(bot.connect()) + loop.close() diff --git a/mira/module.py b/mira/module.py new file mode 100644 index 0000000..9abe09e --- /dev/null +++ b/mira/module.py @@ -0,0 +1,51 @@ +class BaseModule: + def __init__(self, base, config={}, subcommand_table={}): + self._name = '' + self._base = base + self._config = config + self._subcommand_table = subcommand_table + + def set_name(self, name): + if self._name: + raise Exception('Name change of module attempted!') + + self._name = name + + def get_option(self, key, default=None): + return self._config.get(key, default) + + # Used for access control + # Returns True if @jid is allowed to access the command. False otherwise. + # This is configured by either restrict_local or allowed_domains. restrict_local + # takes precedence over allowed_domains + def is_jid_allowed(self, jid): + only_local = self.get_option('restrict_local') + if only_local: + return only_local + + domains = self.get_option('allowed_domains') + if not domains: + return True + + return jid.domain in domains + + def get_subscriptions_for(self, jid): + return self._base.get_subscription_manager.get_subscriptions_for(self._name, jid) + + def add_subscription_for(self, jid, keyword): + return self._base.get_subscription_manager.add_subscription_for(self._name, jid, keyword) + + def remove_subscription_for(self, jid, keyword): + return self._base.get_subscription_manager.remove_subscription_for(self._name, jid, keyword) + + def is_subscribed_to(self, jid, keyword): + return self._base.get_subscription_manager.is_subscribed_to(self._name, jid, keyword) + + def _on_command(self, cmd, msg): + if not self._subcommand_table: + self.on_command(cmd, msg) + elif cmd and cmd[0] in self._subcommand_table: + self._subcommand_table[cmd[0]](cmd[1:], msg) + else: + if '*' in self._subcommand_table: + self._subcommand_table['*'](cmd, msg) diff --git a/mira/modules/test.py b/mira/modules/test.py new file mode 100644 index 0000000..4ef3be0 --- /dev/null +++ b/mira/modules/test.py @@ -0,0 +1,27 @@ +from mira.module import BaseModule + +NAME = 'test' + +class TestModule(BaseModule): + __instance = None + + @staticmethod + def get_instance(base, config): + if TestModule.__instance == None: + TestModule(base, config) + + return TestModule.__instance + + def __init__(self, base, config): + if TestModule.__instance != None: + raise Exception('Trying to init singleton twice') + + super().__init__(base, config) + TestModule.__instance = self + + def on_command(self, cmd, msg): + greeting = self.get_option('greeting', 'OwO, %%user%%!').replace('%%user%%', str(msg.from_.bare())) + self._base.send_message_wrapper(msg.from_, greeting) + +def get_instance(base, config={}): + return TestModule.get_instance(base, config) diff --git a/mira/subscription.py b/mira/subscription.py new file mode 100644 index 0000000..ae84763 --- /dev/null +++ b/mira/subscription.py @@ -0,0 +1,39 @@ +#from collections import namedtuple +# TODO: Allow storing data along with the keyword + +def append_or_insert(dict_, key, value): + if key in dict_ or dict_[key]: + dict_[key].append(value) + else: + dict_[key] = [value] + +def remove_or_delete(dict_, key, value): + if value in dict_[key] and len(dict_[key]) == 1: + del dict_[key] + else: + dict_[key].remove(value) + +class SubscriptionManager: + def __init__(self): + self._subscriptions = {} # Module -> JID -> Keywords + + def get_subscriptions_for(self, module, jid): + if not module in self._subscriptions: + return None + if not jid in self._subscriptions[module]: + return None + + return self._subscriptions[module][jid] + + def is_subscribed_to(self, module, jid, keyword): + return keyword in self.get_subscriptions_for(module, jid) + + def __flush(self): + # TODO + pass + + def add_subscription_for(self, module, jid, keyword): + append_or_insert(self._subscriptions[module], jid, keyword) + + def remove_subscription_for(self, module, jid, keyword): + remove_or_delete(self._subscriptions[module], jid, keyword) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2860cac --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +from setuptools import setup, find_packages + +setup( + name = 'mira', + version = '0.1.0', + description = 'An XMPP bot framework', + url = 'https://git.polynom.me/PapaTutuWawa/mira', + author = 'Alexander "PapaTutuWawa"', + author_email = 'papatutuwawa@polynom.me', + license = 'GPLv3', + packages = find_packages(), + install_requires = [ + 'aioxmpp>=0.11.0', + 'toml>=0.10.2' + ], + zip_safe=True, + entry_points={ + 'console_scripts': [ + 'mira = mira.mira:main' + ] + } +)