From aa560fd89dc5f593e16671ddb92532b91b3543a2 Mon Sep 17 00:00:00 2001 From: Arc <33088785+arcbtc@users.noreply.github.com> Date: Fri, 8 Dec 2023 15:43:40 +0000 Subject: [PATCH] Add files via upload --- LICENSE | 37 +- README.md | 17 + __init__.py | 36 + config.json | 7 + crud.py | 53 ++ lnurl.py | 1 + manifest.json | 9 + migrations.py | 28 + models.py | 24 + static/image/template.png | Bin 0 -> 11678 bytes tasks.py | 59 ++ templates/tpos/_api_docs.html | 79 +++ templates/tpos/_tpos.html | 21 + templates/tpos/index.html | 1150 +++++++++++++++++++++++++++++++ templates/tpos/tpos.html | 1205 +++++++++++++++++++++++++++++++++ views.py | 88 +++ views_api.py | 136 ++++ 17 files changed, 2948 insertions(+), 2 deletions(-) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 config.json create mode 100644 crud.py create mode 100644 lnurl.py create mode 100644 manifest.json create mode 100644 migrations.py create mode 100644 models.py create mode 100644 static/image/template.png create mode 100644 tasks.py create mode 100644 templates/tpos/_api_docs.html create mode 100644 templates/tpos/_tpos.html create mode 100644 templates/tpos/index.html create mode 100644 templates/tpos/tpos.html create mode 100644 views.py create mode 100644 views_api.py diff --git a/LICENSE b/LICENSE index 678845a..c97e67a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,39 @@ -MIT License +> Use any license you like, its your extension. -Copyright (c) 2023 LNbits +--- + +# DON'T BE A DICK PUBLIC LICENSE + +> Version 1.1, December 2016 + +> Copyright (C) [year] [fullname] + +Everyone is permitted to copy and distribute verbatim or modified +copies of this license document. + +> DON'T BE A DICK PUBLIC LICENSE +> TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +1. Do whatever you like with the original work, just don't be a dick. + + Being a dick includes - but is not limited to - the following instances: + + 1a. Outright copyright infringement - Don't just copy this and change the name. + 1b. Selling the unmodified original with no work done what-so-ever, that's REALLY being a dick. + 1c. Modifying the original work to contain hidden harmful content. That would make you a PROPER dick. + +2. If you become rich through modifications, related works/services, or supporting the original work, +share the love. Only a dick would make loads off this work and not buy the original work's +creator(s) a pint. + +3. Code is provided with no warranty. Using somebody else's code and bitching when it goes wrong makes +you a DONKEY dick. Fix the problem yourself. A non-dick would submit the fix back. + +--- + +# MIT License + +> Copyright (c) [year] [fullname] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..d314c71 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Temp - [LNbits](https://github.com/lnbits/lnbits) extension + +For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Extensions) + +## A template extension you can fork and use as a base for a template + +Let the hacking begin! After you have forked this extension you can copy functions from other extensions that you might need. Usually the README is used as a guide of how to us ethe extensiobn. + +### Usage + +1. Enable extension +2. Create a Temp\ + ![create](https://imgur.com/8jNj8Zq.jpg) +3. Open the link for the Temp\ + ![open](https://imgur.com/LZuoWzb.jpg) +4. Press button to generate an invoice and pay\ + ![pay](https://imgur.com/tOwxn77.jpg) \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..357df12 --- /dev/null +++ b/__init__.py @@ -0,0 +1,36 @@ +import asyncio + +from fastapi import APIRouter, Request, Response +from fastapi.routing import APIRoute + +from lnbits.db import Database +from lnbits.helpers import temp_renderer +from lnbits.tasks import catch_everything_and_restart +from typing import Callable +from fastapi.responses import JSONResponse + +db = Database("ext_temp") + +temp_ext: APIRouter = APIRouter( + prefix="/temp", tags=["Temp"] +) + +temp_static_files = [ + { + "path": "/temp/static", + "name": "temp_static", + } +] + +def temp_renderer(): + return temp_renderer(["temp/temps"]) + + +from .tasks import wait_for_paid_invoices +from .views import * +from .views_api import * + + +def temp_start(): + loop = asyncio.get_event_loop() + loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..eb94eab --- /dev/null +++ b/config.json @@ -0,0 +1,7 @@ +{ + "name": "Temp", + "short_description": "Minimal extension to build on", + "tile": "/temp/static/image/temp.png", + "contributors": ["arcbtc"], + "min_lnbits_version": "0.0.1" +} diff --git a/crud.py b/crud.py new file mode 100644 index 0000000..e3c8f63 --- /dev/null +++ b/crud.py @@ -0,0 +1,53 @@ +from typing import List, Optional, Union + +from lnbits.helpers import urlsafe_short_hash + +from . import db +from .models import CreateTempData, Temp, TempClean, LNURLCharge +from loguru import logger + + +async def create_temp(wallet_id: str, data: CreateTempData) -> Temp: + temp_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO temp.temp (id, wallet, name, total) + VALUES (?, ?, ?, ?) + """, + ( + temp_id, + wallet_id, + data.name, + data.total + ), + ) + temp = await get_temp(temp_id) + assert temp, "Newly created temp couldn't be retrieved" + return temp + + +async def get_temp(temp_id: str) -> Optional[Temp]: + row = await db.fetchone("SELECT * FROM temp.temp WHERE id = ?", (temp_id,)) + return Temp(**row) if row else None + +async def get_temps(wallet_ids: Union[str, List[str]]) -> List[Temp]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f"SELECT * FROM temp.temp WHERE wallet IN ({q})", (*wallet_ids,) + ) + return [Temp(**row) for row in rows] + +async def update_temp(temp_id: str, **kwargs) -> Temp: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE temp.temp SET {q} WHERE id = ?", (*kwargs.values(), temp_id) + ) + temp = await get_temp(temp_id) + assert temp, "Newly updated temp couldn't be retrieved" + return temp + +async def delete_temp(temp_id: str) -> None: + await db.execute("DELETE FROM temp.temp WHERE id = ?", (temp_id,)) \ No newline at end of file diff --git a/lnurl.py b/lnurl.py new file mode 100644 index 0000000..a9a7b82 --- /dev/null +++ b/lnurl.py @@ -0,0 +1 @@ +# Maybe your extensions needs some LNURL stuff, for anything LNURL always make sure you are on spec https://github.com/lnurl/luds \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..b31672d --- /dev/null +++ b/manifest.json @@ -0,0 +1,9 @@ +{ + "repos": [ + { + "id": "temp", + "organisation": "lnbits", + "repository": "temp" + } + ] +} diff --git a/migrations.py b/migrations.py new file mode 100644 index 0000000..7214e62 --- /dev/null +++ b/migrations.py @@ -0,0 +1,28 @@ +# The migration file is like a blockchain, never edit only add! + +async def m001_initial(db): + """ + Initial templates table. + """ + await db.execute( + """ + CREATE TABLE temp.temp ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + name TEXT NOT NULL + ); + """ + ) + + +# Here we are adding an extra field to the database + +async def m002_addtip_wallet(db): + """ + Add total to templates table + """ + await db.execute( + """ + ALTER TABLE temp.temp ADD total withdrawlimit INTEGER DEFAULT 0,; + """ + ) diff --git a/models.py b/models.py new file mode 100644 index 0000000..3195103 --- /dev/null +++ b/models.py @@ -0,0 +1,24 @@ +from sqlite3 import Row +from typing import Optional, List +from pydantic import BaseModel + + +class CreateTempData(BaseModel): + wallet: Optional[str] + name: Optional[str] + total: Optional[int] + +class Temp(BaseModel): + id: str + wallet: str + name: str + total: Optional[int] + + @classmethod + def from_row(cls, row: Row) -> "Temp": + return cls(**dict(row)) + +class CreateUpdateItemData(BaseModel): + items: List[Item] + +# add something lnurly \ No newline at end of file diff --git a/static/image/template.png b/static/image/template.png new file mode 100644 index 0000000000000000000000000000000000000000..24aa00d7b4dacc05c235cbbaadffad760d6a3445 GIT binary patch literal 11678 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 literal 0 HcmV?d00001 diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..214bcd1 --- /dev/null +++ b/tasks.py @@ -0,0 +1,59 @@ +import asyncio + +from loguru import logger + +from lnbits.core.models import Payment +from lnbits.core.services import create_invoice, pay_invoice, websocketUpdater +from lnbits.helpers import get_current_extension_name +from lnbits.tasks import register_invoice_listener + +from .crud import get_temp + + +####################################### +########## RUN YOU TASKS HERE ######### +####################################### + + +# the usual task is to listen to invoices related to this extension + +async def wait_for_paid_invoices(): + invoice_queue = asyncio.Queue() + register_invoice_listener(invoice_queue, get_current_extension_name()) + + while True: + payment = await invoice_queue.get() + await on_invoice_paid(payment) + + +# do somethhing when an invoice related top this extension is paid + +async def on_invoice_paid(payment: Payment) -> None: + if payment.extra.get("tag") != "temp": + return + + temp_id = payment.extra.get("tempId") + assert temp_id + + temp = await get_temp(temp_id) + assert temp + + # update something + + data_to_update = { + "total" temp.total + payment.amount + } + + await update_temp(temp_id=temp_id, **data_to_update.dict()) + + + # here we could send some data to a websocket on wss:///api/v1/ws/ + + some_payment_data = { + "name": temp.name, + "amount": payment.amount, + "fee": payment.fee, + "checking_id": payment.checking_id + } + + await websocketUpdater(temp_id, str(some_payment_data)) diff --git a/templates/tpos/_api_docs.html b/templates/tpos/_api_docs.html new file mode 100644 index 0000000..abe4fc0 --- /dev/null +++ b/templates/tpos/_api_docs.html @@ -0,0 +1,79 @@ + + + + + + GET /temp/api/v1/temps +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
+ Returns 200 OK (application/json) +
+ [<temp_object>, ...] +
Curl example
+ curl -X GET {{ request.base_url }}temp/api/v1/temps -H "X-Api-Key: + <invoice_key>" + +
+
+
+ + + + POST /temp/api/v1/temps +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+ {"name": <string>, "currency": <string*ie USD*>} +
+ Returns 201 CREATED (application/json) +
+ {"currency": <string>, "id": <string>, "name": + <string>, "wallet": <string>} +
Curl example
+ curl -X POST {{ request.base_url }}temp/api/v1/temps -d '{"name": + <string>, "currency": <string>}' -H "Content-type: + application/json" -H "X-Api-Key: <admin_key>" + +
+
+
+ + + + + DELETE + /temp/api/v1/temps/<temp_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
Returns 204 NO CONTENT
+ +
Curl example
+ curl -X DELETE {{ request.base_url + }}temp/api/v1/temps/<temp_id> -H "X-Api-Key: <admin_key>" + +
+
+
+
diff --git a/templates/tpos/_tpos.html b/templates/tpos/_tpos.html new file mode 100644 index 0000000..9157728 --- /dev/null +++ b/templates/tpos/_tpos.html @@ -0,0 +1,21 @@ + + + +

