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
import asyncio
from datetime import datetime, timedelta
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
@ -28,51 +32,90 @@ LEVEL_ERROR = 1
EME_NS = 'eu.siacs.conversations.axolotl'
SQL_INIT = """
CREATE TABLE rooms_to_join(
room TEXT UNIQUE);
CREATE TABLE budgets(
id INTEGER PRIMARY KEY,
room TEXT UNIQUE,
descr TEXT
);
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,
budget INTEGER,
jid TEXT,
created_at INTEGER,
budget INTEGER NOT NULL,
name TEXT,
active INTEGER,
FOREIGN KEY(budget)
REFERENCES budgets(id)
ON UPDATE CASCADE
ON DELETE CASCADE
);
ON DELETE CASCADE);
CREATE INDEX members_by_budget
ON members(budget);
CREATE TABLE bills(
id INTEGER PRIMARY KEY,
payer INTEGER,
amount INTEGER,
descr TEXT,
date INTEGER,
c_date INTEGER,
FOREIGN KEY(payer)
REFERENCES members(id)
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
);
ON DELETE CASCADE,
FOREIGN KEY(payer)
REFERENCES members(id));
CREATE TABLE bill_owers(
bill INTEGER,
ower INTEGER,
weight INTEGER,
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(ower)
REFERENCES members(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."""
@ -83,11 +126,11 @@ class Albus(ClientXMPP):
def __init__(self, jid, password, db_path):
ClientXMPP.__init__(self, jid, password)
self.db_path = db_path
self.db = None
self.db_path = db_path
self.auto_authorize = True
self.auto_subscribe = True
self.auto_subscribe = False
self.room_info = {}
@ -102,7 +145,14 @@ class Albus(ClientXMPP):
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:
@ -117,46 +167,50 @@ class Albus(ClientXMPP):
self.xep_0384 = self['xep_0384']
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)
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):
log.info('Initializing db')
await self.db.executescript(SQL_INIT)
await self.db.commit()
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.db.execute('INSERT INTO rooms_to_join (room) VALUES (?)', (mroom,))
await self.db.commit()
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', 'none'):
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':
if mtype == 'available' and maffil in ('owner', 'admin', 'member'):
members.add(mjid)
elif mtype == 'unavailable' and maffil in ('none', 'outcast'):
else:
members.discard(mjid)
async def on_muc_message(self, msg):
@ -188,7 +242,7 @@ class Albus(ClientXMPP):
m = self.cmd_extr.match(body)
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
cmd = m.group('cmd')
@ -196,28 +250,471 @@ class Albus(ClientXMPP):
h = self.cmd_handlers.get(cmd)
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
await h(mroom, mfrom, args)
async def cmd_budget_init(self, mroom, mfrom, descr):
async with self.db.cursor() as c:
await c.execute('INSERT INTO budgets(room, descr) VALUES (?, ?)', (mroom, descr))
b_id = c.lastrowid
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')
members = set()
for m in self.room_info[mroom]:
if m.bare == self.boundjid.bare or m.bare in members:
continue
members.add(m.bare)
await c.execute('INSERT INTO members(budget, jid) VALUES (?, ?)', (b_id, m.bare))
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()
line1 = 'Nouveau budget (%03i): %s\n' % (b_id, descr)
line2 = 'Membres: ' + ', '.join(m.split('@')[0] for m in members)
await self.room_send(mroom, line1 + line2)
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
@ -242,7 +739,7 @@ class Albus(ClientXMPP):
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)
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,):
@ -276,13 +773,14 @@ class Albus(ClientXMPP):
except EncryptionPrepareException as exn:
for error in exn.errors:
if isinstance(error, MissingBundleException):
err = f'attention: le chiffrement a échoué pour %s (missing-bundle)' % error.bare_jid
self.plain_send(mto, mtype, err)
jid = JID(error.bare_jid)
device_list = expect_problems.setdefault(jid, [])
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:
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
except Exception as 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",
help="JID to use")
parser.add_argument("-p", "--password", dest="password",
help="password to use", default="occino3-adtriana0")
help="password to use")
DATA_DIR = os.path.join(
os.path.dirname(os.path.abspath(__file__)),

View File

@ -1,4 +1,4 @@
babel >= 2.14.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-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
# thanks to github.com/mlaily for the POC
import uuid
import sys
@ -7,7 +9,7 @@ import requests
from Crypto.PublicKey import RSA
if len(sys.argv) != 2:
print('usage: %s TRICOUNT_ID' % sys.argv[0])
print(f'usage: {sys.argv[0]} TRICOUNT_ID')
sys.exit(1)
tricount_id = sys.argv[1]