aboutsummaryrefslogtreecommitdiff
path: root/geldschieberbot.py
diff options
context:
space:
mode:
Diffstat (limited to 'geldschieberbot.py')
-rwxr-xr-x[-rw-r--r--]geldschieberbot.py1298
1 files changed, 644 insertions, 654 deletions
diff --git a/geldschieberbot.py b/geldschieberbot.py
index c249dd2..617db51 100644..100755
--- a/geldschieberbot.py
+++ b/geldschieberbot.py
@@ -2,773 +2,763 @@
from datetime import date, datetime, timedelta
import json
+from jsonrpc import JSONRPCResponseManager, dispatcher
import os
-import subprocess
+import re
import sys
+from exceptions import AlreadyRegisteredError, InvalidArgumentError, InvalidAmountError, NotRegisteredError, NotAllowedError
import tanken
-"""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
- cars - dict associating car names to their service charge
- scheduled_cmds - dict associating names to cmds, their schedule, and the last execution
- changes - dict associating users with their changes"""
- state = {
- "balance" : {},
- "name2num" : {},
- "num2name" : {},
- "cars" : {},
- "scheduled_cmds" : {},
- "changes" : {},
- }
-
- # check if a legacy file layout is present
- store_dir = os.path.dirname(state_file)
- if os.path.isfile(os.path.join(store_dir, "balance.json")):
- with open(os.path.join(store_dir, "balance.json"), "r") as f:
- state["balance"] = json.load(f)
-
- with open(os.path.join(store_dir, "registration.json"), "r") as f:
- state["name2num"] = json.load(f)
- for name in state["name2num"]:
- state["num2name"][state["name2num"][name]] = name
-
- with open(os.path.join(store_dir, "last_change.json"), "r") as f:
- state["changes"] = json.load(f)
- for num in state["changes"]:
- name = state["num2name"][num]
- state["changes"][name] = state["changes"][num]
- del(state["changes"][num])
-
-balance = state["balance"]
-name2num = state["name2num"]
-num2name = state["num2name"]
-available_cars = state["cars"]
-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
-
-"""Run without sending messages"""
-quiet = False
-
-"""Should changes be recorded"""
-record_changes = True
-
-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
-
+### HELPERS ###
def to_cent(euro):
- if '.' in euro:
- euro = euro.split('.')
- else:
- 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])
- amount = euro[0] * 100 + euro[1]
- if amount < 0:
- raise ValueError
- return amount
+ """Parse amount into cents"""
+ match = re.match('^(\d+)([,.](\d+))?$', euro)
+ if not match:
+ raise InvalidAmountError(f'"{euro}" not in form <euro>.<cents>')
-def to_euro(cents):
- return f"{cents/100:.2f}"
+ cents = int(match.group(1)) * 100
-def send(msg):
- if not quiet:
- subprocess.run(send_cmd.split(' '), input=msg.encode())
-
-def create_summary(user):
- summary = ""
- cars_summary = ""
- total = 0
- cars_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
- if person in available_cars:
- cars_total -= amount
- cars_summary += f'\t{"<-" if amount < 0 else "->"} {person} {to_euro(abs(amount))}\n'
+ if match.group(3):
+ if len(match.group(3)) > 2:
+ raise InvalidAmountError(f'too precise ({match.group(0)} only two decimals are supported)')
else:
- total -= amount
- summary += f'\t{"<-" if amount < 0 else "->"} {person} {to_euro(abs(amount))}\n'
-
- if not summary:
- summary = "\tAll fine :)"
- else:
- summary += f"\tBalance: {to_euro(total)}"
-
- ret_summary = f'{user}:\n{summary}'
-
- if cars_summary:
- cars_summary += f'\tLiability: {to_euro(cars_total)}'
- ret_summary += f'\n\tCars:\n{cars_summary}'
+ if len(match.group(3)) == 1:
+ cents += int(match.group(3)) * 10
+ else:
+ cents += int(match.group(3))
- return ret_summary
+ return cents
-def create_total_summary():
- summary = "Summary:"
+def to_euro(cents):
+ """Format cents to euros"""
+ return f"{cents/100:.2f}"
- cars_summary = ""
- for person in balance:
- p_summary = create_summary(person)
- if person in available_cars:
- cars_summary += f'\n{p_summary}'
- else:
- summary += f'\n{p_summary}'
-
- if cars_summary:
- summary += f'\nCars:{cars_summary}'
- return summary
-
-def create_members():
- r = ""
- for m in name2num:
- r += m + ": " + name2num[m] + "\n"
- return r
-
-def add_to_balance(name):
- nb = {}
- for m in balance:
- balance[m][name] = 0
- nb[m] = 0
- balance[name] = nb
-
-def create_help():
- return """
-Usage: send a message starting with '!' followed by a command
+USAGE =\
+"""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
+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]
+ alias: teil
+ split amount between the sender and persons
+
+schieb amount recipient -
+ alias: gib
+ give money to recipient
+zieh amount donor
+ alias: nimm
+ get money from donor
+
+transfer amount source destination
+ transfer amount of your balance from source to destination
+
+cars [cmd]
+ interact with the available cars
+cars [list | ls]
+ list available cars and their service charge
+cars add car-name service-charge
+ add new car
+cars new car-name service-charge
+ add new car
+cars pay car-name amount
+ pay a bill for the specified car
+
+tanken amount [person] [car] [info]
+ calculate fuel costs, service charge and add them to the person's and car's balance respectively
+
+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
-transfer amount source destination - transfer amount of your balance from source to destination
-
-cars [cmd] - interact with the available cars
-cars [list | ls] - list available cars and their service charge
-cars add car-name service-charge - add new car
-cars new car-name service-charge - add new car
-cars pay car-name amount - pay a bill for the specified car
-
-tanken amount [person] [car] [info] - calculate fuel costs, service charge and add them to the person's and car's balance respectively
+Happy Geldschieben!
+"""
-fuck - rewind last change
+class Geldschieberbot:
+ """ Geldschieberbot state object
-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
+ Members
+ -------
+ balance - dict of dicts associating two persons to an amount
-Happy Geldschieben!
-"""
+ name2num, num2name - dicts associating numbers to names and vice versa
-cmds = {}
+ cars - dict associating car names to their service charge
-def register(sender, args, msg):
- if len(args) != 2:
- return None, f'not in form "{args[0]} name"'
- name = args[1]
+ scheduled_cmds - dict associating names to cmds, their schedule, and the last execution
- if name in name2num:
- return None, f"{name} already registered"
+ changes - dict associating users with their changes
+ """
+ # Should changes be recorded
+ record_changes = True
+
+ # Don't change the state
+ dry_run = False
+
+ def get_state_path(path):
+ return path or os.environ.get("GSB_STATE_FILE", None) or "state.json"
+
+ def __init__(self, path=None):
+ """Save state to disk"""
+ state_file = Geldschieberbot.get_state_path(path)
+
+ self.cmds = {
+ "reg": self.register,
+ "register": self.register,
+ "sum": self.summary,
+ "summary": self.summary,
+ "ls": self.list_users,
+ "list": self.list_users,
+ "help": self.usage,
+ "usage": self.usage,
+ "split": self.split,
+ "teil": self.split,
+ "schieb": self.transaction,
+ "gib": self.transaction,
+ "zieh": self.transaction,
+ "nimm": self.transaction,
+ "transfer": self.transfer,
+ "cars": self._cars,
+ 'tanken': self._tanken,
+ 'cancel': self.cancel,
+ 'weekly': self.schedule,
+ 'monthly': self.schedule,
+ 'yearly': self.schedule,
+ "fuck": self.fuck,
+ "rewind": self.fuck,
+ "undo": self.fuck,
+ }
+
+ if os.path.isfile(state_file):
+ with open(state_file, "r") as f:
+ for key, value in json.load(f).items():
+ setattr(self, key, value)
+ return
+
+ self.balance = {}
+ self.name2num = {}
+ self.num2name = {}
+ self.cars = {}
+ self.scheduled_cmds = {}
+ self.changes = {}
+
+ def save(self, path=None):
+ """Save state to disk"""
+ if self.dry_run:
+ return
+
+ state_file = Geldschieberbot.get_state_path(path)
+ with open(state_file, "w") as f:
+ json.dump({k: v for k, v in self.__dict__.items() if k != "cmds"}, f)
+
+ def create_summary(self, user):
+ """Create summary string for a single user"""
+ summary = ""
+ cars_summary = ""
+ total = 0
+ cars_total = 0
+ p_balances = self.balance[user] # failes if user is not in balance
+ for person in p_balances:
+ amount = p_balances[person]
+ if amount == 0:
+ continue
+ if person in self.cars:
+ cars_total -= amount
+ cars_summary += f'\t{"<-" if amount < 0 else "->"} {person} {to_euro(abs(amount))}\n'
+ else:
+ total -= amount
+ summary += f'\t{"<-" if amount < 0 else "->"} {person} {to_euro(abs(amount))}\n'
- if sender in num2name:
- return None,"you are already registered"
+ if not summary:
+ summary = "\tAll fine :)"
+ else:
+ summary += f"\tBalance: {to_euro(total)}"
- num2name[sender] = name
- name2num[name] = sender
+ ret_summary = f'{user}:\n{summary}'
- add_to_balance(name)
+ if cars_summary:
+ cars_summary += f'\tLiability: {to_euro(cars_total)}'
+ ret_summary += f'\n\tCars:\n{cars_summary}'
- # add changes list
- changes[name] = []
- return f"Happy geldschiebing {name}!", None
+ return ret_summary
-cmds["reg"] = register
-cmds["register"] = register
+ def create_total_summary(self):
+ """Create summary for all balances"""
+ summary = "Summary:"
-def summary(sender, args, msg):
- if len(args) == 1:
- return create_total_summary(), None
- elif len(args) > 1:
- err = None
- msg = "Summary:\n"
- for name in args[1:]:
- if name in name2num:
- msg += create_summary(name) + "\n"
+ cars_summary = ""
+ for person in self.balance:
+ p_summary = self.create_summary(person)
+ if person in self.cars:
+ cars_summary += f'\n{p_summary}'
else:
- err = f'name "{name}" not registered'
- return msg, err
+ summary += f'\n{p_summary}'
+
+ if cars_summary:
+ summary += f'\nCars:{cars_summary}'
+
+ return summary
+
+ def record(self, recipient, donor, amount):
+ """Apply changes to the balance"""
+ if self.dry_run:
+ return
+ self.balance[donor][recipient] += amount
+ self.balance[recipient][donor] -= amount
+
+ def record_change(self, sender, change):
+ """Add a change for some user"""
+ if self.dry_run or not self.record_changes:
+ return
+ self.changes[sender].append(change)
+
+ def add_to_balance(self, name):
+ """Add new user to our balance"""
+ new_balance = {}
+ for member in self.balance:
+ self.balance[member][name] = 0
+ new_balance[member] = 0
+ self.balance[name] = new_balance
+
+ def add_car(self, car, service_charge):
+ """Add a new car"""
+ if car in self.cars:
+ raise AlreadyRegisteredError(f'"{car}" already registered')
+
+ if car in self.balance:
+ raise AlreadyRegisteredError(f'A user named "{car}" already exists. Please use a different name for this car')
+
+ try:
+ service_charge = to_cent(service_charge)
+ except InvalidAmountError as err:
+ raise InvalidAmountError(f"service-charge ({service_charge}) must be a positive number") from err
+
+ self.cars[car] = service_charge
+ self.add_to_balance(car)
+
+ ### Commands ###
+ def register(self, sender, args, msg):
+ if len(args) != 2:
+ raise InvalidArgumentError(f'\"{" ".join(args)}\" not in form "{args[0]} name"')
+ name = args[1]
+
+ if name in self.name2num:
+ raise AlreadyRegisteredError(f'"{name}" already registered')
+
+ if sender in self.num2name:
+ raise AlreadyRegisteredError(f'you already registered using "{self.num2name[sender]}')
+
+ self.num2name[sender] = name
+ self.name2num[name] = sender
+
+ self.add_to_balance(name)
+
+ # add changes list
+ self.changes[name] = []
+ return f"Happy geldschiebing {name}!"
+
+ def summary(self, sender, args, msg):
+ if len(args) == 1:
+ return self.create_total_summary()
+ elif len(args) > 1:
+ ret = "Summary:\n"
+ for name in args[1:]:
+ if name in self.name2num:
+ ret += self.create_summary(name) + "\n"
+ else:
+ raise NotRegisteredError(f'"{name}" not registered')
+ return ret
-cmds["sum"] = summary
-cmds["summary"] = summary
-def list_users(sender, args, msg):
- return create_members(), None
+ def list_users(self, sender, args, msg):
+ """Create list of all registered members"""
+ users = ""
+ for member in self.name2num:
+ users += f'{member}: {self.name2num[member]}\n'
+ return users
-cmds["ls"] = list_users
-cmds["list"] = list_users
-def usage(sender, args, msg):
- return create_help(), None
+ def usage(self, sender, args, msg):
+ return USAGE
-cmds["help"] = usage
-cmds["usage"] = usage
-def split(sender, args, msg):
- if not sender in num2name:
- return None, 'you must register first'
+ def split(self, sender, args, msg):
+ if not sender in self.num2name:
+ raise NotRegisteredError('please register first using !register')
- if len(args) < 3:
- return None, f'not in form "{args[0]} amount [name]+"'
+ if len(args) < 3:
+ raise InvalidArgumentError(f'\"{" ".join(args)}\" not in form "{args[0]} amount [name]+"')
- try:
amount = to_cent(args[1])
- except:
- return None, "amount must be a positive number"
-
- # len(args) - cmd - amount + sender
- persons = len(args) - 2 + 1
- amount_per_person = int(amount/persons)
-
- if sender in num2name:
- recipient = num2name[sender]
- else:
- return None, "you must register first"
-
- output = f"Split {to_euro(amount)} between {persons} -> {to_euro(amount_per_person)} each\n"
- 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 record_changes and not dry_run:
- changes[recipient].append(change)
+ # len(args) - cmd - amount + sender
+ persons = len(args) - 2 + 1
+ amount_per_person = int(amount/persons)
+ recipient = self.num2name[sender]
- output += "New Balance:\n"
- output += create_summary(recipient)
- return output, None
+ output = f"Split {to_euro(amount)} between {persons} -> {to_euro(amount_per_person)} each\n"
+ change = [args]
+ for p in args[2:]:
+ if not p in self.name2num:
+ output += p + " not known. Please take care manually\n"
+ else:
+ self.record(recipient, p, amount_per_person)
+ change.append([recipient, p, amount_per_person])
-cmds["split"] = split
-cmds["teil"] = split
+ self.record_change(recipient, change)
-def transaction(sender, args, msg):
- if len(args) != 3:
- return None, f'not in form "{args[0]} amount recipient"'
+ output += "New Balance:\n"
+ output += self.create_summary(recipient)
+ return output
- if not sender in balance:
- if sender not in num2name:
- return None, 'you must register first'
- sender = num2name[sender]
- if args[1] in balance:
- recipient, amount = args[1:3]
- elif args[2] in balance:
- amount, recipient = args[1:3]
- else:
- return None, 'recipient not known'
+ def transaction(self, sender, args, msg):
+ if len(args) != 3:
+ raise InvalidArgumentError(f'\"{" ".join(args)}\" not in form "{args[0]} amount recipient"')
- try:
- amount = to_cent(amount)
- except:
- return None, "amount must be a positive number"
+ if sender not in self.balance:
+ if sender not in self.num2name:
+ raise NotRegisteredError('please register first using !register')
+ sender = self.num2name[sender]
- if args[0] in ["!zieh", "!nimm"]:
- amount *= -1
+ if args[1] in self.balance:
+ recipient, amount = args[1:3]
+ elif args[2] in self.balance:
+ amount, recipient = args[1:3]
+ else:
+ raise InvalidArgumentError('unable to determine receipient from {args[1]} or {args[2]}')
- if record_changes and not dry_run:
- changes[sender].append([args, [sender, recipient, amount]])
+ amount = to_cent(amount)
- record(sender, recipient, amount)
+ if args[0] in ["!zieh", "!nimm"]:
+ amount *= -1
- p_balance = balance[sender][recipient]
+ self.record(sender, recipient, amount)
+ self.record_change(sender, ([args, [sender, recipient, amount]]))
- output = ("New Balance: {} {} {} {}\n".format(sender,
- ("->" if p_balance > 0 else "<-"),
- to_euro(abs(p_balance)),
- recipient))
- return output, None
+ p_balance = self.balance[sender][recipient]
-cmds["schieb"] = transaction
-cmds["gib"] = transaction
-cmds["zieh"] = transaction
-cmds["nimm"] = transaction
+ return "New Balance: {} {} {} {}\n".format(sender,
+ ("->" if p_balance > 0 else "<-"),
+ to_euro(abs(p_balance)),
+ recipient)
-def transfer(sender, args, msg):
- if len(args) < 4:
- return None, f'not in form "{args[0]} amount source destination"'
+ def transfer(self, sender, args, msg):
+ if len(args) < 4:
+ raise InvalidArgumentError(f'\"{" ".join(args)}\" not in form "{args[0]} amount source destination"')
- if not sender in num2name:
- return None, 'you must register first'
- else:
- sender = num2name[sender]
+ if sender not in self.num2name:
+ raise NotRegisteredError('please register first using !register')
+ else:
+ sender = self.num2name[sender]
- try:
amount_raw = args[1]
amount_cent = to_cent(amount_raw)
- except:
- return None, "amount must be a positive number"
-
- source, destination = args[2:4]
- if source not in balance:
- return None, f'source "{source}" not known'
-
- elif destination not in balance:
- return None, f'destination "{destination}" not known'
-
- output = ""
- global record_changes
- saved_record_changes = record_changes
- record_changes = False
- change = [args]
-
- ret, err = transaction(sender, ["!zieh", source, amount_raw], "")
- if err:
- # No changes yet we can fail
- return None, err
- else:
- output += ret
- change.append((sender, source, amount_cent))
-
- ret, err = transaction(sender, ["!schieb", destination, amount_raw], "")
- if err:
- output += err + "\nThe balance may be in a inconsistent state please take care manually"
- return output, None
- else:
- output += ret
- change.append((sender, source, amount_cent))
-
- ret, err = transaction(source, ["!zieh", destination, amount_raw], "")
- if err:
- output += err + "\nThe balance may be in a inconsistent state please take care manually"
- return output, None
- else:
- output += ret
- change.append((sender, source, amount_cent))
-
- record_changes = saved_record_changes
- if err is None and record_changes and not dry_run:
- changes[sender].append(change)
-
- return output, None
-
-cmds["transfer"] = transfer
-
-def cars(sender, args, msg):
- # list cars
- if len(args) < 2 or args[1] in ["ls", "list"]:
- if len(available_cars) == 0:
- return "No cars registered yet.", None
-
- ret_msg = ""
-
- if len(args) > 2:
- cars_to_list = args[2:]
- else:
- cars_to_list = available_cars
-
- for car in cars_to_list:
- if car in available_cars:
- ret_msg += f"{car} - service charge {available_cars[car]}ct/km\n"
- ret_msg += create_summary(car) + "\n"
- else:
- return None, f'"{car}" is no available car\n'
- return ret_msg[:-1], None
- # add car
- elif args[1] in ["add", "new"]:
- if len(args) < 4:
- return None, f'not in form "{args[0]} {args[1]} car-name service-charge"'
+ source, destination = args[2:4]
+ if source not in self.balance:
+ raise NotRegisteredError(f'source "{source}" not known')
- car = args[2]
- if car in available_cars:
- return None, '"{}" already registered'.format(car)
+ elif destination not in self.balance:
+ raise NotRegisteredError(f'destination "{destination}" not known')
- if car in balance:
- return None, f'A user named "{car}" already exists. Please use a different name for this car'
+ output = ""
+ saved_record_changes = self.record_changes
+ self.record_changes = False
+ change = [args]
try:
- service_charge = to_cent(args[3])
- except:
- return None, "service-charge must be a positive number"
-
- available_cars[car] = service_charge
- add_to_balance(car)
- return f'added "{car}" as an available car', None
- # pay bill
- elif args[1] in ["pay"]:
- if len(args) < 4:
- return None, f'not in form "{args[0]} {args[1]} car-name amount"'
-
- if not sender in num2name:
- return None, "you must register first"
+ ret = self.transaction(sender, ["!zieh", source, amount_raw], "")
+ except Exception as err:
+ # No changes yet we can fail
+ raise
else:
- sender_name = num2name[sender]
+ output += ret
+ change.append((sender, source, amount_cent))
- car = args[2]
- if car not in available_cars:
- return None, f'car "{car}" not known'
+ try:
+ ret = self.transaction(sender, ["!schieb", destination, amount_raw], "")
+ except Exception as err:
+ output += f"{err}\nThe balance may be in a inconsistent state please take care manually"
+ return output
+ else:
+ output += ret
+ change.append((sender, source, amount_cent))
try:
- amount = to_cent(args[3])
- amount_euro = to_euro(amount)
- except:
- return None, "amount must be a positive number"
+ ret = self.transaction(source, ["!zieh", destination, amount_raw], "")
+ except Exceptoin as err:
+ output += f"{err}\nThe balance may be in a inconsistent state please take care manually"
+ return output
+ else:
+ output += ret
+ change.append((sender, source, amount_cent))
- output = ""
+ self.record_changes = saved_record_changes
+ self.record_change(sender, change)
- global record_changes
- saved_record_changes = record_changes
- record_changes = False
- change = [args]
+ return output
- total_available_charge = 0
- available_charges = []
- for person in balance[car]:
- _amount = balance[car][person]
- if _amount < 0:
- total_available_charge -= _amount
- available_charges.append((person, _amount))
-
- proportion = -1
- if amount < total_available_charge:
- proportion = -1 * (amount / total_available_charge)
-
- _, err = transaction(sender, f"!gib {car} {amount_euro}".split(), "")
- assert(err is None)
- output += f"{sender_name} payed {amount_euro}\n"
-
- # transfere money
- output += f"Transferring {proportion * -100:.2f}% of everybody's charges\n"
- for person, _amount in available_charges:
- if person == sender_name or _amount >= 0:
- continue
+ def _cars(self, sender, args, msg):
+ # list cars
+ if len(args) < 2 or args[1] in ["ls", "list"]:
+ if len(self.cars) == 0:
+ return "No cars registered yet."
- to_move = int(_amount * proportion)
- to_move_euro = to_euro(to_move)
- ret, err = transfer(sender, ["transfer", to_move_euro, car, person], "")
- assert(err is None)
+ ret_msg = ""
- output += "Transfere {} from {} to {}\n".format(to_move_euro, person, sender_name)
+ if len(args) > 2:
+ cars_to_list = args[2:]
+ else:
+ cars_to_list = self.cars
- output += "New Balances:\n"
- output += create_summary(sender_name) + "\n"
- output += create_summary(car)
+ for car in cars_to_list:
+ if car in self.cars:
+ ret_msg += f"{car} - service charge {self.cars[car]}ct/km\n"
+ ret_msg += self.create_summary(car) + "\n"
+ else:
+ raise NotRegisteredError(f'"{car}" is no available car\n')
+
+ return ret_msg[:-1]
+ # add car
+ elif args[1] in ["add", "new"]:
+ if len(args) < 4:
+ raise InvalidArgumentError(f'"{" ".join(args)}" not in form "car {args[1]} car-name service-charge"')
+
+ self.add_car(args[2], args[3])
+ return f'added "{args[2]}" as an available car'
+ # pay bill
+ elif args[1] in ["pay"]:
+ if len(args) < 4:
+ raise InvalidArgumentError(f'"{" ".join(args)}" not in form "car {args[1]} car-name amount"')
+
+ if sender not in self.num2name:
+ raise NotRegisteredError('please register first using !register')
+ else:
+ sender_name = self.num2name[sender]
- record_changes = saved_record_changes
- if record_changes and not dry_run:
- changes[sender_name].append(change)
+ car = args[2]
+ if car not in self.cars:
+ raise NotRegisteredError(f'car "{car}" not known')
- return output, None
- else:
- return None, 'unknown car subcommand "{}".'.format(args[1])
+ amount = to_cent(args[3])
+ amount_euro = to_euro(amount)
-cmds["cars"] = cars
+ output = ""
+
+ saved_record_changes = self.record_changes
+ self.record_changes = False
+ change = [args]
+
+ total_available_charge = 0
+ available_charges = []
+ for person in self.balance[car]:
+ _amount = self.balance[car][person]
+ if _amount < 0:
+ total_available_charge -= _amount
+ available_charges.append((person, _amount))
+
+ proportion = -1
+ if amount < total_available_charge:
+ proportion = -1 * (amount / total_available_charge)
+
+ try:
+ self.transaction(sender, ['!gib', car, amount_euro], "")
+ except Exception:
+ raise
+ output += f"{sender_name} payed {amount_euro}\n"
+
+ # transfer money
+ output += f"Transferring {proportion * -100:.2f}% of everybody's charges\n"
+ for person, _amount in available_charges:
+ if person == sender_name or _amount >= 0:
+ continue
+
+ to_move = int(_amount * proportion)
+ to_move_euro = to_euro(to_move)
+ try:
+ self.transfer(sender, ["transfer", to_move_euro, car, person], "")
+ except Exception:
+ raise
+
+ output += f"Transfere {to_move_euro} from {person} to {sender_name}\n"
+
+ output += "New Balances:\n"
+ output += self.create_summary(sender_name) + "\n"
+ output += self.create_summary(car)
+
+ self.record_changes = saved_record_changes
+ self.record_change(sender_name, change)
+
+ return output
+ else:
+ raise InvalidArgumentError(f'unknown car subcommand "{args[1]}".')
-def _tanken(sender, args, msg):
- if len(args) < 2:
- return None, 'not in form "{} amount [person] [car] [info]"'.format(args[0])
- try:
- amount = to_cent(args[1])
- except:
- return None, "amount must be a number"
-
- # find recipient
- if len(args) > 2 and args[2] in name2num:
- recipient = args[2]
- elif sender in num2name:
- recipient = num2name[sender]
- else:
- return None, "recipient unknown"
-
- # find car
- car = None
- if len(args) > 2 and args[2] in available_cars:
- car = args[2]
- elif len(args) > 3 and args[3] in available_cars:
- car = args[3]
-
- service_charge = 0
- if car:
- service_charge = available_cars[car]
-
- parts, err = tanken.tanken(msg[1:], amount, service_charge)
-
- if err != None:
- return None, err
-
- output = ""
- change = [args]
- for pname, values in parts.items():
- output += pname + ": {}km = fuel: {}, service charge: {}\n".format(values["distance"], to_euro(values["cost"]), to_euro(values["service_charge"]))
- # record service charges
- if pname not in name2num:
- output += pname + " not known."
- if car:
- person_to_charge = pname
- if pname not in name2num:
- person_to_charge = recipient
- output += " {} held accountable for service charge.".format(recipient)
- record(car, person_to_charge, values["service_charge"])
- change.append([car, person_to_charge, values["service_charge"]])
+ def _tanken(self, sender, args, msg):
+ if len(args) < 2:
+ raise InvalidArgumentError(f'"{" ".join(args)}" not in form "{args[0]} amount [person] [car] [descripton]"')
- # recipient paid the fuel -> don't charge them
- if pname == recipient:
- continue
+ amount = to_cent(args[1])
- if pname in name2num:
- record(recipient, pname, values["cost"])
- change.append([recipient, pname, values["cost"]])
+ # find recipient
+ if len(args) > 2 and args[2] in self.name2num:
+ recipient = args[2]
+ elif sender in self.num2name:
+ recipient = self.num2name[sender]
else:
- output += " Please collect fuel cost manually\n"
-
- if record_changes and not dry_run:
- changes[num2name[sender]].append(change)
-
- output += "New Balance:\n"
- output += create_summary(recipient)
- if car:
- output += "\nCar "
- output += create_summary(car)
- return output, None
-
-cmds["tanken"] = _tanken
-
-def fuck(sender, args, msg):
- if not sender in num2name:
- return None, "you must register first"
- else:
- name = num2name[sender]
-
- if len(changes[name]) == 0:
- return "Nothing to rewind", None
-
- # 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:
- if not change[0] in cmds:
- output += "{} {} {} {}\n".format(change[0],
- ("->" if change[2] < 0 else "<-"),
- to_euro(abs(change[2])),
- change[1])
- record(change[1], change[0], change[2])
-
- for change in last_changes:
- if change[0] in cmds:
- ret, err = cmds[change[0]](sender, change, "")
-
- if err:
- output += "ERROR: " + err
- else:
- output += ret
+ raise InvalidArgumentError('unable to determine receipient from {sender} or {args[2]}')
- return output, None
+ # find car
+ car = None
+ if len(args) > 2 and args[2] in self.cars:
+ car = args[2]
+ elif len(args) > 3 and args[3] in self.cars:
+ car = args[3]
-cmds["fuck"] = fuck
-cmds["rewind"] = fuck
-cmds["undo"] = fuck
+ service_charge = 0
+ if car:
+ service_charge = self.cars[car]
-def schedule(sender, args, msg):
- if not sender in num2name:
- return None, "you must register first"
+ parts, err = tanken.tanken(msg.splitlines()[1:], amount, service_charge)
- sender_name = num2name[sender]
+ if err != None:
+ raise Exception(err)
- if len(args) < 3:
- return None, 'not in form "{} name cmd"'.format(args[0])
+ output = ""
+ change = [args]
+ for pname, values in parts.items():
+ output += f"{pname}: {values['distance']}km = fuel: {to_euro(values['cost'])},"
+ output += f" service charge: {to_euro(values['service_charge'])}\n"
+ # record service charges
+ if pname not in self.name2num:
+ output += pname + " not known."
+ if car:
+ person_to_charge = pname
+ if pname not in self.name2num:
+ person_to_charge = recipient
+ output += " {} held accountable for service charge.".format(recipient)
+
+ self.record(car, person_to_charge, values["service_charge"])
+ change.append([car, person_to_charge, values["service_charge"]])
+
+ # recipient paid the fuel -> don't charge them
+ if pname == recipient:
+ continue
- name = args[1]
- cmd = args[2:]
+ if pname in self.name2num:
+ self.record(recipient, pname, values["cost"])
+ change.append([recipient, pname, values["cost"]])
+ else:
+ output += " Please collect fuel cost manually\n"
- if name in scheduled_cmds:
- return None, 'there is already a scheduled command named "{}"'.format(name)
+ self.record_change(self.num2name[sender], change)
- # Test the command
- global dry_run
- old_dry_run, dry_run = dry_run, True
+ output += "New Balance:\n"
+ output += self.create_summary(recipient)
+ if car:
+ output += "\nCar "
+ output += self.create_summary(car)
+ return output
- ret, err = cmds[cmd[0]](sender, cmd, "")
- dry_run = old_dry_run
+ def fuck(self, sender, args, msg):
+ if sender not in self.num2name:
+ raise NotRegisteredError('please register first using !register')
+ else:
+ name = self.num2name[sender]
+
+ if len(self.changes[name]) == 0:
+ return "Nothing to rewind"
+
+ # pop last item
+ last_changes = self.changes[name].pop()
+ args, last_changes = last_changes[0], last_changes[1:]
+
+ output = f"{name}: sorry I fucked up!\nRewinding:\n"
+ output += ' '.join(args) + "\n"
+ for change in last_changes:
+ if not change[0] in self.cmds:
+ output += "{} {} {} {}\n".format(change[0],
+ ("->" if change[2] < 0 else "<-"),
+ to_euro(abs(change[2])),
+ change[1])
+ self.record(change[1], change[0], change[2])
+
+ for change in last_changes:
+ if change[0] in self.cmds:
+ try:
+ ret = self.cmds[change[0]](sender, change, "")
+ except:
+ raise
- if err:
- return None, 'the command "{}" failed and will not be recorded'
+ output += ret
- scheduled_cmd = {"schedule": args[0][1:],
- "last_time": None,
- "sender": sender,
- "cmd": cmd}
+ return output
- scheduled_cmds[name] = scheduled_cmd
- output = 'Recorded the {} command "{}" as "{}"\n'.format(args[0][1:], ' '.join(cmd), name)
- output += "Running {} command {} for {} initially\n".format(scheduled_cmd["schedule"],
- name, sender_name)
+ def schedule(self, sender, args, msg):
+ """Add a scheduled command
- ret, err = cmds[cmd[0]](sender, cmd, "")
- if err:
- output += "ERROR: " + err
- else:
- output += ret
+ Possible schedules are: {'weekly', 'monthly', 'yearly'}.
+ """
+ if sender not in self.num2name:
+ raise NotRegisteredError('please register first using !register')
- changes[sender_name][0].append(["cancel", name])
+ sender_name = self.num2name[sender]
- now = datetime.now().date()
- scheduled_cmd["last_time"] = now.isoformat()
+ if len(args) < 3:
+ raise InvalidArgumentError(f'"{" ".join(args)}" not in form "{args[0]} name cmd"')
- return output, None
+ interval = args[0][1:]
+ name = args[1]
+ cmd = args[2:]
-cmds["weekly"] = schedule
-cmds["monthly"] = schedule
-cmds["yearly"] = schedule
+ if name in self.scheduled_cmds:
+ raise Exception(f'there is already a scheduled command named "{name}"')
-def cancel(sender, args, msg):
- cmd_name = args[1]
- if not cmd_name in scheduled_cmds:
- return None, '"{}" is not a scheduled command'.format(cmd_name)
- cmd = scheduled_cmds[cmd_name]
+ # Test the command
+ old_dry_run, self.dry_run = self.dry_run, True
- if not cmd["sender"] == sender:
- return None, 'only the original creator can cancel this command'
+ try:
+ ret = self.cmds[cmd[0]](sender, cmd, "")
+ except Exception as err:
+ raise Exception(f'the command "{args}" failed and will not be recorded') from err
+ finally:
+ self.dry_run = old_dry_run
- del(scheduled_cmds[cmd_name])
- return 'Cancelled the {} cmd "{}"'.format(cmd["schedule"], cmd_name), None
+ scheduled_cmd = {"schedule": interval,
+ "last_time": None,
+ "sender": sender_name,
+ "cmd": cmd}
-cmds["cancel"] = cancel
+ self.scheduled_cmds[name] = scheduled_cmd
+ output = 'Recorded the {} command "{}" as "{}"\n'.format(interval, ' '.join(cmd), name)
-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!")
+ output += "Running {} command {} for {} initially\n".format(interval,
+ name,
+ sender_name)
- # Read cmds from stdin
- for l in sys.stdin.read().splitlines():
try:
- message = json.loads(l)["envelope"]
- except:
- print(datetime.now(), l, "not valid json")
- continue
-
- 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() for l in message["message"].lower().splitlines()]
-
- if len(body) == 0:
- continue
+ ret = self.cmds[cmd[0]](sender, cmd, "")
+ output += ret
+ except Exception as err:
+ output += f'ERROR: {err}'
- args = body[0].split(' ')
+ self.changes[sender_name][0].append(["cancel", name])
- if args[0].startswith("!"):
- cmd = args[0][1:]
- if cmd in cmds:
- ret, err = cmds[cmd](sender_number, args, body)
- if err:
- send("ERROR: " + err)
- else:
- send(ret)
- else:
- send('ERROR: unknown cmd. Enter !help for a list of commands.')
+ now = datetime.now().date()
+ scheduled_cmd["last_time"] = now.isoformat()
- # Handle scheduled commands
- global record_changes
- record_changes = False
+ return output
- now = datetime.now().date()
- week_delta = timedelta(days=7)
- year_delta = timedelta(days=365)
- for name, cmd in scheduled_cmds.items():
+ def cancel(self, sender, args, msg):
+ """Cancel a scheduled command"""
+ if not sender in self.num2name:
+ raise NotRegisteredError('please register first using !register')
+ sender = self.num2name[sender]
- last_time = cmd["last_time"]
- if hasattr(date, "fromisoformat"):
- last_time = date.fromisoformat(last_time)
- else:
- last_time = date(*map(int, last_time.split("-")))
-
- d = last_time
- while True:
- if cmd["schedule"] == "yearly":
- d = date(d.year+1, d.month, d.day)
- elif cmd["schedule"] == "monthly":
- if d.day > 28:
- d = date(d.year, d.month, 28)
- if d.month == 12:
- d = date(d.year+1, 1, d.day)
- else:
- d = date(d.year, d.month+1, d.day)
- else:
- d = d + timedelta(7)
+ cmd_name = args[1]
+ if not cmd_name in self.scheduled_cmds:
+ raise NotRegisteredError(f'"{cmd_name}" is not a scheduled command')
+ cmd = self.scheduled_cmds[cmd_name]
- if d <= now:
- send("Running {} command {} for {} triggered on {}\n".format(cmd["schedule"],
- name,
- num2name[cmd["sender"]],
- d.isoformat()))
+ if not cmd["sender"] == sender:
+ raise NotAllowedError(f'only {cmd["sender"]}, the original creator can cancel this command')
- ret, err = cmds[cmd["cmd"][0]](cmd["sender"], cmd["cmd"], "")
+ del self.scheduled_cmds[cmd_name]
+ return f'Cancelled the {cmd["schedule"]} command "{cmd_name}"'
- if err:
- send("ERROR: " + err)
- else:
- send(ret)
- cmd["last_time"] = d.isoformat()
- else:
- break
+@dispatcher.add_method
+def receive(messageId, sender, groupId=None, message=None, attachments=None, timestamp=None):
+ """Entry point for jsonrpc"""
+ # we only handle text messages
+ if not message or not message.startswith('!'):
+ return []
+ state = Geldschieberbot()
- with open(state_file, "w") as f:
- json.dump(state, f)
+ args = message.splitlines()[0].split()
+ cmd = args[0][1:]
+ if cmd not in state.cmds:
+ return [{"receipients": groupId, "message": "ERROR: unknown cmd. Enter !help for a list of commands."}]
-if __name__ == "__main__":
- main()
+ func = state.cmds[cmd]
+ try:
+ msgs = func(sender, args, message)
+ except Exception as err:
+ msgs = f'{err.__class__.__name__}: {err}'
+ state.save()
+
+ if not isinstance(msgs, list):
+ msgs = [msgs]
+
+ return [{'recipients': groupId, 'message': msg} for msg in msgs]
+
+if __name__ == '__main__':
+
+ while(True):
+ request = json.dumps(json.loads(sys.stdin.readline()))
+ response = JSONRPCResponseManager.handle(request, dispatcher)
+ print(response.json, flush=True)
+
+ # Handle scheduled commands
+ # global RECORD_CHANGES
+ # RECORD_CHANGES = False
+
+ # now = datetime.now().date()
+ # week_delta = timedelta(days=7)
+ # year_delta = timedelta(days=365)
+
+ # for name, cmd in scheduled_cmds.items():
+ # last_time = cmd["last_time"]
+ # if hasattr(date, "fromisoformat"):
+ # last_time = date.fromisoformat(last_time)
+ # else:
+ # last_time = date(*map(int, last_time.split("-")))
+
+ # d = last_time
+ # while True:
+ # if cmd["schedule"] == "yearly":
+ # d = date(d.year+1, d.month, d.day)
+ # elif cmd["schedule"] == "monthly":
+ # if d.day > 28:
+ # d = date(d.year, d.month, 28)
+ # if d.month == 12:
+ # d = date(d.year+1, 1, d.day)
+ # else:
+ # d = date(d.year, d.month+1, d.day)
+ # else:
+ # d = d + timedelta(7)
+
+ # if d <= now:
+ # msgs.append((f'Running {cmd["schedule"]} command {name} for '
+ # f'{num2name[cmd["sender"]]} triggert on {d.isoformat()}\n'))
+
+ # ret, err = cmds[cmd["cmd"][0]](cmd["sender"], cmd["cmd"], "")
+
+ # if err:
+ # msgs.append("ERROR: " + err)
+ # else:
+ # msgs.append(ret)
+
+ # cmd["last_time"] = d.isoformat()
+ # else:
+ # break