summaryrefslogtreecommitdiff
path: root/taler2.py
diff options
context:
space:
mode:
authorherkulessi <git@herkulessi.de>2026-03-31 23:43:13 +0200
committerherkulessi <git@herkulessi.de>2026-03-31 23:43:13 +0200
commit9023f3fce9765abdb8d70eeb4a1d5ee1aa2daaa7 (patch)
treedbfd71095fdbc56e770786be247065aed9890e8c /taler2.py
Bezahlommat 2.0 initial commit oder so
Diffstat (limited to 'taler2.py')
-rw-r--r--taler2.py255
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