diff options
Diffstat (limited to 'geldschieberbot.py')
| -rw-r--r-- | geldschieberbot.py | 584 |
1 files changed, 342 insertions, 242 deletions
diff --git a/geldschieberbot.py b/geldschieberbot.py index 7433bb0..87b3355 100644 --- a/geldschieberbot.py +++ b/geldschieberbot.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 +from datetime import datetime +from datetime import timedelta import json import os import subprocess @@ -7,41 +9,57 @@ import sys import tanken -"""Dict of dicts associating a second person to an amount""" -balance = {} - -name2num = {} -num2name = {} - -"""Dict associating users with their last change""" -last_change = {} +"""Path where our data is stored persistent on disk""" +state_file = os.environ["GSB_STATE_FILE"] + +if os.path.isfile(state_file): + state = json.load(open(state_file, "r")) +else: + """Dict containing the whole state of geldschieberbot + balance - dict of dicts associating two persons to an amount + name2num, num2name - dicts associating numbers to names and vice versa + scheduled_cmds - dict associating names to cmds, their schedule, and the last execution + changes - dict associating users with their changes""" + state = { + "balance" : {}, + "name2num" : {}, + "num2name" : {}, + "scheduled_cmds" : {}, + "changes" : {}, + } + +balance = state["balance"] +name2num = state["name2num"] +num2name = state["num2name"] +scheduled_cmds = state["scheduled_cmds"] +changes = state["changes"] group_id = os.environ["GSB_GROUP_ID"] send_cmd = os.environ["GSB_SEND_CMD"] +group_send_cmd = send_cmd + group_id """Run without changing the stored state""" dry_run = False -def record(sender, recipient, donor, amount): - """Apply change to the balance and save it""" +"""Run without sending messages""" +quiet = False + +def record(recipient, donor, amount): + """Apply changes to the balance""" # Only change anything if this is not a dry run if not dry_run: balance[donor][recipient] += amount balance[recipient][donor] -= amount - if not sender in last_change: - last_change[sender] = [] - - last_change[sender].append([recipient, donor, amount]) - def to_cent(euro): - euro = euro.split('.') - if len(euro) > 2: + if '.' in euro: + euro = euro.split('.') + else: euro = euro.split(',') - if len(euro) > 2: - raise TypeError + if len(euro) > 2: + raise TypeError euro[0] = int(euro[0]) if len(euro) < 2: euro.append(0) @@ -56,7 +74,8 @@ def to_euro(cents): return str(cents/100) def send(msg): - subprocess.run(send_cmd.split(' '), input=msg.encode()) + if not quiet: + subprocess.run(send_cmd.split(' '), input=msg.encode()) def create_summary(user): summary = "" @@ -80,7 +99,7 @@ def create_total_summary(): for person in balance: summary += '\n' - summary += "* " + create_summary(person) + summary += create_summary(person) return summary @@ -107,266 +126,347 @@ gib amount recipient - give money to recipient zieh amount donor - get money from donor nimm amount donor - get money from donor -tanken amount person [info] - calculate fuel costs and add them to the balance +tanken amount [person] [info] - calculate fuel costs and add them to the balance fuck - rewind last change +weekly name cmd - repeat cmd each week +monthly name cmd - repeat cmd each month +yearly name cmd - repeat cmd each year +cancel name - stop repeating cmd + Happy Geldschieben! """ -def handle_input(inp): - for l in inp.splitlines(): - message = json.loads(l)["envelope"] - - sender_number = message["source"] - if not message["dataMessage"]: - continue - else: - message = message["dataMessage"] - if message["groupInfo"] and message["groupInfo"]["groupId"] != group_id: - continue - body = [l.strip().lower() for l in message["message"].splitlines()] - - w = body[0].split(' ') - - cmd = w[0] - - global last_change - - # supported commands are: - # "!reg" register a name for this number - # "!sum" send a summary to the group - # "!list" "!ls" list members - # "!help" print all commands - # "!split" "!teil" split amount between group - # "!schieb" "!gib" give money to somebody - # "!zieh" "!nimm" add debt of somebody - # "!tanken" calculate fuel cost parts - # "!fuck" rewind last change - if cmd == "!reg": - if len(w) != 2: - send('ERROR: not in form "!reg name"') - continue - - if w[1] in name2num: - send("ERROR: name already registered") - elif sender_number in num2name: - send("ERROR: you are already registered") +cmds = {} + +def register(sender, args, msg): + if len(args) != 2: + send('ERROR: not in form "{} name"'.format(args[0])) + return 1 + name = args[1] + + if name in name2num: + send("ERROR: {} already registered".format(name)) + return 1 + + if sender in num2name: + send("ERROR: you are already registered") + return 1 + + num2name[sender] = name + name2num[name] = sender + + # add to balance + nb = {} + for m in balance: + balance[m][name] = 0 + nb[m] = 0 + balance[name] = nb + + # add changes list + changes[name] = [] + send("Happy geldschiebing {}!".format(name)) + return 0 + +cmds["reg"] = register +cmds["register"] = register + +def summary(sender, args, msg): + if len(args) == 1: + send(create_total_summary()) + elif len(args) > 1: + ret = 0 + msg = "Summary:\n" + for name in args[1:]: + if name in name2num: + msg += create_summary(name) + "\n" else: - num2name[sender_number] = w[1] - name2num[w[1]] = sender_number - - # add to balance - nm = {} - for m in balance: - balance[m][w[1]] = 0 - nm[m] = 0 - balance[w[1]] = nm - - elif cmd == "!sum": - if len(w) == 1: - send(create_total_summary()) - elif len(w) == 2: - if w[1] in name2num: - send("Summary:\n" + create_summary(w[1])) - else: - send("ERROR: name not registered") - else: - send('ERROR: not in form "!sum [name]"') - - elif cmd == "!list" or cmd == "!ls": - send(create_members()) - - elif cmd == "!help": - send(create_help()) - - elif cmd == "!split" or cmd == "!teil": + msg += 'ERROR: name "{}" not registered'.format(name) + "\n" + ret = 1 + send(msg) + return ret - if not sender_number in num2name: - send('ERROR: you must register first') - continue +cmds["sum"] = summary +cmds["summary"] = summary - if len(w) < 3: - send('ERROR: not in form "!{} [amount] [name]+"'.format(cmd)) - continue +def list_users(sender, args, msg): + send(create_members()) - try: - amount = to_cent(w[1]) - except: - send("ERROR: amount musst be a number") - continue +cmds["ls"] = list_users +cmds["list"] = list_users - # -2 because amount and cmd; +1 because the sender is part of the group - persons = len(w) - 2 + 1 - amount_per_person = int(amount/persons) +def usage(sender, args, msg): + send(create_help()) + return 0 - if sender_number in num2name: - recipient = num2name[sender_number] - else: - send("ERROR: you must register first") - continue +cmds["help"] = usage +cmds["usage"] = usage - msg = "Split {} between {} -> {} each\n".format(to_euro(amount), - persons, - to_euro(amount_per_person)) - # clear change history - last_change[sender_number] = [] - for p in w[2:]: - if not p in name2num: - msg += p + " not known. Please take care manually\n" - else: - record(sender_number, recipient, p, amount_per_person) - - msg += "New Balance:\n" - msg += create_summary(recipient) - send(msg) - - elif cmd in ["!schieb", "!gib", "!zieh", "!nimm"]: - if len(w) != 3: - send('ERROR: not in form "!{} amount recipient"'.format(cmd)) - continue - - if not sender_number in num2name: - send('ERROR: you must register first') - continue +def split(sender, args, msg): + if not sender in num2name: + send('ERROR: you must register first') + return 1 - if w[1] in name2num: - recipient = w[1] - amount = w[2] - elif w[2] in name2num: - recipient = w[2] - amount = w[1] - else: - send('ERROR: recipient not known') - continue + if len(args) < 3: + send('ERROR: not in form "{} amount [name]+"'.format(args[0])) + return 1 - sender = num2name[sender_number] + try: + amount = to_cent(args[1]) + except: + send("ERROR: amount must be a number") + return 1 - try: - amount = to_cent(amount) - except: - send("ERROR: amount musst be a number") - continue + # len(args) - cmd - amount + sender + persons = len(args) - 2 + 1 + amount_per_person = int(amount/persons) - if amount < 0: - send("ERROR: amount must be positiv") - continue + if sender in num2name: + recipient = num2name[sender] + else: + send("ERROR: you must register first") + return 1 + + output = "Split {} between {} -> {} each\n".format(to_euro(amount), + persons, + to_euro(amount_per_person)) + change = [args] + for p in args[2:]: + if not p in name2num: + output += p + " not known. Please take care manually\n" + else: + record(recipient, p, amount_per_person) + change.append([recipient, p, amount_per_person]) - if cmd in ["!zieh", "!nimm"]: - amount *= -1 + changes[recipient].append(change) - # clear change history - last_change[sender_number] = [] + output += "New Balance:\n" + output += create_summary(recipient) + send(output) + return 0 - record(sender_number, sender, recipient, amount) +cmds["split"] = split +cmds["teil"] = split - p_balance = balance[sender][recipient] +def transaction(sender, args, msg): + if len(args) != 3: + send('ERROR: not in form "{} amount recipient"'.format(args[0])) + return 1 - send("New Balance: {} {} {} {}\n".format(sender, - ("->" if p_balance > 0 else "<-"), - to_euro(abs(p_balance)), - recipient)) - - elif cmd == "!tanken": - if len(w) < 3: - send('ERROR: !tanken not in form "!tanken amount person [info]"') - continue - try: - amount = to_cent(w[1]) - except: - send("ERROR: amount musst be a number") - continue + if not sender in num2name: + send('ERROR: you must register first') + return 1 + else: + sender = num2name[sender] - if w[2] in name2num: - recipient = w[2] - elif sender_number in name2num: - recipient = num2name[sender_number] + if args[1] in name2num: + recipient, amount = args[1:3] + elif args[2] in name2num: + amount, recipient = args[1:3] + else: + send('ERROR: recipient not known') + return 1 + + try: + amount = to_cent(amount) + except: + send("ERROR: amount must be a number") + return 1 + + if amount < 0: + send("ERROR: amount must be positiv") + return 1 + + if args[0] in ["!zieh", "!nimm"]: + amount *= -1 + + changes[sender].append([args, [sender, recipient, amount]]) + + record(sender, recipient, amount) + + p_balance = balance[sender][recipient] + + send("New Balance: {} {} {} {}\n".format(sender, + ("->" if p_balance > 0 else "<-"), + to_euro(abs(p_balance)), + recipient)) + return 0 + +cmds["schieb"] = transaction +cmds["gib"] = transaction +cmds["zieh"] = transaction +cmds["nimm"] = transaction + +def tanken(sender, args, msg): + if len(args) < 2: + send('ERROR: not in form "{} amount [person] [info]"'.format(args[0])) + return 1 + try: + amount = to_cent(args[1]) + except: + send("ERROR: amount must be a number") + return 1 + + if len(args) > 2 and args[2] in name2num: + recipient = args[2] + elif sender in name2num: + recipient = num2name[sender] + else: + send("ERROR: recipient unknown") + return 1 + + parts, err = tanken.tanken(msg.splitlines()[1:], amount) + + if err != None: + send("ERROR: " + err) + return 1 + + output = "" + change = [args] + for p in parts.items(): + output += p[0] + ": {}km = {}\n".format(p[1][0], to_euro(p[1][1])) + if p[0] != recipient: + if p[0] in name2num: + record(recipient, p[0], p[1][1]) + change.append([recipient, p[0], p[1][1]]) else: - send("ERROR: recipient unknown") - continue - - parts, err = tanken.tanken(body[1:], amount) - - if err != None: - send("ERROR: " + err) - continue - - # clear change history - last_change[sender_number] = [] - - msg = "" - for p in parts.items(): - msg += p[0] + ": {}km = {}\n".format(p[1][0], to_euro(p[1][1])) - if p[0] != recipient: - if p[0] in name2num: - record(sender_number, recipient, p[0], p[1][1]) - else: - msg += p[0] + " not known. Please take care manually\n" - - msg += "New Balance:\n" - msg += create_summary(recipient) - send(msg) - - elif cmd == "!fuck": - if not sender_number in num2name: - send("ERROR: you must register first") - continue - - if not sender_number in last_change: - send("Nothing to rewind") - continue + output += p[0] + " not known. Please take care manually\n" - # copy list - changes = [x for x in last_change[sender_number]] + output += "New Balance:\n" + output += create_summary(recipient) + send(output) + return 0 - # clear change history - last_change[sender_number] = [] - - msg = num2name[sender_number] + ": sorry I fucked up!\n Rewinding:\n" - for change in changes: - msg += "{} -> {} {}\n".format(change[0], change[1], to_euro(change[2])) - record(sender_number, change[1], change[0], change[2]) - - send(msg) +cmds["tanken"] = tanken +#TODO +def fuck(sender, args, msg): + if not sender in num2name: + send("ERROR: you must register first") + return 1 + else: + name = num2name[sender] + + if len(changes[name]) == 0: + send("Nothing to rewind") + return 1 + + # pop last item + last_changes = changes[name].pop() + args, last_changes = last_changes[0], last_changes[1:] + + output = name + ": sorry I fucked up!\nRewinding:\n" + output += ' '.join(args) + "\n" + for change in last_changes: + output += "{} {} {} {}\n".format(change[0], + ("->" if change[2] < 0 else "<-"), + to_euro(abs(change[2])), + change[1]) + record(change[1], change[0], change[2]) + + send(output) + return 0 + +cmds["fuck"] = fuck +cmds["rewind"] = fuck +cmds["undo"] = fuck + +def schedule(sender, args, msg): + if not sender in num2name: + send("ERROR: you must register first") + return 1 + + if args < 3: + send('ERROR: not in form "{} name cmd"'.format(args[0])) + return 1 + + name = args[1] + cmd = args[2:] + + if name in scheduled_cmds: + send('ERROR: there is already a scheduled command named "{}"'.format(name)) + return 1 + + # Test the command + global dry_run + old_dry_run, dry_run = dry_run, True + global quiet + old_quiet, quiet = quiet, True + ret = cmds[cmd[0]](sender, cmd, "") + quiet = old_quiet + dry_run = old_dry_run + + if ret: + send('ERROR: the command "{}" failed and will not be recorded') + return 1 + + scheduled_cmd = {"schedule": args[0][0], + "last_time": None, + "sender": sender, + "cmd": cmd} + + scheduled_cmds[name] = scheduled_cmd + send('Recorded the {} command "{}" as {}'.format(args[0], cmd, name)) + +cmds["weekly"] = schedule +cmds["monthly"] = schedule +cmds["yearly"] = schedule def main(): - if len(sys.argv) > 1 and sys.argv[1] in ["-d", "--dry-run"]: global dry_run dry_run = True print("Dry Run no changes will apply!") - store_path = os.environ["GSB_STORE_PATH"] - balance_path = store_path + "/balance.json" - registration_path = store_path + "/registration.json" - last_change_path = store_path + "/last_change.json" - - global balance - if os.path.isfile(balance_path): - balance = json.load(open(balance_path, "r")) - - global name2num - global num2name - if os.path.isfile(registration_path): - name2num = json.load(open(registration_path, "r")) - for name in name2num: - num2name[name2num[name]] = name + # Read cmds from stdin + for l in sys.stdin.read().splitlines(): + message = json.loads(l)["envelope"] - global last_change - if os.path.isfile(last_change_path): - last_change = json.load(open(last_change_path, "r")) + sender_number = message["source"] + if not message["dataMessage"]: + continue + else: + message = message["dataMessage"] + if message["groupInfo"] and message["groupInfo"]["groupId"] != group_id: + continue + body = message["message"].lower().splitlines() - handle_input(sys.stdin.read()) + w = body[0].split(' ') - with open(balance_path, "w") as f: - json.dump(balance, f) + if w[0].startswith("!"): + cmd = w[0][1:] + if cmd in cmds: + cmds[cmd](sender_number, w, body) + else: + send('ERROR: unknown cmd. Enter !help for a list of commands.') + + # Handle scheduled commands + now = datetime.now() + day_delta = timedelta(60 * 60 * 24) + week_delta = day_delta * 7 + year_delta = day_delta * 365 + for name, cmd in scheduled_cmds.items(): + run = False + last_time = datetime.fromtimestamp(cmd.last_time) + + if cmd.schedule == "y": + run = now - last_time > year_delta + elif cmd.schedule == "w": + run = now.month > last_time.month and now.day >= last_time.day + + run = run or last_time.month == 12 and now.year > last_time.year\ + and now.day >= last_time.day + else: + run = now - last_time > week_delta - with open(registration_path, "w") as f: - json.dump(name2num, f) + if run: + cmds[cmd.cmd[0]](cmd.sender, cmd.cmd, "") + cmd["last_time"] = now.timestamp() - with open(last_change_path, "w") as f: - json.dump(last_change, f) + with open(state_file, "w") as f: + json.dump(state, f) if __name__ == "__main__": main() + |
