albus/albus.py

845 lines
30 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env -S PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python python3
# -*- coding: utf-8 -*-
from argparse import ArgumentParser
import asyncio
2024-04-27 11:07:08 +00:00
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from getpass import getpass
import logging
import os
import re
import sys
2024-04-27 11:07:08 +00:00
import textwrap
import zoneinfo
import aiosqlite
2024-04-27 11:07:08 +00:00
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,
2024-04-27 11:07:08 +00:00
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,
2024-04-27 11:07:08 +00:00
created_at INTEGER,
budget INTEGER NOT NULL,
name TEXT,
active INTEGER,
FOREIGN KEY(budget)
REFERENCES budgets(id)
ON UPDATE CASCADE
2024-04-27 11:07:08 +00:00
ON DELETE CASCADE);
CREATE INDEX members_by_budget
ON members(budget);
CREATE TABLE bills(
id INTEGER PRIMARY KEY,
2024-04-27 11:07:08 +00:00
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
2024-04-27 11:07:08 +00:00
ON DELETE CASCADE,
FOREIGN KEY(payer)
REFERENCES members(id));
CREATE INDEX bills_by_budget
ON bills(budget);
2024-04-27 11:07:08 +00:00
CREATE TABLE bill_allocs(
bill INTEGER NOT NULL,
member INTEGER,
weight REAL,
FOREIGN KEY(bill)
REFERENCES bills(id)
ON UPDATE CASCADE
ON DELETE CASCADE,
2024-04-27 11:07:08 +00:00
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
2024-04-27 11:07:08 +00:00
self.db_path = db_path
self.auto_authorize = True
2024-04-27 11:07:08 +00:00
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
2024-04-27 11:07:08 +00:00
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']
2024-04-27 11:07:08 +00:00
log.info('Loading database from: %s' % self.db_path)
self.db = await aiosqlite.connect(self.db_path)
2024-04-27 11:07:08 +00:00
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):
2024-04-27 11:07:08 +00:00
"""Accept all MUC invites."""
mroom = msg['groupchat_invite']['jid']
await self.process_join(mroom)
async def process_join(self, mroom):
2024-04-27 11:07:08 +00:00
"""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)
2024-04-27 11:07:08 +00:00
members = self.room_info.setdefault(mroom, set())
2024-04-27 11:07:08 +00:00
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):
2024-04-27 11:07:08 +00:00
"""Track room affiliations (required for encryption)."""
mtype = msg['type']
mroom = msg['muc']['room']
maffil = msg['muc']['affiliation']
2024-04-27 11:07:08 +00:00
# normalize jid (without device part)
mjid = JID(msg['muc']['jid'].bare)
members = self.room_info.setdefault(mroom, set())
2024-04-27 11:07:08 +00:00
if mtype == 'available' and maffil in ('owner', 'admin', 'member'):
members.add(mjid)
2024-04-27 11:07:08 +00:00
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:
2024-04-27 11:07:08 +00:00
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:
2024-04-27 11:07:08 +00:00
await self.room_send(mroom, 'erreur: je ne connais pas la commande: %s\nvoir: !help' % cmd)
return
await h(mroom, mfrom, args)
2024-04-27 11:07:08 +00:00
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)
2024-04-27 11:07:08 +00:00
async def cmd_budget_info(self, mroom, mfrom, args):
"""
Renvoie la liste des membres et la description du budget courant.
2024-04-27 11:07:08 +00:00
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()
2024-04-27 11:07:08 +00:00
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.
2024-04-27 11:07:08 +00:00
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)
2024-04-27 11:07:08 +00:00
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):
2024-04-27 11:07:08 +00:00
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:
2024-04-27 11:07:08 +00:00
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",
2024-04-27 11:07:08 +00:00
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())