aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--exceptions.py14
-rwxr-xr-x[-rw-r--r--]geldschieberbot.py1298
-rwxr-xr-xtest.py288
3 files changed, 831 insertions, 769 deletions
diff --git a/exceptions.py b/exceptions.py
new file mode 100644
index 0000000..ca68547
--- /dev/null
+++ b/exceptions.py
@@ -0,0 +1,14 @@
+class InvalidAmountError(ValueError):
+ '''Raise when some member is already registered'''
+
+class AlreadyRegisteredError(Exception):
+ '''Raise when some member is already registered'''
+
+class NotRegisteredError(Exception):
+ '''Raise when the sender is not registered'''
+
+class InvalidArgumentError(Exception):
+ '''Raise when some cmd is called with wrong arguments'''
+
+class NotAllowedError(Exception):
+ '''Raise when some member is already registered'''
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
diff --git a/test.py b/test.py
index 94235e1..4ffea93 100755
--- a/test.py
+++ b/test.py
@@ -8,17 +8,15 @@ from string import Template
import subprocess
import unittest
+import geldschieberbot
+
alice, bob, charlie = "alice", "bob", "charlie"
num = {alice: "+49123456", bob: "+49654321", charlie: "+49615243"}
-os.environ["GSB_GROUP_ID"] = "test"
-os.environ["GSB_STATE_FILE"] = "test/state.json"
-os.environ["GSB_SEND_CMD"] = "cat"
-os.environ["GSB_SEND_GROUP"] = "cat"
-os.environ["GSB_SEND_USER"] = "cat"
-os.environ["GSB_MODULES"] = "geldschiebing.py"
now = datetime.now().date()
+STATE_PATH = "test/state.json"
+
msg_template = Template("""
{"envelope":
{"source":"$sender",
@@ -52,37 +50,72 @@ scheduled_state_template = Template("""
"sender": "+49123456", "cmd": ["split", "3", "bob", "charlie"]}},
"changes": {"alice": [], "bob": [], "charlie": []}}""")
+METHOD_COUNTER = 0
+BOT = None
+
def run_bot(test, sender, cmd):
- msg = msg_template.substitute(sender=sender, msg=cmd).replace("\n", "\\n") + "\n"
- res = subprocess.run(["python3", "./geldschieberbot.py"], text=True, capture_output=True,
- # res = subprocess.run(["python3", "./bot.py"], text=True, capture_output=True,
- input=msg)
-
- if res.returncode != 0:
- print(res.stdout)
- print(res.stderr)
- test.assertEqual(res.returncode, 0)
- test.assertEqual(res.stderr, "")
+ global BOT
+ if not BOT:
+ env = {"GSB_STATE_FILE": STATE_PATH}
+ BOT = subprocess.Popen("./geldschieberbot.py",
+ stdout=subprocess.PIPE,
+ stdin=subprocess.PIPE,
+ text=True,
+ env=env)
+ global METHOD_COUNTER
+ payload = {
+ "method": "receive",
+ "params": {
+ "messageId": 0,
+ "sender": sender,
+ "message": cmd,
+ "groupId": "test",
+ },
+ "jsonrpc": "2.0",
+ "id": METHOD_COUNTER,
+ }
+
+ METHOD_COUNTER += 1
+
+ if BOT.poll():
+ print("BOT died!")
+ BOT = None
+ print(json.dumps(payload), file=BOT.stdin, flush=True)
+ response = json.loads(BOT.stdout.readline())
+
+ return "/n".join([result['message'] for result in response["result"]])
+
+ # res = geldschieberbot.receive(0, sender, message=cmd, timestamp=1544101248419, groupId='test')
+ # msg = msg_template.substitute(sender=sender, msg=cmd).replace("\n", "\\n") + "\n"
+ # res = subprocess.run(["python3", "./geldschieberbot.py"], text=True, capture_output=True,
+ # # res = subprocess.run(["python3", "./bot.py"], text=True, capture_output=True,
+ # input=msg)
+
+ # if res.returncode != 0:
+ # print(res)
+ # print(res.stderr)
+ # test.assertEqual(res.returncode, 0)
+ # test.assertEqual(res.stderr, "")
return res
def save_state(dest):
- copyfile(os.environ["GSB_STATE_FILE"], dest)
+ copyfile(STATE_PATH, dest)
def reset_state(state=None):
if state:
- copyfile(state, os.environ["GSB_STATE_FILE"])
+ copyfile(state, STATE_PATH)
else:
- state = os.environ["GSB_STATE_FILE"]
+ state = STATE_PATH
if os.path.isfile(state):
os.remove(state)
def reset_state_string(string):
- with open(os.environ["GSB_STATE_FILE"], "w") as f:
+ with open(STATE_PATH, "w") as f:
json.dump(json.loads(string), f)
def compare_state(comp_state):
with open(comp_state, "r") as csf, \
- open(os.environ["GSB_STATE_FILE"], "r") as sf:
+ open(STATE_PATH, "r") as sf:
cs = csf.read()
s = sf.read()
return cs == s
@@ -94,25 +127,25 @@ class TestRegCmd(unittest.TestCase):
def test_correct_reg(self):
res = run_bot(self, num[alice], "!reg "+alice)
- self.assertEqual(res.stdout, 'Happy geldschiebing {}!'.format(alice))
+ self.assertEqual(res, 'Happy geldschiebing {}!'.format(alice))
def test_double_reg(self):
- res = run_bot(self, num[alice], "!reg "+alice)
- self.assertEqual(res.stdout, 'Happy geldschiebing {}!'.format(alice))
- res = run_bot(self, num[alice], "!reg "+alice)
- self.assertEqual(res.stdout, 'ERROR: '+alice+' already registered')
+ res = run_bot(self, num[alice], f"!reg {alice}")
+ self.assertEqual(res, f'Happy geldschiebing {alice}!')
+ res = run_bot(self, num[alice], f"!reg {alice}")
+ self.assertEqual(res, f'AlreadyRegisteredError: "{alice}" already registered')
def test_invalid_reg(self):
res = run_bot(self, num[alice], "!reg nase 03")
- self.assertEqual(res.stdout, 'ERROR: not in form "!reg name"')
+ self.assertEqual(res, 'InvalidArgumentError: "!reg nase 03" not in form "!reg name"')
def test_additional_reg(self):
res = run_bot(self, num[alice], "!reg "+alice)
- self.assertEqual(res.stdout, 'Happy geldschiebing {}!'.format(alice))
+ self.assertEqual(res, 'Happy geldschiebing {}!'.format(alice))
res = run_bot(self, num[bob], "!reg "+bob)
- self.assertEqual(res.stdout, 'Happy geldschiebing {}!'.format(bob))
+ self.assertEqual(res, 'Happy geldschiebing {}!'.format(bob))
res = run_bot(self, num[charlie], "!reg "+charlie)
- self.assertEqual(res.stdout, 'Happy geldschiebing {}!'.format(charlie))
+ self.assertEqual(res, 'Happy geldschiebing {}!'.format(charlie))
self.assertTrue(compare_state("test/state.json_3users"))
@@ -128,72 +161,72 @@ class TestTransactionCmd(unittest.TestCase):
def test_correct_schieb(self):
res = run_bot(self, num[alice], "!schieb 10 "+bob)
- self.assertEqual(res.stdout, 'New Balance: {} <- 10.00 {}\n'.format(alice, bob))
+ self.assertEqual(res, 'New Balance: {} <- 10.00 {}\n'.format(alice, bob))
res = run_bot(self, num[bob], "!schieb 10 "+alice)
- self.assertEqual(res.stdout, 'New Balance: {} <- 0.00 {}\n'.format(bob, alice))
+ self.assertEqual(res, 'New Balance: {} <- 0.00 {}\n'.format(bob, alice))
def test_correct_gib(self):
res = run_bot(self, num[alice], "!gib 10 "+bob)
- self.assertEqual(res.stdout, 'New Balance: {} <- 10.00 {}\n'.format(alice, bob))
+ self.assertEqual(res, 'New Balance: {} <- 10.00 {}\n'.format(alice, bob))
res = run_bot(self, num[bob], "!gib 10 "+alice)
- self.assertEqual(res.stdout, 'New Balance: {} <- 0.00 {}\n'.format(bob, alice))
+ self.assertEqual(res, 'New Balance: {} <- 0.00 {}\n'.format(bob, alice))
def test_correct_amount(self):
res = run_bot(self, num[bob], "!schieb 1.1 "+alice)
- self.assertEqual(res.stdout, 'New Balance: {} <- 1.10 {}\n'.format(bob, alice))
+ self.assertEqual(res, 'New Balance: {} <- 1.10 {}\n'.format(bob, alice))
res = run_bot(self, num[bob], "!gib 1,1 "+alice)
- self.assertEqual(res.stdout, 'New Balance: {} <- 2.20 {}\n'.format(bob, alice))
+ self.assertEqual(res, 'New Balance: {} <- 2.20 {}\n'.format(bob, alice))
def test_invalid_amount(self):
res = run_bot(self, num[bob], "!schieb 1b1 "+alice)
- self.assertEqual(res.stdout, 'ERROR: amount must be a positive number')
+ self.assertEqual(res, 'InvalidAmountError: "1b1" not in form <euro>.<cents>')
res = run_bot(self, num[bob], "!schieb ä€ "+alice)
- self.assertEqual(res.stdout, 'ERROR: amount must be a positive number')
+ self.assertEqual(res, 'InvalidAmountError: "ä€" not in form <euro>.<cents>')
def test_correct_schieb_name_before_amount(self):
res = run_bot(self, num[alice], "!schieb "+bob+ " 10")
- self.assertEqual(res.stdout, 'New Balance: {} <- 10.00 {}\n'.format(alice, bob))
+ self.assertEqual(res, 'New Balance: {} <- 10.00 {}\n'.format(alice, bob))
res = run_bot(self, num[bob], "!schieb "+alice+ " 10")
- self.assertEqual(res.stdout, 'New Balance: {} <- 0.00 {}\n'.format(bob, alice))
+ self.assertEqual(res, 'New Balance: {} <- 0.00 {}\n'.format(bob, alice))
def test_correct_gib_name_before_amount(self):
res = run_bot(self, num[alice], "!gib "+charlie+ " 10")
- self.assertEqual(res.stdout, 'New Balance: {} <- 10.00 {}\n'.format(alice, charlie))
+ self.assertEqual(res, 'New Balance: {} <- 10.00 {}\n'.format(alice, charlie))
res = run_bot(self, num[charlie], "!gib "+alice+ " 10")
- self.assertEqual(res.stdout, 'New Balance: {} <- 0.00 {}\n'.format(charlie, alice))
+ self.assertEqual(res, 'New Balance: {} <- 0.00 {}\n'.format(charlie, alice))
def test_correct_nimm(self):
res = run_bot(self, num[alice], "!nimm 10 "+bob)
- self.assertEqual(res.stdout, 'New Balance: {} -> 10.00 {}\n'.format(alice, bob))
+ self.assertEqual(res, 'New Balance: {} -> 10.00 {}\n'.format(alice, bob))
res = run_bot(self, num[bob], "!nimm 10 "+alice)
- self.assertEqual(res.stdout, 'New Balance: {} <- 0.00 {}\n'.format(bob, alice))
+ self.assertEqual(res, 'New Balance: {} <- 0.00 {}\n'.format(bob, alice))
def test_correct_zieh_name_before_amount(self):
res = run_bot(self, num[alice], "!zieh "+charlie+ " 10")
- self.assertEqual(res.stdout, 'New Balance: {} -> 10.00 {}\n'.format(alice, charlie))
+ self.assertEqual(res, 'New Balance: {} -> 10.00 {}\n'.format(alice, charlie))
res = run_bot(self, num[charlie], "!zieh "+alice+ " 10")
- self.assertEqual(res.stdout, 'New Balance: {} <- 0.00 {}\n'.format(charlie, alice))
+ self.assertEqual(res, 'New Balance: {} <- 0.00 {}\n'.format(charlie, alice))
def test_transactions_complex(self):
res = run_bot(self, num[alice], "!schieb "+charlie+ " 1,1")
- self.assertEqual(res.stdout, 'New Balance: {} <- 1.10 {}\n'.format(alice, charlie))
+ self.assertEqual(res, 'New Balance: {} <- 1.10 {}\n'.format(alice, charlie))
res = run_bot(self, num[alice], "!zieh "+charlie+ " 2.1")
- self.assertEqual(res.stdout, 'New Balance: {} -> 1.00 {}\n'.format(alice, charlie))
+ self.assertEqual(res, 'New Balance: {} -> 1.00 {}\n'.format(alice, charlie))
res = run_bot(self, num[charlie], "!schieb "+bob+ " 42")
- self.assertEqual(res.stdout, 'New Balance: {} <- 42.00 {}\n'.format(charlie, bob))
+ self.assertEqual(res, 'New Balance: {} <- 42.00 {}\n'.format(charlie, bob))
res = run_bot(self, num[alice], "!zieh "+bob+ " 0.01")
- self.assertEqual(res.stdout, 'New Balance: {} -> 0.01 {}\n'.format(alice, bob))
+ self.assertEqual(res, 'New Balance: {} -> 0.01 {}\n'.format(alice, bob))
compare_state("test/state.json_transactions1")
@@ -201,12 +234,12 @@ class TestSumCmd(unittest.TestCase):
def test_summary_single_user(self):
reset_state("test/state.json_transactions1")
res = run_bot(self, num[alice], "!sum "+alice)
- self.assertEqual(res.stdout, 'Summary:\nalice:\n\t-> bob 0.01\n\t-> charlie 1.00\n\tBalance: -1.01\n')
+ self.assertEqual(res, 'Summary:\nalice:\n\t-> bob 0.01\n\t-> charlie 1.00\n\tBalance: -1.01\n')
- def test_summary_invalide_single_user(self):
+ def test_summary_invalid_single_user(self):
reset_state()
res = run_bot(self, num[alice], "!sum "+alice)
- self.assertEqual(res.stdout, 'ERROR: name "alice" not registered')
+ self.assertEqual(res, 'NotRegisteredError: "alice" not registered')
def test_summary_double_user(self):
reset_state("test/state.json_transactions1")
@@ -222,7 +255,7 @@ bob:
\t-> charlie 42.00
\tBalance: -41.99
"""
- self.assertEqual(res.stdout, summary)
+ self.assertEqual(res, summary)
def test_summary(self):
reset_state("test/state.json_transactions1")
@@ -241,17 +274,17 @@ charlie:
\t<- alice 1.00
\t<- bob 42.00
\tBalance: 43.00"""
- self.assertEqual(res.stdout, summary)
+ self.assertEqual(res, summary)
class TestMisc(unittest.TestCase):
def test_unknown_command(self):
res = run_bot(self, num[alice], "!foo")
- self.assertEqual(res.stdout, "ERROR: unknown cmd. Enter !help for a list of commands.")
+ self.assertEqual(res, "ERROR: unknown cmd. Enter !help for a list of commands.")
def test_no_command(self):
res = run_bot(self, num[alice], "Hi, how are you?")
- self.assertEqual(res.stdout, "")
+ self.assertEqual(res, "")
class TestListCmd(unittest.TestCase):
@@ -262,12 +295,12 @@ class TestListCmd(unittest.TestCase):
def test_ls(self):
res = run_bot(self, num[alice], "!ls")
msg = "alice: {}\nbob: {}\ncharlie: {}\n".format(num[alice], num[bob], num[charlie])
- self.assertEqual(res.stdout, msg)
+ self.assertEqual(res, msg)
def test_list(self):
res = run_bot(self, num[bob], "!list")
msg = "alice: {}\nbob: {}\ncharlie: {}\n".format(num[alice], num[bob], num[charlie])
- self.assertEqual(res.stdout, msg)
+ self.assertEqual(res, msg)
class TestSplitCmd(unittest.TestCase):
@@ -276,17 +309,17 @@ class TestSplitCmd(unittest.TestCase):
def test_split_unregistered(self):
res = run_bot(self, "+4971576357", "!split")
- self.assertEqual(res.stdout, 'ERROR: you must register first')
+ self.assertEqual(res, 'NotRegisteredError: please register first using !register')
def test_split_invalid_args(self):
res = run_bot(self, num[alice], "!split")
- self.assertEqual(res.stdout, 'ERROR: not in form "!split amount [name]+"')
+ self.assertEqual(res, 'InvalidArgumentError: "!split" not in form "!split amount [name]+"')
res = run_bot(self, num[alice], "!split 10")
- self.assertEqual(res.stdout, 'ERROR: not in form "!split amount [name]+"')
+ self.assertEqual(res, 'InvalidArgumentError: "!split 10" not in form "!split amount [name]+"')
res = run_bot(self, num[alice], "!split foo 10")
- self.assertEqual(res.stdout, 'ERROR: amount must be a positive number')
+ self.assertEqual(res, 'InvalidAmountError: "foo" not in form <euro>.<cents>')
def test_split_one_unknown_user(self):
@@ -298,7 +331,7 @@ New Balance:
alice:
\t<- charlie 10.00
\tBalance: 10.00"""
- self.assertEqual(res.stdout, msg)
+ self.assertEqual(res, msg)
def test_split(self):
res = run_bot(self, num[alice], "!split 30 " + bob + " " + charlie)
@@ -309,7 +342,7 @@ alice:
\t<- bob 10.00
\t<- charlie 10.00
\tBalance: 20.00"""
- self.assertEqual(res.stdout, msg)
+ self.assertEqual(res, msg)
def test_split_whitespace(self):
res = run_bot(self, num[alice], "!split 30 " + bob + " " + charlie + " ")
@@ -320,7 +353,7 @@ alice:
\t<- bob 10.00
\t<- charlie 10.00
\tBalance: 20.00"""
- self.assertEqual(res.stdout, msg)
+ self.assertEqual(res, msg)
class TestCarsAddCmd(unittest.TestCase):
def setUp(self):
@@ -330,36 +363,36 @@ class TestCarsAddCmd(unittest.TestCase):
i = "!cars add foo 0.04"
res = run_bot(self, num[alice], i)
o = 'added "foo" as an available car'
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
i = "!cars new bar 0.02"
res = run_bot(self, num[alice], i)
o = 'added "bar" as an available car'
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
save_state("test/state.json_2cars")
def test_add_invalid_service_charge(self):
i = "!cars add foo 0.04hut"
res = run_bot(self, num[alice], i)
- o = "ERROR: service-charge must be a positive number"
- self.assertEqual(res.stdout, o)
+ o = "InvalidAmountError: service-charge (0.04hut) must be a positive number"
+ self.assertEqual(res, o)
i = "!cars new bar -5"
res = run_bot(self, num[alice], i)
- o = "ERROR: service-charge must be a positive number"
- self.assertEqual(res.stdout, o)
+ o = "InvalidAmountError: service-charge (-5) must be a positive number"
+ self.assertEqual(res, o)
def test_add_name_conflict(self):
i = "!cars add foo 0.04"
res = run_bot(self, num[alice], i)
o = 'added "foo" as an available car'
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
i = "!cars new alice 0.02"
res = run_bot(self, num[alice], i)
- o = 'ERROR: A user named "alice" already exists. Please use a different name for this car'
- self.assertEqual(res.stdout, o)
+ o = 'AlreadyRegisteredError: A user named "alice" already exists. Please use a different name for this car'
+ self.assertEqual(res, o)
class TestCarsTransactions(unittest.TestCase):
def setUp(self):
@@ -369,7 +402,7 @@ class TestCarsTransactions(unittest.TestCase):
i = "!schieb foo 20"
res = run_bot(self, num[alice], i)
o = "New Balance: alice <- 20.00 foo\n"
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
class TestCarPayCmd(unittest.TestCase):
def setUp(self):
@@ -393,7 +426,7 @@ alice:
\tBalance: 30.00
foo:
\tAll fine :)"""
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
def test_alice_pays_more(self):
run_bot(self, num[bob], "!zieh foo 20")
@@ -417,7 +450,7 @@ alice:
foo:
\t-> alice 10.00
\tBalance: -10.00"""
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
def test_alice_pays_half(self):
@@ -440,7 +473,7 @@ foo:
\t<- bob 10.00
\t<- charlie 5.00
\tBalance: 15.00"""
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
class TestCarsListCmd(unittest.TestCase):
def setUp(self):
@@ -451,7 +484,7 @@ class TestCarsListCmd(unittest.TestCase):
i = "!cars"
res = run_bot(self, num[alice], i)
o = "No cars registered yet."
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
def test_implicit_call(self):
i = "!cars"
@@ -463,7 +496,7 @@ foo:
bar - service charge 2ct/km
bar:
\tAll fine :)"""
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
def test_list_all(self):
i = "!cars ls"
@@ -475,7 +508,7 @@ foo:
bar - service charge 2ct/km
bar:
\tAll fine :)"""
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
i = "!cars list"
res = run_bot(self, num[alice], i)
@@ -486,7 +519,7 @@ foo:
bar - service charge 2ct/km
bar:
\tAll fine :)"""
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
def test_list_explicit_car(self):
i = "!cars ls foo"
@@ -495,13 +528,13 @@ bar:
"""foo - service charge 4ct/km
foo:
\tAll fine :)"""
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
def test_list_invalid_explicit_car(self):
i = "!cars ls alice"
res = run_bot(self, num[alice], i)
- o = 'ERROR: "alice" is no available car\n'
- self.assertEqual(res.stdout, o)
+ o = 'NotRegisteredError: "alice" is no available car\n'
+ self.assertEqual(res, o)
class TestTankenCmd(unittest.TestCase):
@@ -523,7 +556,7 @@ alice:
\t<- bob 3.33
\t<- charlie 3.33
\tBalance: 6.66"""
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
def test_tanken_unknown_user(self):
i = \
@@ -540,13 +573,13 @@ New Balance:
alice:
\t<- charlie 3.33
\tBalance: 3.33"""
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
def test_tanken_3users_with_car(self):
i = "!cars add foo 0.04"
res = run_bot(self, num[alice], i)
o = 'added "foo" as an available car'
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
i = \
"""!tanken 10 alice foo
@@ -570,13 +603,13 @@ Car foo:
\t<- bob 0.40
\t<- charlie 0.40
\tBalance: 1.20"""
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
def test_tanken_unknown_user_with_car(self):
i = "!cars add foo 0.04"
res = run_bot(self, num[alice], i)
o = 'added "foo" as an available car'
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
i = \
"""!tanken 10 alice foo
@@ -599,13 +632,13 @@ Car foo:
\t<- alice 0.80
\t<- bob 0.40
\tBalance: 1.20"""
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
def test_tanken_3users_with_car(self):
i = "!cars add foo 0.04"
res = run_bot(self, num[alice], i)
o = 'added "foo" as an available car'
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
i = \
"""!tanken 10 alice foo
@@ -629,7 +662,7 @@ Car foo:
\t<- bob 0.40
\t<- charlie 0.40
\tBalance: 1.20"""
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
class TestTransferCmd(unittest.TestCase):
@@ -645,7 +678,7 @@ class TestTransferCmd(unittest.TestCase):
New Balance: alice <- 5.00 charlie
New Balance: bob -> 5.00 charlie
"""
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
def test_transfer_credit(self):
res = run_bot(self, num[alice], "!schieb bob 5")
@@ -657,7 +690,7 @@ New Balance: bob -> 5.00 charlie
New Balance: alice <- 5.00 charlie
New Balance: bob -> 5.00 charlie
"""
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
def test_transfer_change_dept(self):
res = run_bot(self, num[alice], "!schieb bob 5")
@@ -669,7 +702,7 @@ New Balance: bob -> 5.00 charlie
New Balance: alice <- 5.00 charlie
New Balance: bob -> 5.00 charlie
"""
- self.assertEqual(res.stdout, o)
+ self.assertEqual(res, o)
#TODO: tanken, transfer, cars pay
@@ -680,11 +713,11 @@ class TestFuckCmd(unittest.TestCase):
def test_fuck_unregistered(self):
res = run_bot(self, "+4971576357", "!fuck")
- self.assertEqual(res.stdout, 'ERROR: you must register first')
+ self.assertEqual(res, 'NotRegisteredError: please register first using !register')
def test_fuck_nothing(self):
res = run_bot(self, num[alice], "!fuck")
- self.assertEqual(res.stdout, 'Nothing to rewind')
+ self.assertEqual(res, 'Nothing to rewind')
def test_fuck_transaction(self):
for cmd in ["fuck", "undo", "rewind"]:
@@ -696,7 +729,7 @@ Rewinding:
!schieb bob 10
alice <- 10.00 bob
"""
- self.assertEqual(res.stdout, msg)
+ self.assertEqual(res, msg)
compare_state("test/state.json_3users")
def test_fuck_split(self):
@@ -709,7 +742,7 @@ Rewinding:
alice <- 1.00 bob
alice <- 1.00 charlie
"""
- self.assertEqual(res.stdout, msg)
+ self.assertEqual(res, msg)
compare_state("test/state.json_3users")
def test_fuck_transaction(self):
@@ -721,7 +754,7 @@ Rewinding:
!schieb bob 10
alice <- 10.00 bob
"""
- self.assertEqual(res.stdout, msg)
+ self.assertEqual(res, msg)
compare_state("test/state.json_3users")
class TestScheduleCmd(unittest.TestCase):
@@ -740,7 +773,7 @@ alice:
\t<- bob 1.00
\t<- charlie 1.00
\tBalance: 2.00"""
- self.assertEqual(res.stdout, msg)
+ self.assertEqual(res, msg)
save_state("test/state.json_schedule_weekly")
@@ -751,8 +784,8 @@ Rewinding:
split 3 bob charlie
alice <- 1.00 bob
alice <- 1.00 charlie
-Cancelled the weekly cmd "stuff\""""
- self.assertEqual(res.stdout, msg)
+Cancelled the weekly command "stuff\""""
+ self.assertEqual(res, msg)
# Last exec onw week ago
reset_state_string(scheduled_state_template.substitute(last_time=now-timedelta(7),schedule="weekly"))
@@ -767,12 +800,12 @@ alice:
\t<- bob 2.00
\t<- charlie 2.00
\tBalance: 4.00""".format(now.isoformat())
- self.assertEqual(res.stdout, msg)
+ self.assertEqual(res, msg)
compare_state("test/state.json_schedule_weekly")
res = run_bot(self, num[alice], "!fuck")
- self.assertEqual(res.stdout, 'Nothing to rewind')
+ self.assertEqual(res, 'Nothing to rewind')
# Last exec two week ago
reset_state_string(scheduled_state_template.substitute(last_time=now-timedelta(14),schedule="weekly"))
@@ -794,7 +827,7 @@ alice:
\t<- charlie 3.00
\tBalance: 6.00""".format((now - timedelta(7)).isoformat(),
now.isoformat())
- self.assertEqual(res.stdout, msg)
+ self.assertEqual(res, msg)
os.remove("test/state.json_schedule_weekly")
@@ -810,7 +843,7 @@ alice:
\t<- bob 1.00
\t<- charlie 1.00
\tBalance: 2.00"""
- self.assertEqual(res.stdout, msg)
+ self.assertEqual(res, msg)
save_state("test/state.json_schedule_monthly")
@@ -821,8 +854,8 @@ Rewinding:
split 3 bob charlie
alice <- 1.00 bob
alice <- 1.00 charlie
-Cancelled the monthly cmd "stuff\""""
- self.assertEqual(res.stdout, msg)
+Cancelled the monthly command "stuff\""""
+ self.assertEqual(res, msg)
# Last exec one month ago
if now.month > 1:
@@ -842,12 +875,12 @@ alice:
\t<- bob 2.00
\t<- charlie 2.00
\tBalance: 4.00""".format(now.isoformat())
- self.assertEqual(res.stdout, msg)
+ self.assertEqual(res, msg)
compare_state("test/state.json_schedule_monthly")
res = run_bot(self, num[alice], "!fuck")
- self.assertEqual(res.stdout, 'Nothing to rewind')
+ self.assertEqual(res, 'Nothing to rewind')
# Last exec two month ago
if now.month > 2:
@@ -875,7 +908,7 @@ alice:
\t<- charlie 3.00
\tBalance: 6.00""".format(one_month_ago.isoformat(),
now.isoformat())
- self.assertEqual(res.stdout, msg)
+ self.assertEqual(res, msg)
os.remove("test/state.json_schedule_monthly")
@@ -897,7 +930,32 @@ alice:
fiat:
\t-> alice 3.00
\tBalance: -3.00"""
- self.assertEqual(res.stdout, msg)
+ self.assertEqual(res, msg)
+
+ def test_cancel_not_allowed(self):
+ run_bot(self, num[alice], "!cars add fiat 0.5")
+ run_bot(self, num[alice], "!monthly versicherung cars pay fiat 3")
+ res = run_bot(self, num[bob], "!cancel versicherung")
+ msg = "NotAllowedError: only alice, the original creator can cancel this command"
+ self.assertEqual(res, msg)
+
+ def test_cancel_not_registered(self):
+ res = run_bot(self, "+490000", "!cancel versicherung")
+ msg = "NotRegisteredError: please register first using !register"
+ self.assertEqual(res, msg)
+
+ def test_cancel_not_scheduled_cmd(self):
+ res = run_bot(self, num[alice], "!cancel versicherung")
+ msg = 'NotRegisteredError: "versicherung" is not a scheduled command'
+ self.assertEqual(res, msg)
+
+ def test_cancel(self):
+ run_bot(self, num[alice], "!cars add fiat 0.5")
+ run_bot(self, num[alice], "!monthly versicherung cars pay fiat 3")
+ res = run_bot(self, num[alice], "!cancel versicherung")
+ msg = 'Cancelled the monthly command "versicherung"'
+ self.assertEqual(res, msg)
if __name__ == '__main__':
unittest.main()
+ BOT.terminate()