diff options
Diffstat (limited to 'regional-currency/ask_questions.py')
-rwxr-xr-x | regional-currency/ask_questions.py | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/regional-currency/ask_questions.py b/regional-currency/ask_questions.py new file mode 100755 index 0000000..8c36311 --- /dev/null +++ b/regional-currency/ask_questions.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 + +import base64 +import os +import subprocess +import uuid +from typing import Callable, Dict, TypeVar + +log = open("setup.log", "ab", buffering=0) +CONFIG_FILE = "config/user.conf" + + +def load_conf() -> Dict[str, str]: + conf = {} + with open(CONFIG_FILE, "r") as f: + for kv in f.read().splitlines(): + if len(kv) != 0: + [k, v] = [part.strip() for part in kv.split("=", 1)] + conf[k] = v.strip('"').replace('\\"', '"') + return conf + + +conf = load_conf() + + +def add_conf(name: str, value: str): + conf[name] = value + content = "" + for key, value in conf.items(): + escaped = value.replace('"', '\\"') + content += f'{key}="{escaped}"\n' + with open(CONFIG_FILE, "w") as f: + f.write(content) + + +def run_cmd( + cmd: list[str], input: str | None = None, env: Dict[str, str] | None = None +) -> int: + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + input=input.encode() if input is not None else None, + stdin=subprocess.DEVNULL if input is None else None, + env=env, + ) + log.write(result.stdout) + if result.returncode != 0: + print(result.stdout.decode("utf-8"), end="") + return result.returncode + + +def try_cmd( + cmd: list[str], input: str | None = None, env: Dict[str, str] | None = None +) -> bool: + return run_cmd(cmd, input, env) == 0 + + +A = TypeVar("A") +T = TypeVar("T") + + +def custom( + name: str | None, + action: Callable[[], str | None], + default: T | None = None, + check: Callable[[str], T | None] = lambda it: it, + fmt: Callable[[T], str] = lambda it: str(it), +): + if name is not None: + prev = conf.get(name, None) + if prev is not None: # If got previous value + checked = check(prev) + if checked is not None: # And previous value is valid + add_conf(name, fmt(checked)) # Add again in case normalization changed + return checked + + while True: + raw = action() + if raw is None: + if default is not None: + if name is not None: + add_conf(name, fmt(default)) + return default + continue + else: + checked = check(raw) + if checked is not None: + if name is not None: + add_conf(name, fmt(checked)) + return checked + + +def ask( + name: str | None, + msg: str, + default: T | None = None, + check: Callable[[str], T | None] = lambda it: it, + fmt: Callable[[T], str] = lambda it: str(it), +) -> T: + def do_ask(): + log.write(msg.encode()) + log.write("\n".encode()) + raw = input(msg).strip() + if raw == "": + if default is None: + print("You must enter a value") + return None + return raw + + return custom(name, do_ask, default, check, fmt) + + +def ask_str(name: str | None, msg: str, default: str | None = None) -> str: + return ask(name, msg, default) + + +def ask_currency(name: str, msg: str, default: str | None = None) -> str: + def check_currency(currency: str) -> str | None: + currency = currency.upper() + if not currency.isascii() or not currency.isalpha(): + print("The currency name must be an ASCII alphabetic string") + elif len(currency) < 3 or 11 < len(currency): + print("The currency name had to be between 3 and 11 characters long") + else: + return currency + return None + + return ask(name, msg, default, check_currency) + + +def ask_host(name: str, msg: str, default: str | None = None) -> str: + def check_host(host: str) -> str | None: + success = True + for subdomain in ["backend", "bank", "exchange"]: + success = try_cmd(["ping", "-c", "1", f"{subdomain}.{host}"]) and success + if success: + return host + else: + return None + + return ask(name, msg, default, check_host) + + +def ask_yes_no(name: str | None, msg: str, default: bool | None = None) -> bool: + def check_yes_no(raw: str) -> bool | None: + raw = raw.lower() + if raw == "y" or raw == "yes": + return True + elif raw == "n" or raw == "no": + return False + else: + print("Expected 'y' or 'n'") + return None + + return ask(name, msg, default, check_yes_no, lambda it: "y" if it else "n") + + +ask_currency( + "CURRENCY", + "1. Enter the name of the regional currency (e.g. 'NETZBON'): ", + "NETZBON", +) +ask_currency( + "FIAT_CURRENCY", "2. Enter the name of the fiat currency (e.g. 'CHF'): ", "CHF" +) +ask_str( + "BANK_NAME", + "3. Enter the human-readable name of the bank (e.g. 'Taler Bank'): ", + "Taler Bank", +) +ask_host("DOMAIN_NAME", "4. Enter the domain name (e.g. 'example.com'): ") +if ask_yes_no("ENABLE_TLS", "5. Setup TLS using Let's Encrypt? (Y/n): ", True): + ask_str("TLS_EMAIL", "5.1. Enter an email address for Let's Encrypt: ") + + def ask_tos(): + print( + "5.2. Please read the Terms of Service at https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf." + ) + if not ask_yes_no( + None, + "5.2. You must agree in order to register with the ACME server. Do you agree? (y/n): ", + False, + ): + print("You must agree in order to register with the ACME server") + return None + else: + return "y" + + custom("TLS_TOS", ask_tos) + add_conf("PROTO", "https") +else: + add_conf("PROTO", "http") + +if not ask_yes_no( + "DO_OFFLINE", + "6. Do you want Taler Exchange keys on this server (Y) or externally on another server (n): ", + True, +): + ask_str("MASTER_PUBLIC_KEY", "6.1. Enter the exchange-offline master public key: ") + +if ask_yes_no( + "DO_TELESIGN", + "7. Setup sms two-factor authentication using Telesign https://www.telesign.com? (Y/n): ", + True, +): + + def ask_telesign(): + customer_id = ask_str(None, "7.1. Enter your Telesign Customer ID: ") + api_key = ask_str(None, "7.2. Enter your Telesign API Key: ") + phone_number = ask_str( + None, + "7.3. Enter a phone number to test your API key (e.g. '+447911123456'): ", + ) + auth_token = base64.b64encode(f"{customer_id}:{api_key}".encode()).decode() + if not try_cmd( + ["libeufin-tan-sms.sh", phone_number], + "12345", + {**os.environ, "AUTH_TOKEN": auth_token}, + ): + print( + "Failed to send an sms using Telesign API, check your credentials and phone number" + ) + return None + code = ask_str(None, f"7.4. Enter the code received by {phone_number} : ") + if code != "12345": + print( + f"Wrong code got '{code}' expected '12345', check your credentials and phone number" + ) + return None + return auth_token + + custom("TELESIGN_AUTH_TOKEN", ask_telesign) +ask_str( + "BANK_ADMIN_PASSWORD", + "8. Enter the admin password for the bank (or press enter to autogenerate password): ", + str(uuid.uuid4()), +) |