#!/usr/bin/env -S PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python python3 # -*- coding: utf-8 -*- from argparse import ArgumentParser import asyncio from collections import defaultdict from datetime import datetime, timedelta, timezone from getpass import getpass import logging import os import re import sys import textwrap import zoneinfo import aiosqlite import babel.dates from slixmpp import ClientXMPP, JID from slixmpp.exceptions import IqTimeout, IqError from slixmpp.stanza import Message import slixmpp_omemo from slixmpp_omemo import PluginCouldNotLoad, MissingOwnKey, EncryptionPrepareException from slixmpp_omemo import UndecidedException, UntrustedException, NoAvailableSession from omemo.exceptions import MissingBundleException log = logging.getLogger(__name__) LEVEL_DEBUG = 0 LEVEL_ERROR = 1 EME_NS = 'eu.siacs.conversations.axolotl' SQL_INIT = """ CREATE TABLE budgets( id INTEGER PRIMARY KEY, created_at INTEGER, room TEXT NOT NULL, descr TEXT); CREATE UNIQUE INDEX budget_by_room ON budgets(room); CREATE TABLE members( id INTEGER PRIMARY KEY, created_at INTEGER, budget INTEGER NOT NULL, name TEXT, active INTEGER, FOREIGN KEY(budget) REFERENCES budgets(id) ON UPDATE CASCADE ON DELETE CASCADE); CREATE INDEX members_by_budget ON members(budget); CREATE TABLE bills( id INTEGER PRIMARY KEY, budget INTEGER NOT NULL, payer INTEGER NOT NULL, amount REAL NOT NULL, payed_at INTEGER NOT NULL, created_at INTEGER NOT NULL, type INTEGER NOT NULL, desc TEXT, tags TEXT, FOREIGN KEY(budget) REFERENCES budgets(id) ON UPDATE CASCADE ON DELETE CASCADE, FOREIGN KEY(payer) REFERENCES members(id)); CREATE INDEX bills_by_budget ON bills(budget); CREATE TABLE bill_allocs( bill INTEGER NOT NULL, member INTEGER, weight REAL, FOREIGN KEY(bill) REFERENCES bills(id) ON UPDATE CASCADE ON DELETE CASCADE, FOREIGN KEY(member) REFERENCES members(id), UNIQUE(bill, member)); CREATE INDEX allocs_by_bill ON bill_allocs(bill); CREATE INDEX allocs_by_member ON bill_allocs(member); PRAGMA user_version = 1; """ BILL_NORMAL = 0 BILL_BALANCE = 1 MEMBER_INACTIVE = 0 MEMBER_ACTIVE = 1 TZ_PARIS = zoneinfo.ZoneInfo('Europe/Paris') def timestamp_now(): return int(datetime.now(tz=timezone.utc).replace(microsecond=0).timestamp()) def timestamp_load(ts): return datetime.fromtimestamp(ts, tz=timezone.utc) def timestamp_format(ts): d = timestamp_load(ts).astimezone(TZ_PARIS) return babel.dates.format_date(d, locale='fr_FR', format='long') def clean_docstring(func): return textwrap.dedent(func.__doc__).strip() class Albus(ClientXMPP): """Albus, le bot-chat.""" nick = 'albus' debug_level = LEVEL_DEBUG def __init__(self, jid, password, db_path): ClientXMPP.__init__(self, jid, password) self.db = None self.db_path = db_path self.auto_authorize = True self.auto_subscribe = False self.room_info = {} self.add_event_handler('session_start', self.on_start) self.add_event_handler('message', self.on_message) self.add_event_handler('groupchat_direct_invite', self.on_direct_invite) self.add_event_handler('groupchat_presence', self.on_muc_presence) self.add_event_handler('groupchat_message', self.on_muc_message) self.cmd_match = re.compile(r'^!') self.cmd_extr = re.compile(r'^!(?P(\w|[._-])+)\s*(?P.*)$') self.cmd_handlers = {} self.cmd_handlers['budget-init'] = self.cmd_budget_init self.cmd_handlers['budget-info'] = self.cmd_budget_info self.cmd_handlers['member-add'] = self.cmd_member_add self.cmd_handlers['member-del'] = self.cmd_member_del self.cmd_handlers['depense'] = self.cmd_bill_add self.cmd_handlers['depense-del'] = self.cmd_bill_del self.cmd_handlers['total'] = self.cmd_total self.cmd_handlers['affiche'] = self.cmd_bill_show self.cmd_handlers['help'] = self.cmd_help async def on_exit(self): if self.db is not None: await self.db.close() log.info('Exiting') async def on_start(self, _): self.send_presence() self.get_roster() self.xep_0045 = self['xep_0045'] self.xep_0384 = self['xep_0384'] self.xep_0380 = self['xep_0380'] log.info('Loading database from: %s' % self.db_path) self.db = await aiosqlite.connect(self.db_path) async with self.db.execute('PRAGMA user_version') as c: (user_version,) = await c.fetchone() if user_version == 0: log.info('Creating database tables') await self.db.executescript(SQL_INIT) await self.db.commit() async with self.db.execute('SELECT (room) FROM budgets') as c: async for (room,) in c: await self.process_join(JID(room)) async def on_direct_invite(self, msg): """Accept all MUC invites.""" mroom = msg['groupchat_invite']['jid'] await self.process_join(mroom) async def process_join(self, mroom): """Join MUC and start tracking room affiliations.""" log.info('Joining room: %s' % mroom) await self.xep_0045.join_muc_wait(mroom, self.nick, seconds=0) members = self.room_info.setdefault(mroom, set()) for a in ('owner', 'admin', 'member'): for j in await self.xep_0045.get_affiliation_list(mroom, a): members.add(JID(j)) async def on_muc_presence(self, msg): """Track room affiliations (required for encryption).""" mtype = msg['type'] mroom = msg['muc']['room'] maffil = msg['muc']['affiliation'] # normalize jid (without device part) mjid = JID(msg['muc']['jid'].bare) members = self.room_info.setdefault(mroom, set()) if mtype == 'available' and maffil in ('owner', 'admin', 'member'): members.add(mjid) else: members.discard(mjid) async def on_muc_message(self, msg): mnick = msg['muc']['nick'] mroom = msg['muc']['room'] mfrom = JID(self.xep_0045.get_jid_property(mroom, mnick, 'jid')) if mfrom == self.boundjid: return body = await self.decrypt_message(msg, mfrom, eto=mroom, etype='groupchat') if body is not None: await self.process_muc_message(mroom, mfrom, body) async def on_message(self, msg): mtype = msg['type'] mfrom = msg['from'] if mtype != 'chat' or mfrom == self.boundjid: return body = await self.decrypt_message(msg, mfrom, mfrom, type) if body is not None: await self.process_message(mfrom, body) async def process_muc_message(self, mroom, mfrom, body): if not self.cmd_match.match(body): return m = self.cmd_extr.match(body) if m is None: await self.room_send(mroom, 'erreur: la commande est mal formée\nvoir: !help') return cmd = m.group('cmd') args = m.group('args') h = self.cmd_handlers.get(cmd) if h is None: await self.room_send(mroom, 'erreur: je ne connais pas la commande: %s\nvoir: !help' % cmd) return await h(mroom, mfrom, args) async def budget_descr_string(self, b_id=None, room=None): if b_id is None: if room is None: raise ValueError('at least id or room must be given') async with self.db.execute('SELECT id, descr FROM budgets WHERE room = ?', (room,)) as c: out = await c.fetchone() if out is None: raise ValueError('budget does not exist') (b_id, b_descr) = out else: async with self.db.execute('SELECT descr FROM budgets WHERE id = ?', (b_id,)) as c: b_descr = await c.fetchone() if b_descr is None: raise ValueError('budget does not exist') lines = ['description: %s' % b_descr] async with self.db.execute( 'SELECT id, created_at, name, active FROM members WHERE budget = ?', (b_id,)) as c: async for (m_id, ts, name, act) in c: opts = 'membre depuis le %s' % timestamp_format(ts) if act == MEMBER_INACTIVE: opts += ', inactif' opts += ' [id=%02i]' % m_id lines.append('- %s, %s' % (name, opts)) if len(lines) == 1: lines.append('aucun membre') return '\n'.join(lines) def short_help(self, cmd): return clean_docstring(self.cmd_handlers[cmd]).split('\n')[0] def long_help(self, cmd): return clean_docstring(self.cmd_handlers[cmd]) def usage_help(self, cmd): return clean_docstring(self.cmd_handlers[cmd]).split('\n')[3] async def cmd_help(self, mroom, mfrom, args): """ Affiche la liste des commandes et l'aide. usage: !help CMD """ cmd = args.strip() if not cmd in self.cmd_handlers: if cmd: prepend = "erreur: je ne connais pas la commande %s\n" % cmd else: prepend = "" cmds = [] n = max(len(c) for c in self.cmd_handlers) fmt = "{} -- {}" for k in sorted(self.cmd_handlers): cmds.append('%s -- %s' % (k, self.short_help(k))) msg = '%sliste des commandes:\n%s' % (prepend, '\n'.join(cmds)) return await self.room_send(mroom, msg) else: msg = self.long_help(cmd) return await self.room_send(mroom, msg) async def cmd_budget_init(self, mroom, mfrom, args): """ Initialise le budget avec une description. usage: !budget-init DESCRIPTION """ if not args: err = 'erreur: commande mal formée\n%s' % self.usage_help('budget-init') return await self.room_send(mroom, err) # verifie que le budget existe pas déjà async with self.db.execute('SELECT id FROM budgets WHERE room = ?', (mroom,)) as c: b = await c.fetchone() if b is not None: msg = 'erreur: il y a déjà un budget pour ce canal' return await self.room_send(mroom, msg) # cree le budget now = timestamp_now() (b_id,) = await self.db.execute_insert( 'INSERT INTO budgets(created_at, room, descr) VALUES (?, ?, ?)', (now, mroom, args)) await self.db.commit() msg = await self.budget_descr_string(b_id=b_id) await self.room_send(mroom, msg) async def cmd_budget_info(self, mroom, mfrom, args): """ Renvoie la liste des membres et la description du budget courant. usage: !budget-info """ try: msg = await self.budget_descr_string(room=mroom) except ValueError: return await self.room_send(mroom, 'erreur: pas de budget configuré dans ce canal') else: return await self.room_send(mroom, msg) async def get_id_from_room(self, room): async with self.db.execute('SELECT id FROM budgets WHERE room = ?', (room,)) as c: b_id = await c.fetchone() if b_id is None: return await self.room_send(room, 'erreur: pas de budget configuré dans ce canal') return b_id[0] async def cmd_member_add(self, mroom, mfrom, args): """ Ajoute un membre au budget courant. usage: !member-add NOM """ name = args if not name: err = 'erreur: commande mal formée\n%s' % self.usage_help('member-add') return await self.room_send(mroom, err) b_id = await self.get_id_from_room(mroom) if b_id is None: return # check if member is already present async with self.db.execute('SELECT active FROM members WHERE budget = ? AND name = ?', (b_id, name)) as c: r = await c.fetchone() if r is not None: if r[0] == MEMBER_INACTIVE: now = timestamp_now() await self.db.execute( 'UPDATE members SET active = ? WHERE budget = ? AND name = ?', (MEMBER_ACTIVE, b_id, name)) await self.db.commit() return await self.room_send(mroom, "ok: j'ai réactivé %s" % name) else: err = 'erreur: %s est déjà membre actif de ce budget' % name return await self.room_send(mroom, err) now = timestamp_now() await self.db.execute( 'INSERT INTO members(created_at, budget, name, active) VALUES (?, ?, ?, ?)', (now, b_id, name, MEMBER_ACTIVE)) await self.db.commit() await self.room_send(mroom, "ok: j'ai ajouté %s" % name) async def cmd_member_del(self, mroom, mfrom, name): """ Désactive un membre du budget courant. usage: !member-del NOM """ if not name: err = 'erreur: commande mal formée\n%s' % self.usage_help('member-del') return await self.room_send(mroom, err) b_id = await self.get_id_from_room(mroom) if b_id is None: return # check if member is already present async with self.db.execute('SELECT id, active FROM members WHERE budget = ? AND name = ?', (b_id, name)) as c: r = await c.fetchone() if r is None: return await self.room_send(mroom, "erreur: %s n'est pas présent dans ce budget" % name) (m_id, act) = r if act == MEMBER_INACTIVE: return await self.room_send(mroom, "erreur: %s est déjà désactivé" % name) await self.db.execute( 'UPDATE members SET active = ? WHERE budget = ? AND id = ?', (MEMBER_INACTIVE, b_id, m_id)) await self.db.commit() await self.room_send(mroom, "ok: j'ai désactivé %s" % name) async def cmd_bill_add(self, mroom, mfrom, args): """ Ajoute une nouvelle dépense au budget courant. usage: !depense MONTANT DESCRIPTION; OPTIONS exemples: - payé par moi, pour tous les membres actifs: !depense 10 la fourche - payée par peio, pour tous les membres actifs: !depense 10 la fourche; @peio - payé par peio, pour tous les membres actifs sauf tristu !depense 10 la fourche; @peio -tristu - payé par moi, pour meli (2 parts), tristu (1 part) et cle (3 parts) !depense 10 la fourche; meli:2 tristu cle:3 - payé par tristu, pour meli: !depense 10 la fourche; @tristu meli """ if not args: err = 'erreur: commande mal formée\n%s' % self.usage_help('depense') return await self.room_send(mroom, err) # check if budget exists b_id = await self.get_id_from_room(mroom) if b_id is None: return # parse args m = re.match(r'(?P[0-9]+)\s+(?P[^;]*)(;(?P.*))?$', args) if not m: err = 'erreur: commande mal formée\n%s' % self.usage_help('depense') return await self.room_send(mroom, err) amount = m.group('amount') try: amount = float(amount) except ValueError: return await self.room_send(mroom, "erreur: je ne comprend pas le montant: %s" % amount) descr = m.group('descr') opts = m.group('opts') or '' payed_by = None includes = {} excludes = set() for opt in opts.split(): if opt[0] == '@': # parse payer if payed_by is not None: err = "erreur: seulement une option '@' autorisée." return await self.room_send(mroom, '%s\nusage: %s' % (err, )) payed_by = opt[1:] elif opt[0] == '-': # parse explicit exclusion if len(includes) > 0: err = "erreur: option '-' incompatible avec l'ajout explicite" return await self.room_send(mroom, err) excludes.add(opt[1:]) else: # parse explicit inclusion if len(excludes) > 0: err = "erreur: option '-' incompatible avec l'ajout explicite" return await self.room_send(mroom, err) m = re.match(r'(?P[a-zA-Z]+)(:(?P[0-9]+(.[0-9]+)?))?', opt) if not m: err = "erreur: je ne comprend pas l'option: %s" % opt return await self.room_send(mroom, err) name = m.group('name') weight = float(m.group('weight') or 1.) includes[name] = weight if not payed_by: payed_by = mfrom.bare.split('@')[0] # load members members = {} async with self.db.execute('SELECT id, name, active FROM members WHERE budget = ?', (b_id,)) as c: async for (m_id, name, act) in c: members[name] = (m_id, act) if not payed_by in members: err = "erreur: %s n'est pas membre de ce budget" % payed_by return await self.room_send(mroom, err) # explicit owers if len(includes) > 0: for (m, w) in includes.items(): if m not in members: err = "erreur: %s n'est pas membre de ce budget" % m return await self.room_send(mroom, err) owers = includes # excludes else: owers = {n: 1. for (n, (_, act)) in members.items() if act == MEMBER_ACTIVE} for m in excludes: if m not in members: err = "erreur: %s n'est pas membre de ce budget" % m return await self.room_send(mroom, err) del owers[m] if len(owers) == 0: err = "erreur: une transaction doit etre payé pour au moins une personne" return await self.room_send(mroom, err) now = timestamp_now() (t_id,) = await self.db.execute_insert( 'INSERT INTO bills(budget, payer, amount, payed_at, created_at, type, desc) ' 'VALUES (?, ?, ?, ?, ?, ?, ?)', (b_id, members[payed_by][0], amount, now, now, BILL_NORMAL, descr)) for (m, w) in owers.items(): await self.db.execute_insert( 'INSERT INTO bill_allocs(bill, member, weight) VALUES (?, ?, ?)', (t_id, members[m][0], w)) await self.db.commit() bill_str = await self.format_bill(t_id) await self.room_send(mroom, bill_str) async def cmd_bill_del(self, mroom, mfrom, args): """ Supprime une dépense du budget. usage: !depense-del id=ID """ if not args: err = 'erreur: commande mal formée\n%s' % self.usage_help('depense-del') return await self.room_send(mroom, err) b_id = await self.get_id_from_room(mroom) if b_id is None: return if not args[:3] == 'id=': err = 'erreur: commande mal formée\n%s' % self.usage_help('depense-del') return await self.room_send(mroom, err) try: t_id = int(args[3:]) except ValueError: err = "erreur: je ne comprend pas l'identifiant: %s" % args[3:] return await self.room_send(mroom, err) try: bill_descr = await self.format_bill(t_id) except ValueError: err = "erreur: je ne connais pas la dépense [id=%02i]" % t_id return await self.room_send(mroom, err) await self.db.execute('DELETE FROM bills WHERE id = ?', (t_id,)) await self.db.commit() msg = "ok: j'ai supprimé la transaction:\n" + bill_descr return await self.room_send(mroom, msg) async def cmd_total(self, mroom, mfrom, args): """ Affiche le total des transactions et les ardoises de chaque membre. usage: !total """ b_id = await self.get_id_from_room(mroom) if b_id is None: return balance = defaultdict(float) total = 0. async with self.db.execute('SELECT id, payer, amount, type FROM bills WHERE budget = ?', (b_id,)) as cur_bills: async for (t_id, by, amount, btype) in cur_bills: tot_w = 0. allocs = {} async with self.db.execute('SELECT member, weight FROM bill_allocs WHERE bill = ?', (t_id,)) as cur_allocs: async for (memb, w) in cur_allocs: tot_w += w allocs[memb] = w if btype == BILL_NORMAL: total += amount balance[by] += amount for (m, w) in allocs.items(): balance[m] -= amount * w / tot_w members = {} async with self.db.execute('SELECT id, name FROM members WHERE budget = ?', (b_id,)) as c: async for (m_id, name) in c: members[m_id] = name lines = ['total dépensé: %.2feur' % total] for (m, t) in balance.items(): lines.append('- %s: %+.2feur' % (members[m], t)) return await self.room_send(mroom, '\n'.join(lines)) async def cmd_bill_show(self, mroom, mfrom, args): """ Affiche les dernières dépenses. !affiche id=ID !affiche NOMBRE """ if not args: err = 'erreur: commande mal formée\n%s' % self.usage_help('affiche') return await self.room_send(mroom, err) b_id = await self.get_id_from_room(mroom) if b_id is None: return if args[:3] == 'id=': try: t_id = int(args[3:]) except ValueError: err = "erreur: je ne comprend pas l'identifiant: %s" % args[3:] return await self.room_send(mroom, err) try: msg = await self.format_bill(t_id) except ValueError: err = "erreur: je ne connais pas la dépense [id=%02i]" % t_id return await self.room_send(mroom, err) return await self.room_send(mroom, msg) else: try: num = int(args) except ValueError: err = "erreur: je ne comprend pas le nombre: %s" % args return await self.room_send(mroom, err) lines = [] async with self.db.execute('SELECT id FROM bills WHERE budget = ? ' 'ORDER BY payed_at DESC LIMIT ?', (b_id, num)) as c: async for (t_id,) in c: lines.append(await self.format_bill(t_id)) return await self.room_send(mroom, '\n'.join(lines)) async def format_bill(self, t_id): qry = ('SELECT bills.id, members.name, bills.amount, bills.payed_at, bills.type, bills.desc ' 'FROM bills JOIN members ON bills.payer = members.id WHERE bills.id = ?') async with self.db.execute(qry, (t_id,)) as c: r = await c.fetchone() if r is None: raise ValueError('no such bill: id=%s' % t_id) (t_id, name, amount, payed_at, btype, desc) = r if btype == BILL_NORMAL: fmt = '%s, %.2feur payé par %s pour' elif btype == BILL_BALANCE: fmt = '%s, %.2feur transféré par %s à' fmt %= (desc, amount, name) qry = ('SELECT members.name, bill_allocs.weight FROM bill_allocs ' 'JOIN members ON bill_allocs.member = members.id ' 'WHERE bill_allocs.bill = ?') ppl = [] async with self.db.execute(qry, (t_id,)) as c: async for (name, weight) in c: if weight == 1.: ppl.append(name) else: ppl.append('%s (%.1f parts)' % (name, weight)) return '%s %s le %s [id=%02i]' % (fmt, ', '.join(ppl), timestamp_format(payed_at), t_id) async def process_message(self, mfrom, body): pass async def room_send(self, mroom, mbody): await self.encrypted_send(JID(mroom), 'groupchat', mbody, self.room_info[mroom]) async def decrypt_message(self, msg, mfrom, eto, etype): if not self.xep_0384.is_encrypted(msg): await self.plain_send(eto, etype, 'attention: ce message n\'est pas chiffré') return msg['body'] try: encrypted = msg['omemo_encrypted'] body = await self.xep_0384.decrypt_message(encrypted, mfrom) if body is not None: body = body.decode('utf8') return body except (MissingOwnKey,): await self.plain_send(eto, etype, 'erreur: le déchiffrement a échoué (missing-own-key)') except (NoAvailableSession,) as exn: await self.plain_send(eto, etype, 'erreur: le déchiffrement a échoué (no-available-session)') except (UndecidedException,) as exn: await self.xep_0384.trust(exn.bare_jid, exn.device, exn.ik) return await self.decrypt_message(msg, mfrom, eto, etype) except (UntrustedException,) as exn: await self.plain_send(eto, etype, 'erreur: le déchiffrement a échoué (untrusted-key)') except (EncryptionPrepareException,): await self.plain_send(eto, etype, 'erreur: le déchiffrement a échoué (encryption-prepare)') except (Exception,) as exn: await self.plain_send(eto, etype, 'erreur: petit soucis...\n%r' % exn) raise async def plain_send(self, mto, mtype, body): msg = self.make_message(mto=mto, mtype=mtype) msg['body'] = body return msg.send() async def encrypted_send(self, mto, mtype, body, recipients=None): if recipients is None: recipients = [mto] msg = self.make_message(mto=mto, mtype=mtype) msg['eme']['namespace'] = EME_NS msg['eme']['name'] = self.xep_0380.mechanisms[EME_NS] expect_problems = {} while True: try: encrypt = await self.xep_0384.encrypt_message(body, recipients, expect_problems) msg.append(encrypt) return msg.send() except UndecidedException as exn: await self.xep_0384.trust(exn.bare_jid, exn.device, exn.ik) # TODO: catch NoEligibleDevicesException except EncryptionPrepareException as exn: for error in exn.errors: if isinstance(error, MissingBundleException): err = 'attention: le chiffrement a échoué pour %s (missing-bundle)' self.plain_send(mto, mtype, err % error.bare_jid) device_list = expect_problems.setdefault(JID(error.bare_jid), []) device_list.append(error.device) except (IqError, IqTimeout) as exn: err = ("erreur: je n'arrive pas à récupérer les informations " "de chiffrement pour un participant") self.plain_send(mto, mtype, err) return except Exception as exn: await self.plain_send(mto, mtype, 'erreur: petit soucis...\n%r' % exn) raise if __name__ == '__main__': parser = ArgumentParser(description=Albus.__doc__) parser.add_argument("-q", "--quiet", help="set logging to ERROR", action="store_const", dest="loglevel", const=logging.ERROR, default=logging.INFO) parser.add_argument("-d", "--debug", help="set logging to DEBUG", action="store_const", dest="loglevel", const=logging.DEBUG, default=logging.INFO) parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") DATA_DIR = os.path.join( os.path.dirname(os.path.abspath(__file__)), 'omemo', ) parser.add_argument("--data-dir", dest="data_dir", help="data directory", default=DATA_DIR) args = parser.parse_args() logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") os.makedirs(args.data_dir, exist_ok=True) xmpp = Albus(args.jid, args.password, os.path.join(args.data_dir, 'albus.db')) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0045') # Multi-User Chat xmpp.register_plugin('xep_0199') # XMPP Ping xmpp.register_plugin('xep_0249') # Direct MUC Invitations xmpp.register_plugin('xep_0380') # Explicit Message Encryption xmpp.register_plugin('xep_0437') # Room Activity Indicators xmpp.register_plugin('xep_0313') # Message Archive Management try: xmpp.register_plugin('xep_0384', {'data_dir': args.data_dir}, module=slixmpp_omemo) except (PluginCouldNotLoad,): log.exception('And error occured when loading the omemo plugin.') sys.exit(1) try: xmpp.connect() xmpp.loop.run_until_complete(xmpp.disconnected) except KeyboardInterrupt: xmpp.loop.run_until_complete(xmpp.disconnect()) finally: xmpp.loop.run_until_complete(xmpp.on_exit())