+ Thiago's Point of Sale is a secure, mobile-ready, instant and shareable + point of sale terminal (PoS) for merchants. The PoS is linked to your + LNbits wallet but completely air-gapped so users can ONLY create + invoices. To share the Temp hit the hash on the terminal. +

+ Created by + Tiago Vasconcelos. +
+
+
diff --git a/templates/tpos/index.html b/templates/tpos/index.html new file mode 100644 index 0000000..ffde185 --- /dev/null +++ b/templates/tpos/index.html @@ -0,0 +1,1150 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + New Temp + + + + + +
+
+
Temp
+
+
+ Export to CSV +
+
+ + + + + + + ${ col.label } + + Tip Wallet + Tip Options % + Withdraw PIN + Withdraw Limit + Withdraw Premium + + + + + + + + + + + + PoS QR + Open PoS + + + Click to copy${props.row.id.substring(0,6)}... + + + + ${ (col.name == 'tip_options' && col.value ? + JSON.parse(col.value).join(", ") : col.value) } + + + Click to copy${props.row.tip_wallet.substring(0,6)}... + + N/A + + ${props.row.tip_options} + + N/A + + ${props.row.withdrawpin} + + N/A + + ${props.row.withdrawlimit} + + N/A + + ${props.row.withdrawpremium}% + + 0 + + + + + + + + + +
+
+ + + + + +
+
+
+ Add Item + Delete All + + + + + + Import + Import a JSON file + + + + + + Export + Export a JSON file + + + + +
+
+ +
+
+ + + + + + + + + + + + ${props.row.id.split(':')[1]} + + + ${props.row.title} + + + ${itemFormatPrice(props.row.price, + props.row.id)} + + + ${props.row.disabled} + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
{{SITE_TITLE}} Temp extension
+
+ + + + {% include "temp/_api_docs.html" %} + + {% include "temp/_temp.html" %} + + +
+
+ + + + + + + +
+
+ +
+
+ +
+
+ + + Hit enter to add values + + You can leave this blank. A default rounding option is available + (round amount to a value) + + + + + + + + + + +
+ Update Temp + Create Temp + Cancel +
+
+
+
+ + + + + + + + + +
+ ${itemDialog.data.id ? 'Update Item' : 'Create Item'} + Cancel +
+
+
+
+ + + + + +
+

