This commit is contained in:
lapinot 2024-04-27 13:07:08 +02:00
parent a6fbe03895
commit b312d4305e
4 changed files with 656 additions and 74 deletions

642
albus.py
View File

@ -3,14 +3,18 @@
from argparse import ArgumentParser from argparse import ArgumentParser
import asyncio import asyncio
from datetime import datetime, timedelta from collections import defaultdict
from datetime import datetime, timedelta, timezone
from getpass import getpass from getpass import getpass
import logging import logging
import os import os
import re import re
import sys import sys
import textwrap
import zoneinfo
import aiosqlite import aiosqlite
import babel.dates
from slixmpp import ClientXMPP, JID from slixmpp import ClientXMPP, JID
from slixmpp.exceptions import IqTimeout, IqError from slixmpp.exceptions import IqTimeout, IqError
@ -28,51 +32,90 @@ LEVEL_ERROR = 1
EME_NS = 'eu.siacs.conversations.axolotl' EME_NS = 'eu.siacs.conversations.axolotl'
SQL_INIT = """ SQL_INIT = """
CREATE TABLE rooms_to_join(
room TEXT UNIQUE);
CREATE TABLE budgets( CREATE TABLE budgets(
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
room TEXT UNIQUE, created_at INTEGER,
descr TEXT room TEXT NOT NULL,
); descr TEXT);
CREATE UNIQUE INDEX budget_by_room
ON budgets(room);
CREATE TABLE members( CREATE TABLE members(
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
budget INTEGER, created_at INTEGER,
jid TEXT, budget INTEGER NOT NULL,
name TEXT,
active INTEGER,
FOREIGN KEY(budget) FOREIGN KEY(budget)
REFERENCES budgets(id) REFERENCES budgets(id)
ON UPDATE CASCADE ON UPDATE CASCADE
ON DELETE CASCADE ON DELETE CASCADE);
);
CREATE INDEX members_by_budget
ON members(budget);
CREATE TABLE bills( CREATE TABLE bills(
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
payer INTEGER, budget INTEGER NOT NULL,
amount INTEGER, payer INTEGER NOT NULL,
descr TEXT, amount REAL NOT NULL,
date INTEGER, payed_at INTEGER NOT NULL,
c_date INTEGER, created_at INTEGER NOT NULL,
FOREIGN KEY(payer) type INTEGER NOT NULL,
REFERENCES members(id) desc TEXT,
tags TEXT,
FOREIGN KEY(budget)
REFERENCES budgets(id)
ON UPDATE CASCADE ON UPDATE CASCADE
ON DELETE CASCADE ON DELETE CASCADE,
); FOREIGN KEY(payer)
REFERENCES members(id));
CREATE TABLE bill_owers( CREATE INDEX bills_by_budget
bill INTEGER, ON bills(budget);
ower INTEGER,
weight INTEGER, CREATE TABLE bill_allocs(
bill INTEGER NOT NULL,
member INTEGER,
weight REAL,
FOREIGN KEY(bill) FOREIGN KEY(bill)
REFERENCES bills(id) REFERENCES bills(id)
ON UPDATE CASCADE ON UPDATE CASCADE
ON DELETE CASCADE, ON DELETE CASCADE,
FOREIGN KEY(ower) FOREIGN KEY(member)
REFERENCES members(id) REFERENCES members(id),
ON UPDATE CASCADE UNIQUE(bill, member));
ON DELETE CASCADE
);""" 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): class Albus(ClientXMPP):
"""Albus, le bot-chat.""" """Albus, le bot-chat."""
@ -83,11 +126,11 @@ class Albus(ClientXMPP):
def __init__(self, jid, password, db_path): def __init__(self, jid, password, db_path):
ClientXMPP.__init__(self, jid, password) ClientXMPP.__init__(self, jid, password)
self.db_path = db_path
self.db = None self.db = None
self.db_path = db_path
self.auto_authorize = True self.auto_authorize = True
self.auto_subscribe = True self.auto_subscribe = False
self.room_info = {} self.room_info = {}
@ -102,7 +145,14 @@ class Albus(ClientXMPP):
self.cmd_handlers = {} self.cmd_handlers = {}
self.cmd_handlers['budget-init'] = self.cmd_budget_init 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): async def on_exit(self):
if self.db is not None: if self.db is not None:
@ -117,46 +167,50 @@ class Albus(ClientXMPP):
self.xep_0384 = self['xep_0384'] self.xep_0384 = self['xep_0384']
self.xep_0380 = self['xep_0380'] self.xep_0380 = self['xep_0380']
log.info('Loading db: %s' % self.db_path) log.info('Loading database from: %s' % self.db_path)
self.db = await aiosqlite.connect(self.db_path) self.db = await aiosqlite.connect(self.db_path)
async with self.db.execute("SELECT name FROM sqlite_master WHERE type='table' and name='rooms_to_join'") as c:
r = await c.fetchone()
if r is None:
await self.db_init()
else:
async with self.db.execute('SELECT room FROM rooms_to_join') as c:
async for (room,) in c:
await self.process_join(JID(room))
async def db_init(self): async with self.db.execute('PRAGMA user_version') as c:
log.info('Initializing db') (user_version,) = await c.fetchone()
await self.db.executescript(SQL_INIT) if user_version == 0:
await self.db.commit() 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): async def on_direct_invite(self, msg):
"""Accept all MUC invites."""
mroom = msg['groupchat_invite']['jid'] mroom = msg['groupchat_invite']['jid']
await self.db.execute('INSERT INTO rooms_to_join (room) VALUES (?)', (mroom,))
await self.db.commit()
await self.process_join(mroom) await self.process_join(mroom)
async def process_join(self, mroom): async def process_join(self, mroom):
"""Join MUC and start tracking room affiliations."""
log.info('Joining room: %s' % mroom) log.info('Joining room: %s' % mroom)
await self.xep_0045.join_muc_wait(mroom, self.nick, seconds=0) await self.xep_0045.join_muc_wait(mroom, self.nick, seconds=0)
members = self.room_info.setdefault(mroom, set()) members = self.room_info.setdefault(mroom, set())
for a in ('owner', 'admin', 'member', 'none'): for a in ('owner', 'admin', 'member'):
for j in await self.xep_0045.get_affiliation_list(mroom, a): for j in await self.xep_0045.get_affiliation_list(mroom, a):
members.add(JID(j)) members.add(JID(j))
async def on_muc_presence(self, msg): async def on_muc_presence(self, msg):
"""Track room affiliations (required for encryption)."""
mtype = msg['type'] mtype = msg['type']
mroom = msg['muc']['room'] mroom = msg['muc']['room']
maffil = msg['muc']['affiliation'] maffil = msg['muc']['affiliation']
# normalize jid (without device part)
mjid = JID(msg['muc']['jid'].bare) mjid = JID(msg['muc']['jid'].bare)
members = self.room_info.setdefault(mroom, set()) members = self.room_info.setdefault(mroom, set())
if mtype == 'available': if mtype == 'available' and maffil in ('owner', 'admin', 'member'):
members.add(mjid) members.add(mjid)
elif mtype == 'unavailable' and maffil in ('none', 'outcast'): else:
members.discard(mjid) members.discard(mjid)
async def on_muc_message(self, msg): async def on_muc_message(self, msg):
@ -188,7 +242,7 @@ class Albus(ClientXMPP):
m = self.cmd_extr.match(body) m = self.cmd_extr.match(body)
if m is None: if m is None:
await self.room_send(mroom, 'erreur: la commande est mal formée') await self.room_send(mroom, 'erreur: la commande est mal formée\nvoir: !help')
return return
cmd = m.group('cmd') cmd = m.group('cmd')
@ -196,28 +250,471 @@ class Albus(ClientXMPP):
h = self.cmd_handlers.get(cmd) h = self.cmd_handlers.get(cmd)
if h is None: if h is None:
await self.room_send(mroom, 'erreur: je ne connais pas la commande "%s"' % cmd) await self.room_send(mroom, 'erreur: je ne connais pas la commande: %s\nvoir: !help' % cmd)
return return
await h(mroom, mfrom, args) await h(mroom, mfrom, args)
async def cmd_budget_init(self, mroom, mfrom, descr): async def budget_descr_string(self, b_id=None, room=None):
async with self.db.cursor() as c: if b_id is None:
await c.execute('INSERT INTO budgets(room, descr) VALUES (?, ?)', (mroom, descr)) if room is None:
b_id = c.lastrowid 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')
members = set() lines = ['description: %s' % b_descr]
for m in self.room_info[mroom]: async with self.db.execute(
if m.bare == self.boundjid.bare or m.bare in members: 'SELECT id, created_at, name, active FROM members WHERE budget = ?',
continue (b_id,)) as c:
members.add(m.bare) async for (m_id, ts, name, act) in c:
await c.execute('INSERT INTO members(budget, jid) VALUES (?, ?)', (b_id, m.bare)) 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() await self.db.commit()
line1 = 'Nouveau budget (%03i): %s\n' % (b_id, descr) msg = await self.budget_descr_string(b_id=b_id)
line2 = 'Membres: ' + ', '.join(m.split('@')[0] for m in members) await self.room_send(mroom, msg)
await self.room_send(mroom, line1 + line2)
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): async def process_message(self, mfrom, body):
pass pass
@ -242,7 +739,7 @@ class Albus(ClientXMPP):
await self.plain_send(eto, etype, 'erreur: le déchiffrement a échoué (no-available-session)') await self.plain_send(eto, etype, 'erreur: le déchiffrement a échoué (no-available-session)')
except (UndecidedException,) as exn: except (UndecidedException,) as exn:
await self.xep_0384.trust(exn.bare_jid, exn.device, exn.ik) await self.xep_0384.trust(exn.bare_jid, exn.device, exn.ik)
return await self.decrypt_message(msg) return await self.decrypt_message(msg, mfrom, eto, etype)
except (UntrustedException,) as exn: except (UntrustedException,) as exn:
await self.plain_send(eto, etype, 'erreur: le déchiffrement a échoué (untrusted-key)') await self.plain_send(eto, etype, 'erreur: le déchiffrement a échoué (untrusted-key)')
except (EncryptionPrepareException,): except (EncryptionPrepareException,):
@ -276,13 +773,14 @@ class Albus(ClientXMPP):
except EncryptionPrepareException as exn: except EncryptionPrepareException as exn:
for error in exn.errors: for error in exn.errors:
if isinstance(error, MissingBundleException): if isinstance(error, MissingBundleException):
err = f'attention: le chiffrement a échoué pour %s (missing-bundle)' % error.bare_jid err = 'attention: le chiffrement a échoué pour %s (missing-bundle)'
self.plain_send(mto, mtype, err) self.plain_send(mto, mtype, err % error.bare_jid)
jid = JID(error.bare_jid) device_list = expect_problems.setdefault(JID(error.bare_jid), [])
device_list = expect_problems.setdefault(jid, [])
device_list.append(error.device) device_list.append(error.device)
except (IqError, IqTimeout) as exn: except (IqError, IqTimeout) as exn:
self.plain_send(mto, mtype, 'erreur: je n\'arrive pas à récupérer les informations de chiffrement pour un participant') err = ("erreur: je n'arrive pas à récupérer les informations "
"de chiffrement pour un participant")
self.plain_send(mto, mtype, err)
return return
except Exception as exn: except Exception as exn:
await self.plain_send(mto, mtype, 'erreur: petit soucis...\n%r' % exn) await self.plain_send(mto, mtype, 'erreur: petit soucis...\n%r' % exn)
@ -301,7 +799,7 @@ if __name__ == '__main__':
parser.add_argument("-j", "--jid", dest="jid", parser.add_argument("-j", "--jid", dest="jid",
help="JID to use") help="JID to use")
parser.add_argument("-p", "--password", dest="password", parser.add_argument("-p", "--password", dest="password",
help="password to use", default="occino3-adtriana0") help="password to use")
DATA_DIR = os.path.join( DATA_DIR = os.path.join(
os.path.dirname(os.path.abspath(__file__)), os.path.dirname(os.path.abspath(__file__)),

View File

@ -1,4 +1,4 @@
babel >= 2.14.0
aiosqlite >= 0.19.0 aiosqlite >= 0.19.0
sqlalchemy >= 2.0.0
-e git+https://codeberg.org/poezio/slixmpp.git@slix-1.8.5#egg=slixmpp -e git+https://codeberg.org/poezio/slixmpp.git@slix-1.8.5#egg=slixmpp
-e git+https://codeberg.org/poezio/slixmpp-omemo.git@v0.9.1#egg=slixmpp-omemo -e git+https://codeberg.org/poezio/slixmpp-omemo.git@v0.9.1#egg=slixmpp-omemo

82
scripts/load_db.py Normal file
View File

@ -0,0 +1,82 @@
from argparse import ArgumentParser
from datetime import datetime, timezone
import json
import sqlite3
import sys
def load_isodate(s):
return datetime.fromisoformat(s).replace(microsecond=0).timestamp()
def load_bill(data, user_map, b_id, db, after=None):
if data['kind'] == 'NORMAL':
btype = 0
elif data['kind'] == 'BALANCE':
btype = 1
else:
raise ValueError('bad bill kind: %s' % data['kind'])
c_date = load_isodate(data['created_at'])
p_date = load_isodate(data['payed_at'])
desc = data['descr']
amount = -float(data['amount'])
payer = user_map[data['payed_by']['id']]
if after is not None and c_date < after:
return
c = db.execute(
'INSERT INTO bills(budget, payer, amount, payed_at, created_at, type, desc) '
'VALUES (?, ?, ?, ?, ?, ?, ?)',
(b_id, payer, amount, p_date, c_date, btype, desc))
t_id = c.lastrowid
for a in data['alloc']:
ower = user_map[a['ower']['id']]
weight = a['weight']
if not weight:
continue
weight = float(weight)
db.execute('INSERT INTO bill_allocs(bill, member, weight) VALUES (?, ?, ?)',
(t_id, ower, weight))
if __name__ == '__main__':
parser = ArgumentParser(description='load tricount dump into albus')
parser.add_argument("-u", "--user", help="TRICOUNT_ID:ALBUS_ID mapping",
action="append", dest="user_map")
parser.add_argument("-b", "--budget", help="albus budget ID",
action="store", required=True)
parser.add_argument("-i", "--input", help="input json dump",
action="store", required=True)
parser.add_argument("--db", help="db path", action="store", required=True)
parser.add_argument("--after", help="date filter",
action="store")
args = parser.parse_args()
if args.after is not None:
after = datetime.fromisoformat(args.after)
else:
after = None
user_map = {}
for opt in args.user_map:
(t,a) = opt.split(':')
user_map[int(t)] = int(a)
b_id = args.budget
if args.input == '-':
fd = sys.stdin
else:
fd = open(args.input)
data = json.load(fd)
db = sqlite3.connect(args.db)
bills = data['bills']
bills.sort(key=lambda u: datetime.fromisoformat(u['payed_at']).timestamp())
for b in bills:
load_bill(b, user_map, b_id, db, after)
db.commit()
db.close()

View File

@ -1,5 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# thanks to github.com/mlaily for the POC
import uuid import uuid
import sys import sys
@ -7,7 +9,7 @@ import requests
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
if len(sys.argv) != 2: if len(sys.argv) != 2:
print('usage: %s TRICOUNT_ID' % sys.argv[0]) print(f'usage: {sys.argv[0]} TRICOUNT_ID')
sys.exit(1) sys.exit(1)
tricount_id = sys.argv[1] tricount_id = sys.argv[1]