feat: update offlineshop extension
This commit is contained in:
parent
24bb2e0dc9
commit
e3c7ca0726
|
@ -95,9 +95,14 @@ def register_routes(app: FastAPI) -> None:
|
|||
try:
|
||||
ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}")
|
||||
ext_route = getattr(ext_module, f"{ext.code}_ext")
|
||||
|
||||
ext_statics = getattr(ext_module, f"{ext.code}_static_files")
|
||||
for s in ext_statics:
|
||||
app.mount(s["path"], s["app"], s["name"])
|
||||
|
||||
app.include_router(ext_route)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
raise ImportError(
|
||||
f"Please make sure that the extension `{ext.code}` follows conventions."
|
||||
)
|
||||
|
|
|
@ -1,15 +1,41 @@
|
|||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.routing import Mount
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
|
||||
db = Database("ext_offlineshop")
|
||||
|
||||
offlineshop_static_files = [
|
||||
{
|
||||
"path": "/offlineshop/static",
|
||||
"app": StaticFiles(directory="lnbits/extensions/offlineshop/static"),
|
||||
"name": "offlineshop_static",
|
||||
}
|
||||
]
|
||||
|
||||
offlineshop_ext: APIRouter = APIRouter(
|
||||
prefix="/Extension",
|
||||
tags=["Offlineshop"]
|
||||
prefix="/offlineshop",
|
||||
tags=["Offlineshop"],
|
||||
# routes=[
|
||||
# Mount(
|
||||
# "/static",
|
||||
# app=StaticFiles(directory="lnbits/extensions/offlineshop/static"),
|
||||
# name="offlineshop_static",
|
||||
# )
|
||||
# ],
|
||||
)
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
||||
def offlineshop_renderer():
|
||||
return template_renderer(
|
||||
[
|
||||
"lnbits/extensions/offlineshop/templates",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
from .lnurl import * # noqa
|
||||
from .views import * # noqa
|
||||
from .views_api import * # noqa
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import hashlib
|
||||
from fastapi.params import Query
|
||||
|
||||
from starlette.requests import Request
|
||||
from lnbits.helpers import url_for
|
||||
from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore
|
||||
|
||||
from lnbits.core.services import create_invoice
|
||||
|
@ -8,8 +12,8 @@ from . import offlineshop_ext
|
|||
from .crud import get_shop, get_item
|
||||
|
||||
|
||||
@offlineshop_ext.get("/lnurl/<item_id>")
|
||||
async def lnurl_response(item_id):
|
||||
@offlineshop_ext.get("/lnurl/{item_id}", name="offlineshop.lnurl_response")
|
||||
async def lnurl_response(item_id: int = Query(...)):
|
||||
item = await get_item(item_id)
|
||||
if not item:
|
||||
return {"status": "ERROR", "reason": "Item not found."}
|
||||
|
@ -34,7 +38,7 @@ async def lnurl_response(item_id):
|
|||
|
||||
|
||||
@offlineshop_ext.get("/lnurl/cb/<item_id>")
|
||||
async def lnurl_callback(item_id):
|
||||
async def lnurl_callback(request: Request, item_id: int):
|
||||
item = await get_item(item_id)
|
||||
if not item:
|
||||
return {"status": "ERROR", "reason": "Couldn't find item."}
|
||||
|
@ -51,12 +55,12 @@ async def lnurl_callback(item_id):
|
|||
amount_received = int(request.args.get("amount") or 0)
|
||||
if amount_received < min:
|
||||
return LnurlErrorResponse(
|
||||
reason=f"Amount {amount_received} is smaller than minimum {min}."
|
||||
).dict()
|
||||
reason=f"Amount {amount_received} is smaller than minimum {min}."
|
||||
).dict()
|
||||
elif amount_received > max:
|
||||
return LnurlErrorResponse(
|
||||
reason=f"Amount {amount_received} is greater than maximum {max}."
|
||||
).dict()
|
||||
reason=f"Amount {amount_received} is greater than maximum {max}."
|
||||
).dict()
|
||||
|
||||
shop = await get_shop(item.shop)
|
||||
|
||||
|
@ -75,7 +79,7 @@ async def lnurl_callback(item_id):
|
|||
|
||||
resp = LnurlPayActionResponse(
|
||||
pr=payment_request,
|
||||
success_action=item.success_action(shop, payment_hash) if shop.method else None,
|
||||
success_action=item.success_action(shop, payment_hash, request) if shop.method else None,
|
||||
routes=[],
|
||||
)
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ from lnurl import encode as lnurl_encode # type: ignore
|
|||
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
|
||||
from pydantic import BaseModel
|
||||
from starlette.requests import Request
|
||||
from .helpers import totp
|
||||
|
||||
shop_counters: Dict = {}
|
||||
|
@ -82,20 +83,16 @@ class Item(BaseModel):
|
|||
id: int
|
||||
name: str
|
||||
description: str
|
||||
image: str
|
||||
image: Optional[str]
|
||||
enabled: bool
|
||||
price: int
|
||||
unit: str
|
||||
|
||||
@property
|
||||
def lnurl(self) -> str:
|
||||
return lnurl_encode(
|
||||
url_for("offlineshop.lnurl_response", item_id=self.id, _external=True)
|
||||
def values(self, req: Request):
|
||||
values = self.dict()
|
||||
values["lnurl"] = lnurl_encode(
|
||||
req.url_for("offlineshop.lnurl_response", item_id=self.id)
|
||||
)
|
||||
|
||||
def values(self):
|
||||
values = self._asdict()
|
||||
values["lnurl"] = self.lnurl
|
||||
return values
|
||||
|
||||
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
||||
|
@ -107,14 +104,14 @@ class Item(BaseModel):
|
|||
return LnurlPayMetadata(json.dumps(metadata))
|
||||
|
||||
def success_action(
|
||||
self, shop: Shop, payment_hash: str
|
||||
self, shop: Shop, payment_hash: str, req: Request
|
||||
) -> Optional[LnurlPaySuccessAction]:
|
||||
if not shop.wordlist:
|
||||
return None
|
||||
|
||||
return UrlAction(
|
||||
url=url_for(
|
||||
"offlineshop.confirmation_code", p=payment_hash, _external=True
|
||||
url=req.url_for(
|
||||
"offlineshop.confirmation_code", p=payment_hash
|
||||
),
|
||||
description="Open to get the confirmation code for your purchase.",
|
||||
)
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
<code
|
||||
>curl -X GET {{ request.url_root
|
||||
}}/offlineshop/api/v1/offlineshop/items -H "Content-Type:
|
||||
application/json" -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -d
|
||||
application/json" -H "X-Api-Key: {{ user.wallets[0].inkey }}" -d
|
||||
'{"name": <string>, "description": <string>, "image":
|
||||
<data-uri string>, "price": <integer>, "unit": <"sat"
|
||||
or "USD">}'
|
||||
|
@ -97,7 +97,7 @@
|
|||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}/offlineshop/api/v1/offlineshop -H
|
||||
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||
"X-Api-Key: {{ user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
@ -120,7 +120,7 @@
|
|||
>curl -X GET {{ request.url_root
|
||||
}}/offlineshop/api/v1/offlineshop/items/<item_id> -H
|
||||
"Content-Type: application/json" -H "X-Api-Key: {{
|
||||
g.user.wallets[0].inkey }}" -d '{"name": <string>,
|
||||
user.wallets[0].inkey }}" -d '{"name": <string>,
|
||||
"description": <string>, "image": <data-uri string>,
|
||||
"price": <integer>, "unit": <"sat" or "USD">}'
|
||||
</code>
|
||||
|
@ -139,7 +139,7 @@
|
|||
<code
|
||||
>curl -X GET {{ request.url_root
|
||||
}}/offlineshop/api/v1/offlineshop/items/<item_id> -H "X-Api-Key:
|
||||
{{ g.user.wallets[0].inkey }}"
|
||||
{{ user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
|
|
@ -331,5 +331,5 @@
|
|||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="https://cdn.jsdelivr.net/npm/pica@6.1.1/dist/pica.min.js"></script>
|
||||
<script src="/offlineshop/static/js/index.js"></script>
|
||||
<script src="{{ url_for('offlineshop_static', path='js/index.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -2,25 +2,24 @@ import time
|
|||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi.params import Depends
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.models import Payment, User
|
||||
from lnbits.core.crud import get_standalone_payment
|
||||
|
||||
from . import offlineshop_ext
|
||||
from . import offlineshop_ext, offlineshop_renderer
|
||||
from .crud import get_item, get_shop
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
@offlineshop_ext.get("/")
|
||||
# @validate_uuids(["usr"], required=True)
|
||||
# @check_user_exists()
|
||||
async def index(request: Request):
|
||||
return await templates.TemplateResponse("offlineshop/index.html", {"request": request,"user":g.user})
|
||||
from fastapi import Request, HTTPException
|
||||
|
||||
|
||||
@offlineshop_ext.get("/print")
|
||||
@offlineshop_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
return offlineshop_renderer().TemplateResponse("offlineshop/index.html", {"request": request, "user": user.dict()})
|
||||
|
||||
|
||||
@offlineshop_ext.get("/print", response_class=HTMLResponse)
|
||||
async def print_qr_codes(request: Request):
|
||||
items = []
|
||||
for item_id in request.args.get("items").split(","):
|
||||
|
@ -34,29 +33,32 @@ async def print_qr_codes(request: Request):
|
|||
}
|
||||
)
|
||||
|
||||
return await templates.TemplateResponse("offlineshop/print.html", {"request": request,"items":items})
|
||||
return offlineshop_renderer().TemplateResponse("offlineshop/print.html", {"request": request,"items":items})
|
||||
|
||||
|
||||
@offlineshop_ext.get("/confirmation")
|
||||
async def confirmation_code():
|
||||
async def confirmation_code(p: str):
|
||||
style = "<style>* { font-size: 100px}</style>"
|
||||
|
||||
payment_hash = request.args.get("p")
|
||||
payment_hash = p
|
||||
payment: Payment = await get_standalone_payment(payment_hash)
|
||||
if not payment:
|
||||
return (
|
||||
f"Couldn't find the payment {payment_hash}." + style,
|
||||
HTTPStatus.NOT_FOUND,
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail=f"Couldn't find the payment {payment_hash}." + style
|
||||
)
|
||||
if payment.pending:
|
||||
return (
|
||||
f"Payment {payment_hash} wasn't received yet. Please try again in a minute."
|
||||
+ style,
|
||||
HTTPStatus.PAYMENT_REQUIRED,
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||
detail=f"Payment {payment_hash} wasn't received yet. Please try again in a minute." + style
|
||||
)
|
||||
|
||||
if payment.time + 60 * 15 < time.time():
|
||||
return "too much time has passed." + style
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.REQUEST_TIMEOUT,
|
||||
detail="Too much time has passed." + style
|
||||
)
|
||||
|
||||
|
||||
item = await get_item(payment.extra.get("item"))
|
||||
shop = await get_shop(item.shop)
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
from typing import Optional
|
||||
import json
|
||||
|
||||
from typing import List, Optional
|
||||
from fastapi.params import Depends
|
||||
from pydantic.main import BaseModel
|
||||
|
||||
from http import HTTPStatus
|
||||
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
|
||||
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse, JSONResponse # type: ignore
|
||||
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type
|
||||
from lnbits.utils.exchange_rates import currencies
|
||||
from lnbits.requestvars import g
|
||||
|
||||
|
@ -22,46 +28,43 @@ from .models import ShopCounter
|
|||
|
||||
@offlineshop_ext.get("/api/v1/currencies")
|
||||
async def api_list_currencies_available():
|
||||
return jsonify(list(currencies.keys()))
|
||||
return json.dumps(list(currencies.keys()))
|
||||
|
||||
|
||||
@offlineshop_ext.get("/api/v1/offlineshop")
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_shop_from_wallet():
|
||||
shop = await get_or_create_shop_by_wallet(g().wallet.id)
|
||||
# @api_check_wallet_key("invoice")
|
||||
async def api_shop_from_wallet(r: Request, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
shop = await get_or_create_shop_by_wallet(wallet.wallet.id)
|
||||
items = await get_items(shop.id)
|
||||
|
||||
try:
|
||||
return (
|
||||
{
|
||||
**shop._asdict(),
|
||||
**{
|
||||
"otp_key": shop.otp_key,
|
||||
"items": [item.values() for item in items],
|
||||
},
|
||||
},
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
return {
|
||||
**shop.dict(),
|
||||
**{
|
||||
"otp_key": shop.otp_key,
|
||||
"items": [item.values(r) for item in items],
|
||||
},
|
||||
}
|
||||
except LnurlInvalidUrl:
|
||||
return (
|
||||
{
|
||||
"message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor."
|
||||
},
|
||||
HTTPStatus.UPGRADE_REQUIRED,
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.UPGRADE_REQUIRED,
|
||||
detail="LNURLs need to be delivered over a publically accessible `https` domain or Tor.",
|
||||
)
|
||||
|
||||
|
||||
class CreateItemsData(BaseModel):
|
||||
name: str
|
||||
name: str
|
||||
description: str
|
||||
image: Optional[str]
|
||||
price: int
|
||||
unit: str
|
||||
image: Optional[str]
|
||||
price: int
|
||||
unit: str
|
||||
|
||||
|
||||
@offlineshop_ext.post("/api/v1/offlineshop/items")
|
||||
@offlineshop_ext.put("/api/v1/offlineshop/items/{item_id}")
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_add_or_update_item(data: CreateItemsData, item_id=None):
|
||||
shop = await get_or_create_shop_by_wallet(g().wallet.id)
|
||||
# @api_check_wallet_key("invoice")
|
||||
async def api_add_or_update_item(data: CreateItemsData, item_id=None, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
shop = await get_or_create_shop_by_wallet(wallet.wallet.id)
|
||||
if item_id == None:
|
||||
await add_item(
|
||||
shop.id,
|
||||
|
@ -71,7 +74,7 @@ async def api_add_or_update_item(data: CreateItemsData, item_id=None):
|
|||
data.price,
|
||||
data.unit,
|
||||
)
|
||||
return "", HTTPStatus.CREATED
|
||||
return HTMLResponse(status_code=HTTPStatus.CREATED)
|
||||
else:
|
||||
await update_item(
|
||||
shop.id,
|
||||
|
@ -82,36 +85,35 @@ async def api_add_or_update_item(data: CreateItemsData, item_id=None):
|
|||
data.price,
|
||||
data.unit,
|
||||
)
|
||||
return "", HTTPStatus.OK
|
||||
|
||||
|
||||
@offlineshop_ext.delete("/api/v1/offlineshop/items/{item_id}")
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_delete_item(item_id):
|
||||
shop = await get_or_create_shop_by_wallet(g().wallet.id)
|
||||
# @api_check_wallet_key("invoice")
|
||||
async def api_delete_item(item_id, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
shop = await get_or_create_shop_by_wallet(wallet.wallet.id)
|
||||
await delete_item_from_shop(shop.id, item_id)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
|
||||
class CreateMethodData(BaseModel):
|
||||
method: str
|
||||
method: str
|
||||
wordlist: Optional[str]
|
||||
|
||||
|
||||
@offlineshop_ext.put("/api/v1/offlineshop/method")
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_set_method(data: CreateMethodData):
|
||||
# @api_check_wallet_key("invoice")
|
||||
async def api_set_method(data: CreateMethodData, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
method = data.method
|
||||
|
||||
wordlist = data.wordlist.split("\n") if data.wordlist else None
|
||||
wordlist = [word.strip() for word in wordlist if word.strip()]
|
||||
|
||||
shop = await get_or_create_shop_by_wallet(g().wallet.id)
|
||||
shop = await get_or_create_shop_by_wallet(wallet.wallet.id)
|
||||
if not shop:
|
||||
return "", HTTPStatus.NOT_FOUND
|
||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
|
||||
|
||||
updated_shop = await set_method(shop.id, method, "\n".join(wordlist))
|
||||
if not updated_shop:
|
||||
return "", HTTPStatus.NOT_FOUND
|
||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
|
||||
|
||||
ShopCounter.reset(updated_shop)
|
||||
return "", HTTPStatus.OK
|
||||
|
|
|
@ -149,9 +149,9 @@ def url_for(
|
|||
url = f"{base}{endpoint}{url_params}"
|
||||
return url
|
||||
|
||||
def template_renderer() -> Jinja2Templates:
|
||||
def template_renderer(additional_folders: List = []) -> Jinja2Templates:
|
||||
t = Jinja2Templates(
|
||||
loader=jinja2.FileSystemLoader(["lnbits/templates", "lnbits/core/templates"]),
|
||||
loader=jinja2.FileSystemLoader(["lnbits/templates", "lnbits/core/templates", *additional_folders]),
|
||||
)
|
||||
t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE
|
||||
t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE
|
||||
|
|
|
@ -5,6 +5,7 @@ import typing
|
|||
|
||||
from starlette import templating
|
||||
from starlette.datastructures import QueryParams
|
||||
from starlette.requests import Request
|
||||
|
||||
from lnbits.requestvars import g
|
||||
|
||||
|
@ -22,8 +23,8 @@ class Jinja2Templates(templating.Jinja2Templates):
|
|||
def get_environment(self, loader: "jinja2.BaseLoader") -> "jinja2.Environment":
|
||||
@jinja2.contextfunction
|
||||
def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
|
||||
request = context["request"]
|
||||
return request.url_for(name, **path_params)
|
||||
request: Request = context["request"] # type: starlette.requests.Request
|
||||
return request.app.url_path_for(name, **path_params)
|
||||
|
||||
def url_params_update(init: QueryParams, **new: typing.Any) -> QueryParams:
|
||||
values = dict(init)
|
||||
|
|
Loading…
Reference in New Issue
Block a user