+ ${ urlDialog.data.name }
${ + urlDialog.data.shareUrl } +

+
+
+ Copy URL + Close +
+
+
+ + + +
+ Importing ${fileDataDialog?.count} items +
+
+ + + + + + + + + + ${item.title} + ${item.description} + + + + + + + +
+ Import + Close +
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/templates/tpos/tpos.html b/templates/tpos/tpos.html new file mode 100644 index 0000000..8c4a56f --- /dev/null +++ b/templates/tpos/tpos.html @@ -0,0 +1,1205 @@ +{% extends "public.html" %} {% block toolbar_title %} {{ temp.name }} + + +{% endblock %} {% block footer %}{% endblock %} {% block page_container %} + + + +
+
+

${ amountFormatted }

+
${ fsat } sat
+
+
+ Total: ${totalFormatted}
${totalfsat} sat
+
+
+
+
+
+ + +
+
+
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + C + 0 + + + + Ok +
+
+
+
+ + + + + + + + + + + +
+ + +
+
+

${totalFormatted}

+
${totalfsat} sat
+
+
+ + + + + + + + + + + + + + + +
+ ${item.title} + +
+ +
+ ${item.quantity} +
+ +
+
+ ${item.formattedPrice} + + +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+ + + +
+
+
+
+ + + + +
${item.title}
+
${item.formattedPrice}
+
+ ${item.description} +
+
+ + + Add + +
+
+
+ + + + ${cartDrawer ? 'Hide Cart' :' Open Cart'} +
+ EXIT ATM MODE + + + + + +
+

