diff options
Diffstat (limited to 'geldschieberbot.py')
| -rwxr-xr-x[-rw-r--r--] | geldschieberbot.py | 1298 |
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 |
