aboutsummaryrefslogtreecommitdiff
path: root/geldschieberbot.py
diff options
context:
space:
mode:
Diffstat (limited to 'geldschieberbot.py')
-rw-r--r--geldschieberbot.py584
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()
+