diff options
| author | herkulessi <git@herkulessi.de> | 2026-03-31 23:43:13 +0200 |
|---|---|---|
| committer | herkulessi <git@herkulessi.de> | 2026-03-31 23:43:13 +0200 |
| commit | 9023f3fce9765abdb8d70eeb4a1d5ee1aa2daaa7 (patch) | |
| tree | dbfd71095fdbc56e770786be247065aed9890e8c /taler2.py | |
Bezahlommat 2.0 initial commit oder so
Diffstat (limited to 'taler2.py')
| -rw-r--r-- | taler2.py | 255 |
1 files changed, 255 insertions, 0 deletions
diff --git a/taler2.py b/taler2.py new file mode 100644 index 0000000..8bb8a3a --- /dev/null +++ b/taler2.py @@ -0,0 +1,255 @@ +import base64 +import threading +import qrcode +import io +import requests +import tkinter as tk +import tkinter.font +import sys +import traceback +import config +from fakegeld import MoneyTransaction +from threading import Thread + +class UnreachableError(RuntimeError): + def __init__(self, *args: object) -> None: + super().__init__(*args) + +class TalerBank: + def __init__(self, url, username, currency, max_withdrawal, password: str | None = None, token: str | None = None) -> None: + self.url: str = url + self.username: str = username + self.__token: str | None = token + self.__password: str | None = password + self.currency: str = currency + self.max_withdrawal: str = max_withdrawal + self.__headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + } + if self.__password is not None: + self.__headers['Authorization'] = 'Basic ' + str(base64.b64encode(f'{self.username}:{self.__password}'.encode())) + if self.__token is not None: + self.__headers['Authorization'] = 'Bearer secret-token:' + self.__token + + def _post(self, uri, json=None): + print("_post1", "POST", uri, json) + request = requests.post(f"{self.url}{uri}", json=json, headers=self.__headers) + print("_post2", request.status_code, request.text) + return request + def _get(self, uri): + print("GET", uri) + request = requests.get(f"{self.url}{uri}", headers=self.__headers) + print(request.status_code, request.text) + return request + def create_withdrawal(self): + return Withdrawal(self) + +class Withdrawal: + def __init__(self, bank: TalerBank) -> None: + self.lock = threading.Lock() + self.bank: TalerBank = bank + self.amount: float = 0 + self.state: str = "pending" + request = self.bank._post(f"/accounts/{self.bank.username}/withdrawals", { "no_amount_to_wallet": True }) + request_json = request.json() + self.withdrawal_id: str = request_json["withdrawal_id"] + self.withdrawal_uri: str = request_json["taler_withdraw_uri"] + + def _final(self, target): + print('trying to reach', target) + with self.lock: + if self.state == target: + return + if self.state == ("confirmed" if target == "aborted" else "aborted"): + raise RuntimeError(f"Withdrawal {self.withdrawal_id} is already final, finalizing again won't work") + request = self.bank._post(f"/accounts/{self.bank.username}/withdrawals/{self.withdrawal_id}/{target[:-2]}", { "amount": f"{self.bank.currency}:{self.amount}"} if target == "confirmed" else None ) + if not request.status_code == 204: + raise RuntimeError(f"Withdrawal {self.withdrawal_id} could not be {target}: {request.status_code}\n{request.text}") + while self.poll(target=target): + pass + + + def poll(self, target="selected"): + state = None + with self.lock: + state = self.state + if target == state: + return False + if state == "aborted": + raise UnreachableError(f"Withdrawal {self.withdrawal_id} was polled after getting aborted which is a dead end. Nothing can happen now.") + if state == "confirmed": + raise UnreachableError(f"Withdrawal {self.withdrawal_id} was polled after getting confirmed which is a dead end. Nothing can happen now.") + if target == "selected" and not state == "pending": + raise RuntimeError(f"Withdrawal {self.withdrawal_id} is not pending and therefore can't be polled") + if target == "aborted" and state == "confirmed": + raise RuntimeError(f"Withdrawal {self.withdrawal_id} is confirmed and therefore can't ever get cancelled") + if target == "confirmed" and not state == "selected": + raise RuntimeError(f"Withdrawal {self.withdrawal_id} is {state} and therefore can't reach confirmed before selection and confirmation") + + request = self.bank._get(f"/withdrawals/{self.withdrawal_id}?old_state={self.state}&timeout_ms=1000") + if not request.status_code == 200: + raise RuntimeError(f"Withdrawal {self.withdrawal_id} could not be polled: {request.status_code}\n{request.text}") + + request_json = request.json() + with self.lock: + self.state = request_json["status"] + state = request_json["status"] + if target == state: + return False + if state == "aborted": + raise UnreachableError(f"Withdrawal {self.withdrawal_id} was polled after getting aborted which is a dead end. Nothing can happen now.") + if state == "confirmed": + raise UnreachableError(f"Withdrawal {self.withdrawal_id} was polled after getting confirmed which is a dead end. Nothing can happen now.") + return True + + def qr(self): + return qrcode.make(self.withdrawal_uri) + def end(self): + if self.amount == 0 or self.state == "pending": + try: + self.abort() + except UnreachableError as _: + self.confirm() + else: + try: + self.confirm() + except UnreachableError as _: + self.abort() + + def abort(self): + self._final("aborted") + def confirm(self): + self._final("confirmed") + + + def set_amount(self, amount): + with self.lock: + self.amount = amount + def add_amount(self, amount): + with self.lock: + self.amount += amount + + +# global taler_qrcode +taler_qrcode: tk.Label +withdrawal: Withdrawal | None +bank: TalerBank + +def startwin(): + global root + topratio = 0.2 + botratio = 0.15 + + mode = "menu" + + root = tk.Tk() + root.tk_strictMotif(boolean=True) + + #dpi = root.winfo_fpixels('li') + #font = ("Sans", int(24 * 72 / dpi+0.5)) + #fontsmall = ("Sans", int(20 * 72 / dpi+0.5)) + font = tk.font.Font(size=-24) + fontsmall = tk.font.Font(size=-20) + + if len(sys.argv) >= 2 and sys.argv[1] == "-s": + root.maxsize(width=480, height=320) + root.minsize(width=480, height=320) + else: + root.attributes("-fullscreen", True) # run fullscreen + root.wm_attributes("-topmost", True) # keep on top + + + global main_start + main_start = tk.Frame(master=root) + main_start.place(relx=0, rely=0, width=480, height=320) + global main_scan + main_scan = tk.Frame(master=root) + global main_taler + main_taler = tk.Frame(master=root) + + def main_start_button_func(): + main_start.place_forget() + main_scan.place(relx=0, rely=0, relwidth=1, relheight=1) + main_start_button = tk.Button(master=main_start, command=main_start_button_func, text="Click here to start", font=font) + main_start_button.place(relx=0, rely=0, relwidth=1, relheight=1) + + main_taler_explainer = tk.Label(master=main_taler, text="Gebe jetzt die Scheine in den Leser.\nDie überweiseung wird nach inaktivität abgeschlossen.",) + main_taler_explainer.place(relx=0, rely=0, relwidth=1, relheight=1) + + global taler_qrcode + #print(taler_qrcode) + taler_qrcode = tk.Label(master=main_scan, text="(Auflade-QR-Code wird\nhier angezeigt werden.)") + print(taler_qrcode) + taler_qrcode.place(relx=0, rely = 0, relwidth=0.6, relheight=1) + + taler_explainer = tk.Label(master=main_scan, text="Bitte scanne\nden QR-Code\nmit deinem Wallet\nund akzeptiere\ndie Aufladung\num fortzufahren", font=fontsmall) + taler_explainer.place(relx=.65, rely=0, relwidth=0.35, relheight=1) + + root.mainloop() + print("Exited Mainloop") + return +def stop(): + global gui + global root + root.quit() + gui.join() + sys.exit() + +def main(): + global taler_qrcode, gui, bank, withdrawal + if hasattr(config, 'PASSWORD'): + bank = TalerBank(url = config.URL, username = config.USERNAME, password = config.PASSWORD, currency = config.CURRENCY, max_withdrawal = config.MAX_WITHDRAWAL) + if hasattr(config, 'TOKEN'): + bank = TalerBank(url = config.URL, username = config.USERNAME, token = config.TOKEN, currency = config.CURRENCY, max_withdrawal = config.MAX_WITHDRAWAL) + + gui = Thread(target=startwin) + gui.start() + while not "main_scan" in globals(): + pass + while True: + try: + withdrawal = Withdrawal(bank) + transaction = MoneyTransaction(withdrawal.end, withdrawal.add_amount) + try: + from PIL import ImageTk + image = withdrawal.qr() + image.resize((200,200)) + image = ImageTk.PhotoImage(image.resize((200,200))) + taler_qrcode.configure(image = image, width = 200, height = 200) + pollcount = 0 + while withdrawal.poll(): + if pollcount > 500: + withdrawal.end() + pollcount += 1 + main_scan.place_forget() + main_taler.place(relx=0, rely=0, relwidth=1, relheight=1) + transaction.start() + try: + while withdrawal.poll(target="confirmed"): + pass + except UnreachableError: + pass + except Exception: + traceback.print_exc() + main_scan.place_forget() + main_taler.place_forget() + main_start.place(relx=0, rely=0, relwidth=1, relheight=1) + finally: + main_scan.place_forget() + main_taler.place_forget() + main_start.place(relx=0, rely=0, relwidth=1, relheight=1) + transaction.stop() + except Exception: + main_scan.place_forget() + main_taler.place_forget() + main_start.place(relx=0, rely=0, relwidth=1, relheight=1) + traceback.print_exc() + + + + +if __name__ == '__main__': + main() + +# vim: ts=4 noexpandtab |