${ amountWithTipFormatted }

+
+ ${ fsat } + sat + ( + ${ tipAmountFormatted } tip) +
+ +
+
+ Copy invoice + Pay in wallet + Close +
+
+
+ + + +
+ Would you like to leave a tip? +
+
+ ${ tip }% + +
+ + + Ok +
+
+
+ No, thanks + Close +
+
+
+ + + + + + +
+

+ {{ temp.name }}
{{ request.url }} +

+
+
+ Copy URL + Close +
+
+
+ + + + + + + + + + + + + + + No paid invoices + + + + + ${payment.amount / 1000} sats + Hash: ${payment.checking_id.slice(0, 30)}... + + + ${payment.dateFrom} + + + + + + + + + + +
Withdraw PIN
+
+ + + + +
+ +
+
+
+
+
+
+
+{% endblock %} {% block styles %} + +{% endblock %} {% block scripts %} + + +{% endblock %} diff --git a/views.py b/views.py new file mode 100644 index 0000000..8cd1fb7 --- /dev/null +++ b/views.py @@ -0,0 +1,88 @@ +from http import HTTPStatus + +from fastapi import Depends, Request +from fastapi.templating import Jinja2Temps +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse + +from lnbits.core.models import User +from lnbits.decorators import check_user_exists +from lnbits.settings import settings + +from . import temp_ext, temp_renderer +from .crud import get_temp + +temps = Jinja2Temps(directory="temps") + + +####################################### +##### ADD YOUR PAGE ENDPOINTS HERE #### +####################################### + + +# Backend admin page + +@temp_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return temp_renderer().TempResponse( + "temp/index.html", {"request": request, "user": user.dict()} + ) + + +# Frontend shareable page + +@temp_ext.get("/{temp_id}") +async def temp(request: Request, temp_id): + temp = await get_temp(temp_id) + if not temp: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Temp does not exist." + ) + return temp_renderer().TempResponse( + "temp/temp.html", + { + "request": request, + "temp": temp, + "withdrawamtemps": temp.withdrawamtemps, + "web_manifest": f"/temp/manifest/{temp_id}.webmanifest", + }, + ) + + +# Customise a manifest, or remove manifest completely + +@temp_ext.get("/manifest/{temp_id}.webmanifest") +async def manifest(temp_id: str): + remp= await get_temp(temp_id) + if not temp: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Temp does not exist." + ) + + return { + "short_name": settings.lnbits_site_title, + "name": temp.name + " - " + settings.lnbits_site_title, + "icons": [ + { + "src": settings.lnbits_custom_logo + if settings.lnbits_custom_logo + else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png", + "type": "image/png", + "sizes": "900x900", + } + ], + "start_url": "/temp/" + temp_id, + "background_color": "#1F2234", + "description": "Minimal extension to build on", + "display": "standalone", + "scope": "/temp/" + temp_id, + "theme_color": "#1F2234", + "shortcuts": [ + { + "name": temp.name + " - " + settings.lnbits_site_title, + "short_name": temp.name, + "description": temp.name + " - " + settings.lnbits_site_title, + "url": "/temp/" + temp_id, + } + ], + } diff --git a/views_api.py b/views_api.py new file mode 100644 index 0000000..6dc1bc6 --- /dev/null +++ b/views_api.py @@ -0,0 +1,136 @@ +from http import HTTPStatus +import json + +import httpx +from fastapi import Depends, Query, Request +from lnurl import decode as decode_lnurl +from loguru import logger +from starlette.exceptions import HTTPException + +from lnbits.core.crud import get_user +from lnbits.core.models import Payment +from lnbits.core.services import create_invoice +from lnbits.core.views.api import api_payment +from lnbits.decorators import ( + WalletTypeInfo, + check_admin, + get_key_type, + require_admin_key, + require_invoice_key, +) + +from . import temp_ext +from .crud import ( + create_temp, + update_temp, + delete_temp, + get_temp, + get_temps +) +from .models import CreateTempData, PayLnurlWData, LNURLCharge, CreateUpdateItemData + + +####################################### +##### ADD YOUR API ENDPOINTS HERE ##### +####################################### + + +# TYPICAL ENDPOINTS + +# get all the records belonging to the user + +@temp_ext.get("/api/v1/temps", status_code=HTTPStatus.OK) +async def api_temps( + all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type) +): + wallet_ids = [wallet.wallet.id] + if all_wallets: + user = await get_user(wallet.wallet.user) + wallet_ids = user.wallet_ids if user else [] + return [temp.dict() for temp in await get_temps(wallet_ids)] + + +# get a specific record belonging to a user + +@temp_ext.put("/api/v1/temps/{temp_id}") +async def api_temp_update( + data: CreateTempData, + temp_id: str, + wallet: WalletTypeInfo = Depends(require_admin_key), +): + if not temp_id: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Temp does not exist." + ) + temp = await get_temp(temp_id) + assert temp, "Temp couldn't be retrieved" + + if wallet.wallet.id != temp.wallet: + raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your Temp.") + temp = await update_temp(temp_id=temp_id, **data.dict()) + return temp.dict() + + +# Create a new record + +@temp_ext.post("/api/v1/temps", status_code=HTTPStatus.CREATED) +async def api_temp_create( + data: CreateTempData, wallet: WalletTypeInfo = Depends(get_key_type) +): + temp = await create_temp(wallet_id=wallet.wallet.id, data=data) + return temp.dict() + + +# Delete a record + +@temp_ext.delete("/api/v1/temps/{temp_id}") +async def api_temp_delete( + temp_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) +): + temp = await get_temp(temp_id) + + if not temp: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Temp does not exist." + ) + + if temp.wallet != wallet.wallet.id: + raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your Temp.") + + await delete_temp(temp_id) + return "", HTTPStatus.NO_CONTENT + + +# ANY OTHER ENDPOINTS YOU NEED + +# This endpoint creates a payment + +@tpos_ext.post("/api/v1/temps/payment/{temp_id}", status_code=HTTPStatus.CREATED) +async def api_tpos_create_invoice( + temp_id: str, amount: int = Query(..., ge=1), memo: str = "" +) -> dict: + temp = await get_temp(temp_id) + + if not temp: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Temp does not exist." + ) + + # we create a payment and add some tags, so tasks.py can grab the payment once its paid + + try: + payment_hash, payment_request = await create_invoice( + wallet_id=temp.wallet, + amount=amount, + memo=f"{memo} to {temp.name}" if memo else f"{temp.name}", + extra={ + "tag": "temp", + "tipAmount": tipAmount, + "tempId": tempId, + "amount": amount, + }, + ) + except Exception as e: + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) + + return {"payment_hash": payment_hash, "payment_request": payment_request} \ No newline at end of file