#!/usr/bin/env python3 import json import os import subprocess 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 = {} group_id = os.environ["GSB_GROUP_ID"] send_cmd = os.environ["GSB_SEND_CMD"] """Run without changing the stored state""" dry_run = False def record(sender, recipient, donor, amount): """Apply change to the balance and save it""" # 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: raise TypeError euro[0] = int(euro[0]) if len(euro) < 2: euro.append(0) else: if len(euro[1]) == 1: euro[1] = int(euro[1]) * 10 else: euro[1] = int(euro[1]) return euro[0] * 100 + euro[1] def to_euro(cents): return str(cents/100) def send(msg): subprocess.run(send_cmd.split(' '), input=msg.encode()) def create_summary(user): summary = "" total = 0 p_balances = balance[user] # failes if user is not in balance for person in p_balances: amount = p_balances[person] if amount == 0: continue total -= amount summary += "\t{} {} {}\n".format("<-" if amount < 0 else "->", person, to_euro(abs(amount))) if summary == "": summary = "\tAll fine :)\n" else: summary += "\tBalance: {}".format(to_euro(total)) summary = user + ":\n" + summary return summary def create_total_summary(): summary = "Summary:" for person in balance: summary += '\n' summary += "* " + create_summary(person) return summary def create_members(): r = "" for m in name2num: r += m + ": " + name2num[m] + "\n" return r def create_help(): return """ Usage: send a message starting with '!' followed by a command Commands: ls | list - print all registered members help - print this help message reg name - register the sender with the name: name sum [name] - print a summary split amount person [persons] - split amount between the sender and persons teil amount person [persons] - split amount between the sender and persons schieb amount recipient - give money to recipient 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 fuck - rewind last change Happy Geldschieben! """ def handle_input(inp): for l in inp.splitlines(): message = json.loads(l)["envelope"] sender_number = message["source"] if not "dataMessage" in message: continue else: message = message["dataMessage"] if message["groupInfo"] and message["groupInfo"]["groupId"] != group_id: continue body = message["message"].lower().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") 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": if not sender_number in num2name: send('ERROR: you must register first') continue if len(w) < 3: send('ERROR: not in form "!{} [amount] [name]+"'.format(cmd)) continue try: amount = to_cent(w[1]) except: send("ERROR: amount musst be a number") continue # -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) if sender_number in num2name: recipient = num2name[sender_number] else: send("ERROR: you must register first") continue 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 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 sender = num2name[sender_number] try: amount = to_cent(amount) except: send("ERROR: amount musst be a number") continue if amount < 0: send("ERROR: amount must be positiv") continue if cmd in ["!zieh", "!nimm"]: amount *= -1 # clear change history last_change[sender_number] = [] record(sender_number, 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)) 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 w[2] in name2num: recipient = w[2] elif sender_number in name2num: recipient = num2name[sender_number] 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 # copy list changes = [x for x in last_change[sender_number]] # 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) 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 global last_change if os.path.isfile(last_change_path): last_change = json.load(open(last_change_path, "r")) handle_input(sys.stdin.read()) with open(balance_path, "w") as f: json.dump(balance, f) with open(registration_path, "w") as f: json.dump(name2num, f) with open(last_change_path, "w") as f: json.dump(last_change, f) if __name__ == "__main__": main()