Files
pest-nocturne/app.py

103 lines
3.9 KiB
Python

#!/usr/bin/env python3
# Pest Nocturne — WSU EM019 study suite, freemium paywall (BTCPay)
import os, json, uuid, time, sqlite3, hmac, hashlib
from flask import Flask, request, jsonify, send_from_directory, redirect
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(BASE_DIR, "orders.db")
HTML = os.path.join(BASE_DIR, "index.html")
# BTCPay config (store FDT6DHWEaA7DF5WFePkp4pufgQ9yy1G6JfzTCwey1jYn)
BTCPAY_URL = os.environ.get("BTCPAY_URL", "https://10.30.20.140")
BTCPAY_STORE = os.environ.get("BTCPAY_STORE", "FDT6DHWEaA7DF5WFePkp4pufgQ9yy1G6JfzTCwey1jYn")
BTCPAY_KEY = os.environ.get("BTCPAY_KEY", "206f08d0a8efc4cfbe524188756c5bd1b2fd277a")
PRICE_USD = os.environ.get("PRICE_USD", "5.00")
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "pn-nocturne-webhook-secret-1788251891")
app = Flask(__name__)
def db():
c = sqlite3.connect(DB_PATH)
c.execute("CREATE TABLE IF NOT EXISTS orders (id TEXT PRIMARY KEY, created REAL, paid INTEGER DEFAULT 0)")
c.commit()
return c
def create_btcpay_invoice(order_id):
"""Create a BTCPay invoice and return its checkout URL, or None."""
import urllib.request, ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE # BTCPay uses a self-signed cert
payload = json.dumps({
"amount": PRICE_USD,
"currency": "USD",
"metadata": {"orderId": order_id},
"checkout": {"redirectURL": f"https://nocturne.thetempleofdoom.com/?paid=1&order={order_id}"},
}).encode()
req = urllib.request.Request(
f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE}/invoices",
data=payload, method="POST",
headers={"Authorization": f"token {BTCPAY_KEY}", "Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=20, context=ctx) as r:
data = json.loads(r.read())
url = data.get("checkoutLink") or data.get("checkout_url")
if url:
# API returns a LAN address; rewrite to the public BTCPay domain
# so customers outside the network can reach the checkout.
url = url.replace("10.30.20.140", "btcpay.thetempleofdoom.com")
return url
except Exception as e:
app.logger.error(f"BTCPay invoice error: {e}")
return None
@app.route("/")
def index():
return send_from_directory(BASE_DIR, "index.html")
@app.route("/images/<path:filename>")
def images(filename):
return send_from_directory(os.path.join(BASE_DIR, "images"), filename)
@app.route("/api/status")
def api_status():
oid = request.args.get("order", "")
c = db()
row = c.execute("SELECT paid FROM orders WHERE id=?", (oid,)).fetchone()
c.close()
return jsonify({"paid": bool(row and row[0])})
@app.route("/unlock", methods=["POST"])
def unlock():
order_id = uuid.uuid4().hex[:16]
c = db()
c.execute("INSERT INTO orders (id, created, paid) VALUES (?,?,0)", (order_id, time.time()))
c.commit()
c.close()
checkout_url = create_btcpay_invoice(order_id)
if not checkout_url:
return jsonify({"error": "could not create invoice"}), 502
return jsonify({"order_id": order_id, "checkout_url": checkout_url})
@app.route("/webhook/btcpay", methods=["POST"])
def webhook():
raw = request.get_data()
sig = request.headers.get("BTCPay-Sig", "")
exp = "sha256=" + hmac.new(WEBHOOK_SECRET.encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(exp, sig):
return "bad sig", 401
data = json.loads(raw) if raw else {}
etype = data.get("type", "")
order_id = (data.get("metadata") or {}).get("orderId")
if etype == "InvoiceSettled" and order_id:
c = db()
c.execute("UPDATE orders SET paid=1 WHERE id=?", (order_id,))
c.commit()
c.close()
app.logger.info(f"order {order_id} settled")
return jsonify({"ok": True})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)