def delete_user(auth, client): """ Delete a user :auth: dict :client: users_client object """ log("What user you want to delete?") user_to_delete = find_user_by_username(auth, client) if user_to_delete is False: log("Could not find user.", serv="ERROR") return False confirmation = yes_or_no("You really want to delete %s?" % user_to_delete["username"]) if confirmation is False: log("Aborted...") return False try: client.users_user_id_delete(int(user_to_delete["id"])) log("Successfully deleted user %s" % user_to_delete["username"], serv="SUCCESS") return True except: log("Could not delete user %s. Error by backend" % user_to_delete["username"], serv="ERROR") return False
def find_user_by_username(auth, client): """ Helper function that returns a user object that is found by searching for a username :auth: dict :client: users_client object :username: str :returns: user object """ user_to_find = None user_object = None user_to_find = input("Username: "******"Username too short (>=3).", serv="Error") return False # find user id users = client.users_get() for user in users: if user.to_dict()["username"] == user_to_find: user_object = user.to_dict() if user_object is None: log("Could not find user %s. Are you sure about the username?" % user_to_find, serv="ERROR") return False return user_object
def show_item(auth, client, itemid): """ Shows detail of a single item :auth: dict :client: item_client object :returns: bool """ log(client.items_item_id_get(itemid).to_dict()) return True
def welcome_banner(): """ Prints a big ascii interpretation from the club mate logo :returns: bool """ logo_banner = """ =?I777777II?????II777777?= 7777? I777? I77I I77 =777777777+ I77= =77I ?777777= I7777777777777+ =77+ =77= I7777777777 77777777777777777+ 77? ?77 I7777777777777 =7777777777777777777 ?77 77= I777777777777777? 7777777777777777777777 77= ?7I ?77777777777777777 77777777777777777777777 77 +77 77777777777777777777 7777777777777777777777777 77 77 777777777777777777777 77777777777777777777777777 +7= 77 77777777777777777777777 777777777777777777777777777+ 77 I7 777777777777777777777777 +7777777777777I+= +777777 77 =7 77777777777777777777?= +?77777777777? ?77777777 77 7 7777777777= =7777777777777777777777777= 7777777777777 7I 77 77777+ ==+????++= I7777777777777777? 7 I7 77777777I+ ?77 ?777?+?7777+ +I777777777777777777777777 77 7I 77777777777777777777777777 7777 777777777777 = 777777777777777777777777777777 7 7 77777777777777777777777777+ ? 7 I=777777777 777 =77777777777777777777777777777 77 77 77777777777777777777777777=777 =+== I7777 7+ 777777777777777777777777777777 +7 77 77777777777777777777777777 I? 77 77777=7777777777777777777777777777777777777? 7 77 77777777777777777777777777777777= 777777777+?77777777777777I7777777777II7777777777I 7 77 77777777777777777777777777 I 777777 ?777777777777777+7777777777 77777777777+ =7 ?7 77777777777777777777777? 77+ 7777777777777777=7777+7777+777777777777 I7 7 ?777777777777777777I +7 =77777777777777+7777 7777 777777777777 7? 77 777777777777777 77777777777+77777 77 I=77777777777+ 7 7 +777777777777 777777777 777=77+77I777777777777 7I 77 I777777777 +7777=77=777?=777 777777777777 I7 77 7777777 7 777=?777777? I77777777777= 7 77 I777 77777777777 77777777777 7 77 ? ?777777777777I 77777777777 7 ?7 777777777777777I77777777777 77 77 777777777777I==I777777777= 7I +7+ 77777 ?77777777777+ 77 +7+ 777 ?77777777 77 77= 77 +77777 77= 77 7 I7? 77I 77 +77? 77I +77I =777= 7777I+ ?7777+ ++?IIII7III??+= github.com/k4cg/heiko (v{}) / github.com/k4cg/matomat-service """.format(pkg_resources.require("heiko")[0].version) os.system('clear') log(logo_banner) return True
def nfc_detect(): key = DEFAULT_KEY.copy() v, uid, ttype, dat = mnfc.read(1, 1, key, False) header = "" try: header = dat.decode() except: pass if v == 0: log("found " + ttype + " with uid " + uid) return uid, header return None, None
def create_item(auth, client): """ Asks admin for details and creates new item in the backend :auth: dict :returns: bool """ name = input("Name of Drink: ") # TODO: Remove this? MaaS does not seem to mind non-alphanumerical charaters... if name.isalnum() is False: log("Item name not valid. Please be alphanumerical.", serv="ERROR") return False cost = float(input("Price in EUR (i.e. 1 or 1.20): ")) * 100 if cost < 0: log("Negative price is not allowed ;)", serv="ERROR") return False try: client.items_post(name, int(cost)) log("Successfully added new item with name %.2f and cost %.2f" % (name, float(cost) / 100), serv="SUCCESS") except swagger_client.rest.ApiException as api_expception: log("Item could not be created in the backend: " + api_expception.body, serv="ERROR") return False return True
def reset_user_nfc(auth, auth_client, user_client): """ Gives an admin the capability to reset password for a specific user and write new nfc token. It asks for username, trys to find userid by name and then asks for new password. :auth: dict :client: users_client object :returns: bool """ log("What user you want to reset the password for?") user_to_reset = find_user_by_username(auth, user_client) if user_to_reset is False: log("Could not find user.", serv="ERROR") return False password = "".join( random.choice(string.ascii_letters + "0123456789") for i in range(23)) try: user_client.users_user_id_resetpassword_patch(user_to_reset["id"], password, password) log("Successfully reset password for user %s with id %s." % (user_to_reset["username"], user_to_reset["id"]), serv="SUCCESS") except: log("Could not reset password for user %s with id %s. Error by backend" % (user_to_reset["username"], user_to_reset["id"]), serv="ERROR") return False return nfc_format_card(auth_client, user_to_reset["username"], password)
def reset_user_password(auth, client): """ Gives an admin the capability to reset password for a specific user. It asks for username, trys to find userid by name and then asks for new password. :auth: dict :client: users_client object :returns: bool """ log("What user you want to reset the password for?") user_to_reset = find_user_by_username(auth, client) if user_to_reset is False: log("Could not find user.", serv="ERROR") return False passwordnew = getpass.getpass("Password: "******"Repeat password: "******"id"], passwordnew, passwordrepeat) log("Successfully changed password for user %s with id %s." % (user_to_reset["username"], user_to_reset["id"]), serv="SUCCESS") return True except: log("Could not reset password for user %s with id %s. Error by backend" % (user_to_reset["username"], user_to_reset["id"]), serv="ERROR") return False
def add_credits_admin(auth, client): """ Add credits by admin :auth: dict :client: users_client object :returns: bool """ log("What user you want to add the credits for?") user = find_user_by_username(auth, client) if user is False: log("Could not find user.", serv="ERROR") return False add_credits = float(input("Paid amount (EUR): ")) * 100 try: r = client.users_user_id_credits_add_patch(user["id"], int(add_credits)) auth["user"]["credits"] = r.to_dict()["credits"] log("Successfully set the credits for user %s to %.2f Euro" % (user["username"], auth["user"]["credits"] / 100), serv="SUCCESS") return True except: log("Could not add %.2f Euro credits for user %s. Backend error." % (add_credits, user["username"]), serv="ERROR") return False
def reset_credits(auth, client): """ Set credits to defined amount by admin :auth: dict :client: users_client object :returns: bool """ log("What user you want to set the credits for?") user_to_reset = find_user_by_username(auth, client) if user_to_reset is False: log("Could not find user.", serv="ERROR") return False new_credits = float(input("Set new credits (EUR): ")) * 100 try: client.users_user_id_credits_withdraw_patch(user_to_reset["id"], user_to_reset["credits"]) r = client.users_user_id_credits_add_patch(user_to_reset["id"], int(new_credits)) auth["user"]["credits"] = r.to_dict()["credits"] log("Successfully set the credits for user %s to %.2f Euro" % (user_to_reset["username"], new_credits / 100), serv="SUCCESS") return True except: log("Could not set the credits for user %s to %.2f Euro. Backend error." % (user_to_reset["username"], new_credits), serv="ERROR") return False
def show_user_stats(auth, client): """ Presents consumption statistics to user :auth: dict :client: users_client object :returns: bool """ try: stats = client.users_user_id_stats_get(auth["user"]["id"]) except swagger_client.rest.ApiException: log("Could fetch stats from the database.", serv="ERROR") return False drinks = [] sum_money = 0 for drink in stats: money = float(drink["consumed"]) * float(drink["cost"]) / 100 sum_money = sum_money + money drinks.append([drink["name"], drink["consumed"], money]) log("Your consumption statistics:\n") log( tabulate(drinks, headers=["Name", "Consumptions", "Coins spent (EUR)"], tablefmt="presto", floatfmt=".2f")) log("\nCoins spent overall: {:.2f} EUR".format(sum_money)) return True
def nfc_write(uid, header, token, key=None): if key is None: key = DEFAULT_KEY.copy() secLen = 3 * 16 hb = header.encode() hb += b"\x00" * (secLen - len(hb)) datLen = 14 * secLen b = token.encode() b += b"\x00" * (datLen - len(b)) v = mnfc.write(1, 15, uid, hb + b, key, False) if v == 0: log("write successful")
def change_password(auth, client): """ Gives user the capability to reset password for himself/herself. :auth: dict :client: users_client object :returns: bool """ password = getpass.getpass("Current Password: "******"New Password: "******"Repeat password: "******"user"]["id"], password, passwordnew, passwordrepeat) log("Successfully changed your password!", serv="SUCCESS") return True except: log("Could not set your password. Error by backend", serv="ERROR") return False
def add_credits(auth, client): """ Asks user to input the amount he put into the box and adds this amount of credits to his/her account :auth: dict :returns: bool """ try: credits = float(input("EUR: ")) if credits < 0 or credits > 100: raise ValueError except ValueError: log("Invalid input. Valid values: 1-100", serv="ERROR") return False # calc input from eur into cents cents = credits * 100 try: # send update request to backend r = client.users_user_id_credits_add_patch(str(auth["user"]["id"]), int(cents)) # TODO: Replace hack that updates local auth object to reflect changes into the banner auth["user"]["credits"] = r.to_dict()["credits"] # notify user log("Your credit is now %.2f" % (auth["user"]["credits"] / 100), serv="SUCCESS") return True except: log("Updating your credits in the backend was not successful. Ask people for help", serv="ERROR") return False
def delete_item(auth, client): """ Deletes an item from the backend :auth: dict :client: item_client object :returns: bool """ itemid = input("What item (ID)?: ") item_name = client.items_item_id_get(itemid).to_dict()["name"] really_delete = yes_or_no("Do you really want to delete %s?" % item_name) if really_delete is False: log("Aborted") return False try: client.items_item_id_delete(itemid) log("Item with id %s was deleted" % itemid, serv="SUCCESS") return True except: log("Could not delete item with id %s" % itemid, serv="ERROR") return False
def create_user(auth, client): """ Asks administrator for details and creates a new user in the database. :auth: dict :returns: bool """ is_admin = yes_or_no("Admin?") if is_admin: admin = 1 else: admin = 0 name = input("Username: "******"Username too short (>=3).", serv="Error") return False if name.isalnum() is False: log("Username not valid. Please be alphanumerical.", serv="Error") return False password = getpass.getpass("Password: "******"Repeat password: "******"Error creating user", serv="ERROR") return False
def banner(auth=None): """ Prints a fancy ascii art banner to user and (if already logged in) greets the person with username and current credits :auth: dict :returns: bool """ mate_banner = """ __ __ _ _____ ___ __ __ _ _____ | \/ | / \|_ _/ _ \| \/ | / \|_ _| | |\/| | / _ \ | || | | | |\/| | / _ \ | | | | | |/ ___ \| || |_| | | | |/ ___ \| | |_| |_/_/ \_\_| \___/|_| |_/_/ \_\_| """ os.system('clear') log(mate_banner) if auth is not None: if auth["user"]["admin"] is True: log("Hi %s, current credits: %.2f Euro. You are admin!\n" % (auth["user"]["username"], auth["user"]["credits"] / 100)) else: log("Hi %s, current credits: %.2f Euro.\n" % (auth["user"]["username"], auth["user"]["credits"] / 100)) return True
def nfc_format_card(auth_client, username, password): uid, header = nfc_detect() if uid is not None: ans = input("overwrite card? [yN] ") if ans != "y": return False ans = input("token lifetime in days? ") if not ans.isnumeric(): log("invalid lifetime") return False days = int(ans) token = "" try: auth2 = auth_client.auth_login_post(username, password, validityseconds=days * 3600 * 24).to_dict() token = auth2["token"] except swagger_client.rest.ApiException: log("Wrong password!", serv="ERROR") return False except (ConnectionRefusedError, urllib3.exceptions.MaxRetryError): log("Connection to backend was refused!", serv="ERROR") return False nfc_write(uid, "matomat1:" + username + ":", token) return True return False
def list_users(auth, client): """ Shows all users from the database to an successfully authenticated administrator :auth: dict :returns: bool """ try: users = client.users_get() except swagger_client.rest.ApiException: log("Could fetch users from the database.", serv="ERROR") return False it = [] for u in users: d = u.to_dict() it.append( [d["id"], d["username"], float(d["credits"]) / 100, d["admin"]]) log("List of current users in the database:\n") log( tabulate(it, headers=["ID", "Username", "Credits (EUR)", "Admin?"], tablefmt="presto", floatfmt=".2f")) return True
def consume_item(auth, client, itemid): """ Sends request to the backend that user took 1 item out of the fridge :auth: dict :itemid: int :returns: bool """ # Lets try to be a little funny prost_msgs = [ "Well, drink responsible", "Gesondheid (Cheers in Afrikaans)", "Gan bay (Cheers in Mandarin)", "Na zdravi (Cheers in Czech)", "Proost (Cheers in Dutch)", "Ah la vo-tre sahn-tay (Cheers in French)", "Zum Wohl! (Cheers in German)", "Yamas (Cheers in Greek)", "Slawn-cha (Cheers in Irish Gaelic)", "Salute (Cheers in Italian)", "Kanpai (Cheers in Japanese)", "Gun bae (Cheers in Korean)", "Na zdrowie (Cheers in Polish)", "Happy hacking!", "Well.. just hackspace things.", ] try: client.items_item_id_consume_patch(itemid) # TODO: Temp hack to display correct credits in banner cost = float(client.items_item_id_get(itemid).to_dict()["cost"]) auth["user"]["credits"] = auth["user"]["credits"] - cost log(random.choice(prost_msgs), serv="SUCCESS") log("Cost: %.2f Euro" % (cost / 100), serv="SUCCESS") return True except swagger_client.rest.ApiException: log("Not enough credits, dude.", serv="ERROR") return False except: log("Something went wrong, contact developer!", serv="ERROR") return False
def show_help(items_client, admin=False, cfgobj=None): """ Shows the basic navigation to the user. :items_client: object :admin: bool :returns: bool """ if admin is True: actions = admin_actions.copy() else: actions = user_actions.copy() try: if cfgobj["accounting"]["user_can_add_credits"] is False: del actions[USER_KEY_INSERT_COINS] except KeyError: pass # Reset consumables, to avoid stale entries: consumables.clear() try: for item in items_client.items_get(): item_dict = item.to_dict() action_key = str(item_dict["id"]) consumables.update({action_key: item_dict}) actions.update({ action_key: "Consume {} ({:.2f})".format(item_dict['name'], item_dict['cost'] / 100) }) except swagger_client.rest.ApiException: log("Could not get items from the database.", serv="ERROR") log("Available actions:") for key in sorted(actions.keys()): log("[%s] %s" % (key, actions[key])) return True
def update_item(auth, client): """ Adjust the name/price of an item :auth: dict :returns: bool """ item_id = input("ID of item: ") if item_id.isalnum() is False: return False # Ask for new Cost try: new_cost = float(input("New price in EUR (i.e. 1 or 1.20): ")) * 100 except ValueError: return False if new_cost < 0: log("Negative price is not allowed ;)", serv="ERROR") return False # Ask for new Name # TODO as not needed # get current item name current_item = client.items_item_id_get(item_id).to_dict() current_cost = current_item["cost"] current_name = current_item["name"] try: client.items_item_id_patch(item_id, name=current_name, cost=int(new_cost)) log("Successfully modified price of %s from %s to %s" % (current_name, float(current_cost) / 100, float(new_cost) / 100), serv="SUCCESS") except swagger_client.rest.ApiException as api_expception: log("Item could not be updated in the backend: " + api_expception.body, serv="ERROR") return False return True
def transfer_coins(auth, client): """ Transfer credits to another user :auth_client: dict :user_client: dict :returns: bool """ log("What user you want to send credits to?") target_user = find_user_by_username(auth, client) if target_user is False: # error message already printed at this point by find_user_by_username() return False # Ask for amount to transfer try: credits_to_transfer = float( input("How many credits you want to transfer (i.e. 1 or 1.20): ") ) * 100 except ValueError: return False try: req = client.users_user_id_credits_transfer_patch( user_id=target_user['id'], credits=int(credits_to_transfer)) transferred_credits = float(req.to_dict()['credits']) / 100 log("Successfully transferred {:.2f} credits to user {}".format( transferred_credits, target_user['username']), serv="SUCCESS") return True except: log("Error transferring credits to user {}".format( target_user['username']), serv="ERROR") return False
def create_user_nfc(auth_client, user_client): """ Asks administrator for details and creates a new NFC user in the database (= user with random dummy password and NFC token). :auth: dict :returns: bool """ name = input("Username: "******"Username too short (>=3).", serv="Error") return False if name.isalnum() is False: log("Username not valid. Please be alphanumerical.", serv="Error") return False is_admin = yes_or_no("Admin?") if is_admin: admin = 1 else: admin = 0 password = "".join( random.choice(string.ascii_letters + "0123456789") for i in range(23)) try: user_client.users_post(name, password, password, admin) except: log("Error creating user", serv="ERROR") return False return nfc_format_card(auth_client, name, password)
def list_items_stats(auth, client): """ Lists stats for all items in the database :auth: dict :returns: bool """ try: items = client.items_stats_get() except swagger_client.rest.ApiException: log("Could not show items from the database.", serv="ERROR") it = [] revenue = 0.0 for i in items: d = i.to_dict() it.append([d["id"], float(d["cost"]) / 100, d["name"], d["consumed"]]) revenue += float(d["cost"]) / 100 * float(d["consumed"]) log(tabulate(it, headers=["ID", "Cost (EUR)", "Drink", "Consumptions"], floatfmt=".2f", tablefmt="presto")) log("total revenue (EUR): %.2f" % revenue) return True
def sigalrm_handler(signal, frame): log("\nAuto-logout timer triggered!") sys.exit(1)
def migrate_user(auth, client, cfgobj): """ Migration function that reads data from old matomat sqlite and creates user in the backend :auth: dict :client: userauth object :cfgobj: config dict :returns: bool """ # Check if configured database is there and can be read try: migration_database = cfgobj["accounting"]["migration_database"] if os.access(migration_database, os.R_OK): matomat_db = sqlite3.connect(migration_database) else: raise IOError except (IOError, KeyError): log("Migration database cannot be found or is not configured. Aborting.", serv="ERROR") return False # First, ask for Admin yes/no is_admin = yes_or_no("Should the migrated user be admin?") if is_admin: admin = 1 else: admin = 0 # Search Username log("Please give username to look for in sqlite from matomat.db") name = input("Username: "******"Username too short (>=3).", serv="Error") return False if name.isalnum() is False: log("Username not valid. Please be alphanumerical.", serv="Error") return False log("Looking for user %s in given sqlite..." % name) # fetch results from sqlite cur = matomat_db.cursor() cur.execute("SELECT username, credits from user where username = \"%s\";" % name) rows = cur.fetchall() matomat_db.commit() matomat_db.close() if len(rows) > 1: log("Search resulted in multiple result sets... exitting", serv="ERROR") return False # assign values user_to_migrate, credits_to_migrate = rows[0] log("Found user {} with credits {:.2f} Euro!".format(user_to_migrate, int(credits_to_migrate) / 100), serv="SUCCESS") confirmation = yes_or_no("Wanna migrate her?") if confirmation is False: log("Aborting...") return False log("Please add new credentials!") password = getpass.getpass("Password: "******"Repeat password: "******"Successfully created user %s" % user_to_migrate, serv="SUCCESS") except: log("Error creating user", serv="ERROR") return False # Adding credits try: if float(credits_to_migrate) < 0: credits_to_migrate = str(int(credits_to_migrate)).replace('-', '') client.users_user_id_credits_withdraw_patch(new_user.to_dict()["id"], int(credits_to_migrate)) log("Set credit to -{:.2f}".format(float(credits_to_migrate) / 100), serv="SUCCESS") else: client.users_user_id_credits_add_patch(new_user.to_dict()["id"], int(credits_to_migrate)) log("Set credit to {:.2f}".format(float(credits_to_migrate) / 100), serv="SUCCESS") except: log("Error setting credits {:.2f}".format(float(credits_to_migrate) / 100), serv="ERROR") return False return True
def user_menu(auth, auth_client, items_client, users_client, service_client, cfgobj): """ Shows the menu to the user, clears screen, draws the navigation screen This is kind of the main loop of heiko. If you need new options, add them here otherwise they are not being executed. :auth: dict :items_client: object :users_client: object :service_client: object :cfgobj: dict :returns: is_logged_in, is_exit (both bool) """ signal.alarm(AUTOLOGOUT_TIME_SECONDS) try: optionInput = input(">>> ") if optionInput not in user_actions.keys( ) and optionInput not in consumables.keys(): option = USER_KEY_HELP else: option = optionInput except EOFError: return user_exit(cfgobj) if option in consumables.keys(): consume_item(auth, items_client, consumables[option]['id']) say(cfgobj, "cheers") if option == USER_KEY_INSERT_COINS: add_credits(auth, users_client) say(cfgobj, "transaction_success") if option == USER_KEY_SHOW_STATS: show_user_stats(auth, users_client) if option == USER_KEY_TRANSFER_COINS: transfer_coins(auth, users_client) if option == USER_KEY_ADMINISTRATION: is_exit = False say(cfgobj, "admin") draw_help = True while is_exit is False: is_exit, draw_help = admin_menu(auth, auth_client, items_client, users_client, service_client, cfgobj, draw_help=draw_help) # when exit was executed, draw normal user help again option = USER_KEY_HELP if option == USER_KEY_CHANGE_PASSWORD: change_password(auth, users_client) if option == USER_KEY_NFC: username = auth["user"]["username"] log("Put your card on the reader now.") log("Relogin required:") password = getpass.getpass('Password: ') nfc_format_card(auth_client, username, password) if option == USER_KEY_HELP: banner(auth) show_help(items_client, admin=False, cfgobj=cfgobj) if option == USER_KEY_EXIT: return user_exit(cfgobj) return True, False
def login(maas_builder, auth_client, cfgobj): """ Shows banner, asks user to authenticate via username/password and creates auth token that we reuse after auth was successful once. :returns: tuple """ is_logged_in = False auth = None welcome_banner() log("Please authenticate yourself!") token = "" user = "" password = "" try: print("User: "******"", flush=True) uid = "" if cfgobj['nfc']['enable']: while sys.stdin not in select.select([sys.stdin], [], [], 0)[0]: uid, header = nfc_detect() if uid: break time.sleep(0.2) if uid: token = nfc_read(uid) else: user = sys.stdin.readline().strip() password = getpass.getpass('Password: ') except EOFError: say(cfgobj, "error") return is_logged_in, auth if token: t = token.split(".")[1] try: userdict = json.loads( base64.b64decode(t + "=" * (4 - len(t) % 4)).decode()) except json.JSONDecodeError: say(cfgobj, "error") log("Token json error!", serv="ERROR") time.sleep(1) except binascii.Error: say(cfgobj, "error") log("Token base64 error!", serv="ERROR") time.sleep(1) auth = {"token": token, "user": userdict} try: users_client = maas_builder.build_users_client(auth["token"]) tmp = users_client.users_user_id_get(userdict["id"]).to_dict() for key in tmp: auth["user"][key] = tmp[key] is_logged_in = True greet_user(cfgobj, auth["user"]["username"]) except swagger_client.rest.ApiException: say(cfgobj, "error") log("Invalid token!", serv="ERROR") time.sleep(1) except (ConnectionRefusedError, urllib3.exceptions.MaxRetryError): say(cfgobj, "error") log("Connection to backend was refused!", serv="ERROR") time.sleep(5) else: try: auth = auth_client.auth_login_post(user, password).to_dict() is_logged_in = True greet_user(cfgobj, auth["user"]["username"]) except swagger_client.rest.ApiException: say(cfgobj, "error") log("Wrong username and/or password!", serv="ERROR") time.sleep(1) except (ConnectionRefusedError, urllib3.exceptions.MaxRetryError): say(cfgobj, "error") log("Connection to backend was refused!", serv="ERROR") time.sleep(5) return is_logged_in, auth
def admin_exit(cfgobj): say(cfgobj, "quit") log("Switching back to normal menu, sir.", serv="SUCCESS") return True, False