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 pollcount: int = 0 def startwin(): global root 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(): global pollcount pollcount = 0 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="Hier klicken zum Starten", 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.", font=font) main_taler_explainer.place(relx=0, rely=0, relwidth=1, relheight=0.4) main_taler_explainer2 = tk.Label(master=main_taler, text=f"Die {bank.currency}s werden nach\ninaktivität ausgezahlt.", font=fontsmall) main_taler_explainer2.place(relx=0, rely=0.4, relwidth=1, relheight=0.4) def transaction_end_button(): global withdrawal if withdrawal is not None: withdrawal.end() main_taler_end = tk.Button(master=main_taler, command=transaction_end_button, text="", font=font) main_taler_end.place(relx=0, rely = 0.8, relwidth=1, relheight=0.2) global 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) global pollcount pollcount = 0 while withdrawal.poll(): if pollcount > 30: 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