diff --git a/__init__.py b/__init__.py index 0f8a435..fe6b506 100644 --- a/__init__.py +++ b/__init__.py @@ -23,6 +23,13 @@ } ] +tpos_redirect_paths = [ + { + "from_path": "/.well-known/assetlinks.json", + "redirect_to_path": "/api/v1/well-known/assetlinks.json", + } +] + scheduled_tasks: list[asyncio.Task] = [] @@ -47,6 +54,7 @@ def tpos_start(): __all__ = [ "db", "tpos_ext", + "tpos_redirect_paths", "tpos_start", "tpos_static_files", "tpos_stop", diff --git a/crud.py b/crud.py index e903236..00e0d96 100644 --- a/crud.py +++ b/crud.py @@ -1,3 +1,6 @@ +import json +from typing import Any + from lnbits.db import Database from lnbits.helpers import urlsafe_short_hash @@ -6,6 +9,54 @@ db = Database("ext_tpos") +WRAPPER_ASSETLINKS_CACHE_KEY = "wrapper_assetlinks" + + +async def get_wrapper_assetlinks_cache() -> tuple[dict | list, int] | None: + row: Any = await db.fetchone( + "SELECT value, updated_at FROM tpos.cache WHERE key = :key", + {"key": WRAPPER_ASSETLINKS_CACHE_KEY}, + ) + if not row or not row.get("value"): + return None + return json.loads(row["value"]), int(row["updated_at"] or 0) + + +async def set_wrapper_assetlinks_cache( + assetlinks: dict | list, updated_at: int +) -> None: + payload = json.dumps(assetlinks) + existing: Any = await db.fetchone( + "SELECT key FROM tpos.cache WHERE key = :key", + {"key": WRAPPER_ASSETLINKS_CACHE_KEY}, + ) + if existing: + await db.execute( + """ + UPDATE tpos.cache + SET value = :value, updated_at = :updated_at + WHERE key = :key + """, + { + "key": WRAPPER_ASSETLINKS_CACHE_KEY, + "value": payload, + "updated_at": updated_at, + }, + ) + return + + await db.execute( + """ + INSERT INTO tpos.cache (key, value, updated_at) + VALUES (:key, :value, :updated_at) + """, + { + "key": WRAPPER_ASSETLINKS_CACHE_KEY, + "value": payload, + "updated_at": updated_at, + }, + ) + async def create_tpos(data: CreateTposData) -> Tpos: tpos_id = urlsafe_short_hash() diff --git a/migrations.py b/migrations.py index 5c4be83..886c670 100644 --- a/migrations.py +++ b/migrations.py @@ -299,3 +299,16 @@ async def m023_add_allow_price_adjustment(db: Database): await db.execute(""" ALTER TABLE tpos.pos ADD allow_price_adjustment BOOLEAN DEFAULT true; """) + + +async def m024_add_assetlinks_cache(db: Database): + """ + Add cache table for TPoS wrapper Android asset links. + """ + await db.execute(""" + CREATE TABLE tpos.cache ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at INTEGER NOT NULL DEFAULT 0 + ); + """) diff --git a/services.py b/services.py index 9400c96..d7c33b6 100644 --- a/services.py +++ b/services.py @@ -1,3 +1,4 @@ +import time from typing import Any import httpx @@ -11,8 +12,47 @@ from lnbits.settings import settings from loguru import logger +from .crud import get_wrapper_assetlinks_cache, set_wrapper_assetlinks_cache from .helpers import from_csv, inventory_tags_to_list +WRAPPER_ASSETLINKS_URL = ( + "https://github.com/lnbits/TPoS-Stripe-Tap-to-Pay-Wrapper-Stripev5" + "/releases/latest/download/assetlinks.json" +) +WRAPPER_ASSETLINKS_CACHE_SECONDS = 60 * 60 + + +async def fetch_wrapper_assetlinks() -> dict | list: + now = int(time.time()) + cached = await get_wrapper_assetlinks_cache() + if cached: + cached_assetlinks, cached_at = cached + cache_fresh = now - cached_at < WRAPPER_ASSETLINKS_CACHE_SECONDS + if cache_fresh: + return cached_assetlinks + + try: + async with httpx.AsyncClient( + follow_redirects=True, headers={"User-Agent": settings.user_agent} + ) as client: + resp = await client.get(WRAPPER_ASSETLINKS_URL, timeout=10) + resp.raise_for_status() + assetlinks = resp.json() + except Exception as exc: + if cached: + logger.warning(f"Using cached TPoS wrapper assetlinks.json: {exc!s}") + return cached[0] + raise RuntimeError("Unable to fetch TPoS wrapper assetlinks.json.") from exc + + if not isinstance(assetlinks, (dict, list)): + if cached: + logger.warning("Using cached TPoS wrapper assetlinks.json: invalid JSON") + return cached[0] + raise RuntimeError("TPoS wrapper assetlinks.json is not valid JSON.") + + await set_wrapper_assetlinks_cache(assetlinks, now) + return assetlinks + async def deduct_inventory_stock(wallet_id: str, inventory_payload: dict) -> None: wallet = await get_wallet(wallet_id) diff --git a/static/js/index.js b/static/js/index.js index 3cfb790..a9547e2 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -777,6 +777,22 @@ window.app = Vue.createApp({ tpos.loadingWrapperToken = false } }, + async warmWrapperAssetlinks(enabled) { + if (!enabled) { + return + } + try { + await LNbits.api.request( + 'GET', + '/tpos/api/v1/well-known/assetlinks.json' + ) + } catch (error) { + Quasar.Notify.create({ + type: 'warning', + message: 'Unable to cache TPoS wrapper assetlinks.json.' + }) + } + }, openUrlDialog(id) { if (this.tposs.stripe_card_payments) { this.urlDialog.data = _.findWhere(this.tposs, { diff --git a/templates/tpos/index.html b/templates/tpos/index.html index df3d85b..8f18dd7 100644 --- a/templates/tpos/index.html +++ b/templates/tpos/index.html @@ -832,6 +832,7 @@