From d5119809549c3a8255c1677a98451e88115f674e Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Thu, 18 Nov 2021 11:25:50 +0100 Subject: geldschieberbot: add export-state cmd Add new command sending the state as an attachment. To allow this we refactor the return type of the command functions intead of returning a tuple(msg, err) they now return a dictionary. This is more flexible the members are position independent and it allows us to add arbitraty new return values like 'attachment'. If 'attachment' is present in the result of a command it will be attached to the message send. --- geldschieberbot.py | 305 +++++++++++++++++++++++++++++------------------------ 1 file changed, 167 insertions(+), 138 deletions(-) diff --git a/geldschieberbot.py b/geldschieberbot.py index fb168d4..68e6e1d 100644 --- a/geldschieberbot.py +++ b/geldschieberbot.py @@ -105,13 +105,15 @@ def to_euro(cents): return f"{cents/100:.2f}" -def send(msg): +def send(msg, attachment=None, cmd=send_cmd): if not quiet: - subprocess.run(send_cmd.split(' '), input=msg.encode(), check=False) + if attachment: + cmd += f'-a {attachment}' + subprocess.run(cmd.split(' '), input=msg.encode(), check=False) def create_summary(user): - summary = "" + msg = '' cars_summary = "" total = 0 cars_total = 0 @@ -125,14 +127,14 @@ def create_summary(user): 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' + msg += f'\t{"<-" if amount < 0 else "->"} {person} {to_euro(abs(amount))}\n' - if not summary: - summary = "\tAll fine :)" + if not msg: + msg = '\tAll fine :)' else: - summary += f"\tBalance: {to_euro(total)}" + msg += f'\tBalance: {to_euro(total)}' - ret_summary = f'{user}:\n{summary}' + ret_summary = f'{user}:\n{msg}' if cars_summary: cars_summary += f'\tLiability: {to_euro(cars_total)}' @@ -141,23 +143,23 @@ def create_summary(user): return ret_summary -def create_total_summary(): - summary = "Summary:" +def create_total_summary() -> str: + msg = 'Summary:' - cars_summary = "" + 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}' + msg += f'\n{p_summary}' if cars_summary: - summary += f'\nCars:{cars_summary}' - return summary + msg += f'\nCars:{cars_summary}' + return msg -def create_members(): +def create_members() -> str: r = "" for m in name2num: r += m + ": " + name2num[m] + "\n" @@ -210,6 +212,8 @@ tanken amount [person] [car] [info] - calculate fuel costs, service charge and a fuck [change] - rewind last or specific change list-changes [n] [first] - list the last n changes starting at first +export-state - send the state file + weekly name cmd - repeat cmd each week monthly name cmd - repeat cmd each month yearly name cmd - repeat cmd each year @@ -222,22 +226,22 @@ Happy Geldschieben! cmds = {} -def register(sender, args, msg): +def register(sender, args, msg) -> dict[str, str]: # pylint: disable=unused-argument if len(args) != 2: - return None, f'not in form "{args[0]} name"' + return {'err': f'not in form "{args[0]} name"'} name = args[1] try: to_cent(name) - return None, "pure numerical names are not allowed" + return {'err': 'pure numerical names are not allowed'} except (ValueError, TypeError): pass if name in name2num: - return None, f"{name} already registered" + return {'err': f'{name} already registered'} if sender in num2name: - return None, "you are already registered" + return {'err': 'you are already registered'} num2name[sender] = name name2num[name] = sender @@ -246,67 +250,66 @@ def register(sender, args, msg): # add changes list changes[name] = [] - return f"Happy geldschiebing {name}!", None + return {'msg': f'Happy geldschiebing {name}!'} cmds["reg"] = register cmds["register"] = register -def summary(sender, args, msg): # pylint: disable=unused-argument +def summary(sender, args, msg) -> dict[str, str]: # pylint: disable=unused-argument if len(args) == 1: if not sender in num2name: - return None, "You must register first to print your summary" + return {'err': 'You must register first to print your summary'} name = num2name[sender] - return f"Summary:\n{create_summary(name)}", None + return {'msg': f'Summary:\n{create_summary(name)}'} - err = None msg = "Summary:\n" for name in args[1:]: if name in name2num or name in available_cars: msg += create_summary(name) + "\n" else: - err = f'name "{name}" not registered' - return msg, err + return {'err': f'name "{name}" not registered'} + return {'msg': msg} cmds["sum"] = summary cmds["summary"] = summary -def full_summary(sender, args, msg): # pylint: disable=unused-argument +def full_summary(sender, args, msg) -> dict[str, str]: # pylint: disable=unused-argument if len(args) == 1: - return create_total_summary(), None + return {'msg': create_total_summary()} else: - return None, f"{args[0][1:]} takes no arguments" + return {'err': f'{args[0][1:]} takes no arguments'} cmds["full-sum"] = full_summary cmds["full-summary"] = full_summary -def list_users(sender, args, msg): # pylint: disable=unused-argument - return create_members(), None +def list_users(sender, args, msg) -> dict[str, str]: # pylint: disable=unused-argument + return {'msg': create_members()} cmds["ls"] = list_users cmds["list"] = list_users -def usage(sender, args, msg): # pylint: disable=unused-argument - return create_help(), None +def usage(sender, args, msg) -> dict[str, str]: # pylint: disable=unused-argument + return {'msg': create_help()} cmds["help"] = usage cmds["usage"] = usage -def split(sender, args, msg): +def split(sender, args, msg) -> dict[str, str]: # pylint: disable=unused-argument if not sender in num2name: - return None, 'you must register first' + return {'err': 'you must register first'} if len(args) < 3: - return None, f'not in form "{args[0]} amount [name]+"' + return {'err': f'not in form "{args[0]} amount [name]+"'} try: amount = to_cent(args[1]) @@ -318,9 +321,9 @@ def split(sender, args, msg): amount = to_cent(args[2]) persons = [args[1]] except (ValueError, TypeError): - return None, "amount must be a positive number" + return {'err': 'amount must be a positive number'} else: - return None, "amount must be a positive number" + return {'err': 'amount must be a positive number'} # persons + sender npersons = len(persons) + 1 @@ -336,7 +339,8 @@ def split(sender, args, msg): for p in persons: if p in name2num: if p == recipient: - output += f"{p}, you will be charged multiple times. This may not be what you want\n" + output += (f'{p}, you will be charged multiple times. ' + 'This may not be what you want\n') else: record(recipient, p, amount_per_person) change.append([recipient, p, amount_per_person]) @@ -348,20 +352,20 @@ def split(sender, args, msg): output += "New Balance:\n" output += create_summary(recipient) - return output, None + return {'msg': output} cmds["split"] = split cmds["teil"] = split -def transaction(sender, args, msg): +def transaction(sender, args, msg) -> dict[str, str]: # pylint: disable=unused-argument if len(args) != 3: - return None, f'not in form "{args[0]} amount recipient"' + return {'err': f'not in form "{args[0]} amount recipient"'} if not sender in balance: if sender not in num2name: - return None, 'you must register first' + return {'err': 'you must register first'} sender = num2name[sender] if args[1] in balance: @@ -369,15 +373,15 @@ def transaction(sender, args, msg): elif args[2] in balance: amount, recipient = args[1:3] else: - return None, 'recipient not known' + return {'err': 'recipient not known'} if sender == recipient: - return None, 'you can not transfer money to or from yourself' + return {'err': 'you can not transfer money to or from yourself'} try: amount = to_cent(amount) except (ValueError, TypeError): - return None, "amount must be a positive number" + return {'err': 'amount must be a positive number'} if args[0] in ["!zieh", "!nimm"]: amount *= -1 @@ -392,7 +396,7 @@ def transaction(sender, args, msg): output = ("New Balance: {} {} {} {}\n".format( sender, ("->" if p_balance > 0 else "<-"), to_euro(abs(p_balance)), recipient)) - return output, None + return {'msg': output} cmds["schieb"] = transaction @@ -401,12 +405,12 @@ cmds["zieh"] = transaction cmds["nimm"] = transaction -def transfer(sender, args, msg): +def transfer(sender, args, msg) -> dict[str, str]: # pylint: disable=unused-argument if len(args) < 4: - return None, f'not in form "{args[0]} amount source destination"' + return {'err': f'not in form "{args[0]} amount source destination"'} if not sender in num2name: - return None, 'you must register first' + return {'err': 'you must register first'} sender = num2name[sender] @@ -414,14 +418,14 @@ def transfer(sender, args, msg): amount_raw = args[1] amount_cent = to_cent(amount_raw) except (ValueError, TypeError): - return None, "amount must be a positive number" + return {'err': 'amount must be a positive number'} source, destination = args[2:4] if source not in balance: - return None, f'source "{source}" not known' + return {'err': f'source "{source}" not known'} if destination not in balance: - return None, f'destination "{destination}" not known' + return {'err': f'destination "{destination}" not known'} output = "" global record_changes @@ -429,30 +433,32 @@ def transfer(sender, args, msg): record_changes = False change = [args] - ret, err = transaction(sender, ["!zieh", source, amount_raw], "") - if err: + ret = transaction(sender, ["!zieh", source, amount_raw], "") + if 'err' in ret: # No changes yet we can fail - return None, err + return {'err': ret['err']} - output += ret + output += ret['msg'] # Sender <- X Source change.append((sender, source, -amount_cent)) - ret, err = transaction(sender, ["!schieb", destination, amount_raw], "") + ret = transaction(sender, ["!schieb", destination, amount_raw], "") + err = ret.get('err', None) if err: output += err + "\nThe balance may be in a inconsistent state please take care manually" - return output, None + return {'msg': output} - output += ret + output += ret['msg'] # Sender -> X Destination change.append((sender, destination, amount_cent)) - ret, err = transaction(source, ["!zieh", destination, amount_raw], "") + ret = transaction(source, ["!zieh", destination, amount_raw], "") + err = ret.get('err', None) if err: output += err + "\nThe balance may be in a inconsistent state please take care manually" - return output, None + return {'msg': output} - output += ret + output += ret['msg'] # Destination -> X Source change.append((destination, source, amount_cent)) @@ -460,17 +466,17 @@ def transfer(sender, args, msg): if err is None and record_changes and not dry_run: changes[sender].append(change) - return output, None + return {'msg': output} cmds["transfer"] = transfer -def cars(sender, args, msg): +def cars(sender, args, msg) -> dict[str, str]: # pylint: disable=unused-argument # list cars if len(args) < 2 or args[1] in ["ls", "list"]: if len(available_cars) == 0: - return "No cars registered yet.", None + return {'msg': 'No cars registered yet.'} ret_msg = "" @@ -484,63 +490,71 @@ def cars(sender, args, msg): 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 {'err': f'"{car}" is no available car\n'} - return ret_msg[:-1], None + return {'msg': ret_msg[:-1]} # add car if args[1] in ["add", "new"]: if len(args) < 4: - return None, f'not in form "{args[0]} {args[1]} car-name service-charge"' + return { + 'err': + f'not in form "{args[0]} {args[1]} car-name service-charge"' + } car = args[2] if car in available_cars: - return None, f'"{car}" already registered' + return {'err': f'"{car}" already registered'} if car in balance: - return None, f'A user named "{car}" already exists. Please use a different name for this car' + return { + 'err': + f'A user named "{car}" already exists. Please use a different name for this car' + } try: service_charge = to_cent(args[3]) except (ValueError, TypeError): - return None, "service-charge must be a positive number" + return {'err': '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 + return {'msg': f'added "{car}" as an available car'} # remove car if args[1] in ["rm", "remove"]: if len(args) < 3: - return None, f'not in form "{args[0]} {args[1]} car-name"' + return {'err': f'not in form "{args[0]} {args[1]} car-name"'} car = args[2] if car not in available_cars: - return None, f'A car with the name "{car}" does not exists' + return {'err': f'A car with the name "{car}" does not exists'} del available_cars[car] remove_from_balance(car) - return f'removed "{car}" from the available cars', None + return {'msg': f'removed "{car}" from the available cars'} # pay bill if args[1] in ["pay"]: if len(args) < 4: - return None, f'not in form "{args[0]} {args[1]} car-name amount"' + return { + 'err': f'not in form "{args[0]} {args[1]} car-name amount"' + } if not sender in num2name: - return None, "you must register first" + return {'err': 'you must register first'} sender_name = num2name[sender] car = args[2] if car not in available_cars: - return None, f'car "{car}" not known' + return {'err': f'car "{car}" not known'} try: amount = to_cent(args[3]) amount_euro = to_euro(amount) except (ValueError, TypeError): - return None, "amount must be a positive number" + return {'err': 'amount must be a positive number'} output = "" @@ -561,8 +575,8 @@ def cars(sender, args, msg): if amount < total_available_charge: proportion = -1 * (amount / total_available_charge) - _, err = transaction(sender, f"!gib {car} {amount_euro}".split(), "") - assert err is None + ret = transaction(sender, f'!gib {car} {amount_euro}'.split(), '') + assert 'err' not in ret output += f"{sender_name} payed {amount_euro}\n" # transfer money @@ -573,9 +587,8 @@ def cars(sender, args, msg): 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 = transfer(sender, ['transfer', to_move_euro, car, person], '') + assert 'err' not in ret output += "Transfer {} from {} to {}\n".format( to_move_euro, person, sender_name) @@ -588,21 +601,21 @@ def cars(sender, args, msg): if record_changes and not dry_run: changes[sender_name].append(change) - return output, None + return {'msg': output} - return None, f'unknown car subcommand "{args[1]}".' + return {'err': f'unknown car subcommand "{args[1]}".'} cmds["cars"] = cars -def _tanken(sender, args, msg): +def _tanken(sender, args, msg) -> dict[str, str]: if len(args) < 2: - return None, f'not in form "{args[0]} amount [person] [car] [info]"' + return {'err': f'not in form "{args[0]} amount [person] [car] [info]"'} try: amount = to_cent(args[1]) except (ValueError, TypeError): - return None, "amount must be a number" + return {'err': 'amount must be a number'} # find recipient if len(args) > 2 and args[2] in name2num: @@ -610,7 +623,7 @@ def _tanken(sender, args, msg): elif sender in num2name: recipient = num2name[sender] else: - return None, "recipient unknown" + return {'err': 'recipient unknown'} # find car car = None @@ -626,7 +639,7 @@ def _tanken(sender, args, msg): parts, err = tanken.tanken(msg[1:], amount, service_charge) if err: - return None, err + return {'err': err} output = "" change = [args] @@ -664,31 +677,31 @@ def _tanken(sender, args, msg): if car: output += "\nCar " output += create_summary(car) - return output, None + return {'msg': output} cmds["tanken"] = _tanken -def fuck(sender, args, msg): +def fuck(sender, args, msg) -> dict[str, str]: # pylint: disable=unused-argument if not sender in num2name: - return None, "you must register first" + return {'err': 'you must register first'} name = num2name[sender] nchanges = len(changes[name]) if nchanges == 0: - return "Nothing to rewind", None + return {'msg': 'Nothing to rewind'} change_to_rewind = -1 if len(args) >= 2: try: change_to_rewind = int(args[1]) - 1 except ValueError: - return None, "change to rewind must be a number" + return {'err': 'change to rewind must be a number'} if change_to_rewind > nchanges: - return None, "change to rewind is bigger than there are changes" + return {'err': 'change to rewind is bigger than there are changes'} # pop last item last_changes = changes[name].pop(change_to_rewind) @@ -706,14 +719,14 @@ def fuck(sender, args, msg): for change in last_changes: if change[0] in cmds: - ret, err = cmds[change[0]](sender, change, "") + ret = cmds[change[0]](sender, change, "") - if err: - output += "ERROR: " + err + if 'err' in ret: + output += "ERROR: " + ret['err'] else: - output += ret + output += ret['msg'] - return output, None + return {'msg': output} cmds["fuck"] = fuck @@ -721,9 +734,9 @@ cmds["rewind"] = fuck cmds["undo"] = fuck -def list_changes(sender, args, msg): +def list_changes(sender, args, msg) -> dict[str, str]: if not sender in num2name: - return None, "you must register first" + return {'err': 'you must register first'} sender_name = num2name[sender] @@ -732,11 +745,11 @@ def list_changes(sender, args, msg): try: changes_to_list = int(args[1]) except ValueError: - return None, 'the amount of changes to list must be a number' + return {'err': 'the amount of changes to list must be a number'} nchanges = len(changes[sender_name]) if nchanges == 0: - return "Nothing to list", None + return {'msg': 'Nothing to list'} first_to_list = max(nchanges - changes_to_list, 0) @@ -744,12 +757,16 @@ def list_changes(sender, args, msg): try: first_to_list = int(args[2]) - 1 except ValueError: - return None, 'the first change to list must be a number' + return {'err': 'the first change to list must be a number'} if first_to_list > nchanges: - return None, 'the first change to list is bigger than there are changes' + return { + 'err': + 'the first change to list is bigger than there are changes' + } msg = "" + i = 0 for i, change in enumerate(changes[sender_name]): if i < first_to_list: continue @@ -770,37 +787,48 @@ def list_changes(sender, args, msg): # prepend message header because we want to know how much changes we actually listed msg = f'Changes from {sender_name} {first_to_list + 1}-{i + 1}\n' + msg - return msg, None + return {'msg': msg} cmds["list-changes"] = list_changes -def schedule(sender, args, msg): +def export_state(sender, args, msg) -> dict[str, str]: # pylint: disable=unused-argument if not sender in num2name: - return None, "you must register first" + return {'err': 'you must register first'} + + msg = f'State from {datetime.now().date().isoformat()}' + return {'msg': msg, 'attachment': state_file} + + +cmds["export-state"] = export_state + + +def schedule(sender, args, msg) -> dict[str, str]: # pylint: disable=unused-argument + if not sender in num2name: + return {'err': 'you must register first'} sender_name = num2name[sender] if len(args) < 3: - return None, f'not in form "{args[0]} name cmd"' + return {'err': f'not in form "{args[0]} name cmd"'} name = args[1] cmd = args[2:] if name in scheduled_cmds: - return None, f'there is already a scheduled command named "{name}"' + return {'err': f'there is already a scheduled command named "{name}"'} # Test the command global dry_run old_dry_run, dry_run = dry_run, True - ret, err = cmds[cmd[0]](sender, cmd, "") + ret = cmds[cmd[0]](sender, cmd, '') dry_run = old_dry_run - if err: - return None, 'the command "{}" failed and will not be recorded' + if 'err' in ret: + return {'err': 'the command "{}" failed and will not be recorded'} scheduled_cmd = { "schedule": args[0][1:], @@ -816,18 +844,18 @@ def schedule(sender, args, msg): output += "Running {} command {} for {} initially\n".format( scheduled_cmd["schedule"], name, sender_name) - ret, err = cmds[cmd[0]](sender, cmd, "") - if err: - output += "ERROR: " + err + ret = cmds[cmd[0]](sender, cmd, "") + if 'err' in ret: + output += 'ERROR: ' + ret['err'] else: - output += ret + output += ret['msg'] changes[sender_name][0].append(["cancel", name]) now = datetime.now().date() scheduled_cmd["last_time"] = now.isoformat() - return output, None + return {'msg': output} cmds["weekly"] = schedule @@ -835,30 +863,30 @@ cmds["monthly"] = schedule cmds["yearly"] = schedule -def cancel(sender, args, msg): +def cancel(sender, args, msg) -> dict[str, str]: # pylint: disable=unused-argument cmd_name = args[1] if not cmd_name in scheduled_cmds: - return None, f'"{cmd_name}" is not a scheduled command' + return {'err': f'"{cmd_name}" is not a scheduled command'} cmd = scheduled_cmds[cmd_name] if not cmd["sender"] == sender: - return None, 'only the original creator can cancel this command' + return {'err': 'only the original creator can cancel this command'} del scheduled_cmds[cmd_name] - return f'Cancelled the {cmd["schedule"]} cmd "{cmd_name}"', None + return {'msg': f'Cancelled the {cmd["schedule"]} cmd "{cmd_name}"'} cmds["cancel"] = cancel -def thanks(sender, args, msg): +def thanks(sender, args, msg) -> dict[str, str]: # pylint: disable=unused-argument sender_name = num2name.get(sender, sender) msg = f'You are welcome. It is a pleasure to work with you, {sender_name}.' nick = None if len(args) == 1 else args[1] if nick: msg = f"{msg}\nBut don't call me {nick}." - return msg, None + return {'msg': msg} cmds["thanks"] = thanks @@ -897,11 +925,13 @@ def main(): 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) + ret = cmds[cmd](sender_number, args, body) + if 'err' in ret: + send(f'ERROR: {ret["err"]}') else: - send(ret) + if not 'msg' in ret: + print(ret) + send(ret['msg'], attachment=ret.get('attachment', None)) else: send('ERROR: unknown cmd. Enter !help for a list of commands.') @@ -938,13 +968,12 @@ def main(): format(cmd["schedule"], name, num2name[cmd["sender"]], d.isoformat())) - ret, err = cmds[cmd["cmd"][0]](cmd["sender"], cmd["cmd"], - "") + ret = cmds[cmd["cmd"][0]](cmd["sender"], cmd["cmd"], "") - if err: - send("ERROR: " + err) + if 'err' in ret: + send("ERROR: " + ret['err']) else: - send(ret) + send(ret['msg']) cmd["last_time"] = d.isoformat() else: -- cgit v1.2.3