update
This commit is contained in:
parent
a6fbe03895
commit
b312d4305e
642
albus.py
642
albus.py
|
@ -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__)),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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]
|
||||
|
||||
|
|
Loading…
Reference in New Issue