albus/albus.py

845 lines
30 KiB
Python
Executable File

#!/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<cmd>(\w|[._-])+)\s*(?P<args>.*)$')
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<amount>[0-9]+)\s+(?P<descr>[^;]*)(;(?P<opts>.*))?$', 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<name>[a-zA-Z]+)(:(?P<weight>[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())