another v1 fixup + moved lnurl stuff to models

This commit is contained in:
Arc 2024-11-15 00:06:28 +00:00
parent 42b5edaf5d
commit 8b2b36a4f9
11 changed files with 82 additions and 104 deletions

View file

@ -1,6 +1,7 @@
import asyncio import asyncio
from fastapi import APIRouter from fastapi import APIRouter
from lnbits.tasks import create_permanent_unique_task
from loguru import logger from loguru import logger
from .crud import db from .crud import db
@ -39,8 +40,6 @@ def myextension_stop():
def myextension_start(): def myextension_start():
from lnbits.tasks import create_permanent_unique_task
task = create_permanent_unique_task("ext_myextension", wait_for_paid_invoices) task = create_permanent_unique_task("ext_myextension", wait_for_paid_invoices)
scheduled_tasks.append(task) scheduled_tasks.append(task)

View file

@ -2,7 +2,7 @@
"name": "MyExtension", "name": "MyExtension",
"short_description": "Minimal extension to build on", "short_description": "Minimal extension to build on",
"tile": "/myextension/static/image/myextension.png", "tile": "/myextension/static/image/myextension.png",
"min_lnbits_version": "0.12.5", "min_lnbits_version": "1.0.0",
"contributors": [ "contributors": [
{ {
"name": "Alan Bits", "name": "Alan Bits",

66
crud.py
View file

@ -1,77 +1,41 @@
from typing import Optional, Union from typing import List, Optional, Union
from lnbits.db import Database from lnbits.db import Database
from lnbits.helpers import insert_query, update_query
from .models import MyExtension from .models import MyExtension
db = Database("ext_myextension") db = Database("ext_myextension")
table_name = "myextension.maintable"
async def create_myextension(data: MyExtension) -> MyExtension: async def create_myextension(data: MyExtension) -> MyExtension:
await db.execute( await db.insert("myextension.maintable", data)
insert_query(table_name, data),
(*data.dict().values(),),
)
return data return data
# this is how we used to do it
# myextension_id = urlsafe_short_hash()
# await db.execute(
# """
# INSERT INTO myextension.maintable
# (id, wallet, name, lnurlpayamount, lnurlwithdrawamount)
# VALUES (?, ?, ?, ?, ?)
# """,
# (
# myextension_id,
# wallet_id,
# data.name,
# data.lnurlpayamount,
# data.lnurlwithdrawamount,
# ),
# )
# myextension = await get_myextension(myextension_id)
# assert myextension, "Newly created table couldn't be retrieved"
async def get_myextension(myextension_id: str) -> Optional[MyExtension]: async def get_myextension(myextension_id: str) -> Optional[MyExtension]:
row = await db.fetchone( return await db.fetchone(
f"SELECT * FROM {table_name} WHERE id = ?", (myextension_id,) "SELECT * FROM myextension.maintable WHERE id = :id",
{"id": myextension_id},
MyExtension,
) )
return MyExtension(**row) if row else None
async def get_myextensions(wallet_ids: Union[str, list[str]]) -> list[MyExtension]: async def get_myextensions(wallet_ids: Union[str, List[str]]) -> List[MyExtension]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
q = ",".join([f"'{w}'" for w in wallet_ids])
q = ",".join(["?"] * len(wallet_ids)) return await db.fetchall(
rows = await db.fetchall( f"SELECT * FROM myextension.maintable WHERE wallet IN ({q}) ORDER BY id",
f"SELECT * FROM {table_name} WHERE wallet IN ({q})", (*wallet_ids,) model=MyExtension,
) )
return [MyExtension(**row) for row in rows]
async def update_myextension(data: MyExtension) -> MyExtension: async def update_myextension(data: MyExtension) -> MyExtension:
await db.execute( await db.update("myextension.maintable", data)
update_query(table_name, data),
(
*data.dict().values(),
data.id,
),
)
return data return data
# this is how we used to do it
# q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
# await db.execute(
# f"UPDATE myextension.maintable SET {q} WHERE id = ?",
# (*kwargs.values(), myextension_id),
# )
async def delete_myextension(myextension_id: str) -> None: async def delete_myextension(myextension_id: str) -> None:
await db.execute(f"DELETE FROM {table_name} WHERE id = ?", (myextension_id,)) await db.execute(
"DELETE FROM myextension.maintable WHERE id = :id", {"id": myextension_id}
)

View file

@ -15,9 +15,7 @@ async def m001_initial(db):
name TEXT NOT NULL, name TEXT NOT NULL,
total INTEGER DEFAULT 0, total INTEGER DEFAULT 0,
lnurlpayamount INTEGER DEFAULT 0, lnurlpayamount INTEGER DEFAULT 0,
lnurlwithdrawamount INTEGER DEFAULT 0, lnurlwithdrawamount INTEGER DEFAULT 0
lnurlwithdraw TEXT,
lnurlpay TEXT
); );
""" """
) )

View file

@ -2,6 +2,8 @@
from typing import Optional from typing import Optional
from fastapi import Request
from lnurl.core import encode as lnurl_encode
from pydantic import BaseModel from pydantic import BaseModel
@ -20,5 +22,19 @@ class MyExtension(BaseModel):
name: str name: str
lnurlwithdrawamount: int lnurlwithdrawamount: int
total: int total: int
lnurlpay: Optional[str]
lnurlwithdraw: Optional[str] def lnurlpay(self, req: Request) -> str:
url = req.url_for("myextension.api_lnurl_pay", myextension_id=self.id)
url_str = str(url)
if url.netloc.endswith(".onion"):
url_str = url_str.replace("https://", "http://")
return lnurl_encode(url_str)
def lnurlwithdraw(self, req: Request) -> str:
url = req.url_for("myextension.api_lnurl_withdraw", myextension_id=self.id)
url_str = str(url)
if url.netloc.endswith(".onion"):
url_str = url_str.replace("https://", "http://")
return lnurl_encode(url_str)

View file

@ -2,7 +2,6 @@ import asyncio
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.core.services import websocket_updater from lnbits.core.services import websocket_updater
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from .crud import get_myextension, update_myextension from .crud import get_myextension, update_myextension
@ -16,7 +15,7 @@ from .crud import get_myextension, update_myextension
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue, get_current_extension_name()) register_invoice_listener(invoice_queue, "ext_myextension")
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()
await on_invoice_paid(payment) await on_invoice_paid(payment)

View file

@ -27,10 +27,10 @@
<q-table <q-table
dense dense
flat flat
:data="myex" :rows="myex"
row-key="id" row-key="id"
:columns="myexTable.columns" :columns="myexTable.columns"
:pagination.sync="myexTable.pagination" v-model:pagination="myexTable.pagination"
> >
<myextension v-slot:header="props"> <myextension v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
@ -250,7 +250,7 @@
obj.myextension = ['/myextension/', obj.id].join('') obj.myextension = ['/myextension/', obj.id].join('')
return obj return obj
} }
new Vue({ window.app = Vue.createApp({
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
delimiters: ['${', '}'], delimiters: ['${', '}'],

View file

@ -10,11 +10,11 @@
<div class="text-center"> <div class="text-center">
<a class="text-secondary" href="lightning:{{ lnurl }}"> <a class="text-secondary" href="lightning:{{ lnurl }}">
<q-responsive :ratio="1" class="q-mx-md"> <q-responsive :ratio="1" class="q-mx-md">
<qrcode <lnbits-qrcode
:value="qrValue" :value="qrValue"
:options="{width: 800}" :options="{width: 800}"
class="rounded-borders" class="rounded-borders"
></qrcode> ></lnbits-qrcode>
</q-responsive> </q-responsive>
</a> </a>
</div> </div>
@ -49,9 +49,7 @@
</div> </div>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) window.app = Vue.createApp({
new Vue({
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {

View file

@ -28,7 +28,7 @@ def myextension_renderer():
@myextension_generic_router.get("/", response_class=HTMLResponse) @myextension_generic_router.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)): async def index(request: Request, user: User = Depends(check_user_exists)):
return myextension_renderer().TemplateResponse( return myextension_renderer().TemplateResponse(
"myextension/index.html", {"request": request, "user": user.dict()} "myextension/index.html", {"request": request, "user": user.json()}
) )
@ -47,7 +47,7 @@ async def myextension(request: Request, myextension_id):
{ {
"request": request, "request": request,
"myextension_id": myextension_id, "myextension_id": myextension_id,
"lnurlpay": myextension.lnurlpay, "lnurlpay": myextension.lnurlpay(request),
"web_manifest": f"/myextension/manifest/{myextension_id}.webmanifest", "web_manifest": f"/myextension/manifest/{myextension_id}.webmanifest",
}, },
) )

View file

@ -9,8 +9,6 @@ from lnbits.decorators import (
require_admin_key, require_admin_key,
require_invoice_key, require_invoice_key,
) )
from lnbits.helpers import urlsafe_short_hash
from lnurl import encode as lnurl_encode
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from .crud import ( from .crud import (
@ -29,11 +27,15 @@ myextension_api_router = APIRouter()
##### ADD YOUR API ENDPOINTS HERE ##### ##### ADD YOUR API ENDPOINTS HERE #####
####################################### #######################################
# Note: we add the lnurl params to returns so the links
# are generated in the MyExtension model in models.py
## Get all the records belonging to the user ## Get all the records belonging to the user
@myextension_api_router.get("/api/v1/myex", status_code=HTTPStatus.OK) @myextension_api_router.get("/api/v1/myex", status_code=HTTPStatus.OK)
async def api_myextensions( async def api_myextensions(
req: Request,
all_wallets: bool = Query(False), all_wallets: bool = Query(False),
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(get_key_type),
): ):
@ -41,7 +43,14 @@ async def api_myextensions(
if all_wallets: if all_wallets:
user = await get_user(wallet.wallet.user) user = await get_user(wallet.wallet.user)
wallet_ids = user.wallet_ids if user else [] wallet_ids = user.wallet_ids if user else []
return [myextension.dict() for myextension in await get_myextensions(wallet_ids)] return [
{
**myextension.dict(),
"lnurlpay": myextension.lnurlpay(req),
"lnurlwithdraw": myextension.lnurlwithdraw(req),
}
for myextension in await get_myextensions(wallet_ids)
]
## Get a single record ## Get a single record
@ -52,13 +61,17 @@ async def api_myextensions(
status_code=HTTPStatus.OK, status_code=HTTPStatus.OK,
dependencies=[Depends(require_invoice_key)], dependencies=[Depends(require_invoice_key)],
) )
async def api_myextension(myextension_id: str): async def api_myextension(myextension_id: str, req: Request):
myextension = await get_myextension(myextension_id) myextension = await get_myextension(myextension_id)
if not myextension: if not myextension:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist." status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
) )
return myextension.dict() return {
**myextension.dict(),
"lnurlpay": myextension.lnurlpay(req),
"lnurlwithdraw": myextension.lnurlwithdraw(req),
}
## update a record ## update a record
@ -67,13 +80,11 @@ async def api_myextension(myextension_id: str):
@myextension_api_router.put("/api/v1/myex/{myextension_id}") @myextension_api_router.put("/api/v1/myex/{myextension_id}")
async def api_myextension_update( async def api_myextension_update(
data: CreateMyExtensionData, data: CreateMyExtensionData,
req: Request,
myextension_id: str, myextension_id: str,
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(get_key_type),
) -> MyExtension: ) -> MyExtension:
if not myextension_id:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
)
myextension = await get_myextension(myextension_id) myextension = await get_myextension(myextension_id)
assert myextension, "MyExtension couldn't be retrieved" assert myextension, "MyExtension couldn't be retrieved"
@ -85,7 +96,12 @@ async def api_myextension_update(
for key, value in data.dict().items(): for key, value in data.dict().items():
setattr(myextension, key, value) setattr(myextension, key, value)
return await update_myextension(myextension) myextension = await update_myextension(data)
return {
**myextension.dict(),
"lnurlpay": myextension.lnurlpay(req),
"lnurlwithdraw": myextension.lnurlwithdraw(req),
}
## Create a new record ## Create a new record
@ -93,29 +109,17 @@ async def api_myextension_update(
@myextension_api_router.post("/api/v1/myex", status_code=HTTPStatus.CREATED) @myextension_api_router.post("/api/v1/myex", status_code=HTTPStatus.CREATED)
async def api_myextension_create( async def api_myextension_create(
request: Request,
data: CreateMyExtensionData, data: CreateMyExtensionData,
req: Request,
key_type: WalletTypeInfo = Depends(require_admin_key), key_type: WalletTypeInfo = Depends(require_admin_key),
) -> MyExtension: ) -> MyExtension:
myextension_id = urlsafe_short_hash()
lnurlpay = lnurl_encode(
str(request.url_for("myextension.api_lnurl_pay", myextension_id=myextension_id))
)
lnurlwithdraw = lnurl_encode(
str(
request.url_for(
"myextension.api_lnurl_withdraw", myextension_id=myextension_id
)
)
)
data.wallet = data.wallet or key_type.wallet.id data.wallet = data.wallet or key_type.wallet.id
myext = MyExtension( myextension = create_myextension(data)
id=myextension_id, return {
lnurlpay=lnurlpay, **myextension.dict(),
lnurlwithdraw=lnurlwithdraw, "lnurlpay": myextension.lnurlpay(req),
**data.dict(), "lnurlwithdraw": myextension.lnurlwithdraw(req),
) }
return await create_myextension(myext)
## Delete a record ## Delete a record
@ -163,7 +167,7 @@ async def api_myextension_create_invoice(
# so tasks.py can grab the payment once its paid # so tasks.py can grab the payment once its paid
try: try:
payment_hash, payment_request = await create_invoice( payment = await create_invoice(
wallet_id=myextension.wallet, wallet_id=myextension.wallet,
amount=amount, amount=amount,
memo=f"{memo} to {myextension.name}" if memo else f"{myextension.name}", memo=f"{memo} to {myextension.name}" if memo else f"{myextension.name}",
@ -177,4 +181,4 @@ async def api_myextension_create_invoice(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc) status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
) from exc ) from exc
return {"payment_hash": payment_hash, "payment_request": payment_request} return {"payment_hash": payment.payment_hash, "payment_request": payment.bolt11}

View file

@ -61,7 +61,7 @@ async def api_lnurl_pay_cb(
if not myextension: if not myextension:
return {"status": "ERROR", "reason": "No myextension found"} return {"status": "ERROR", "reason": "No myextension found"}
_, payment_request = await create_invoice( payment = await create_invoice(
wallet_id=myextension.wallet, wallet_id=myextension.wallet,
amount=int(amount / 1000), amount=int(amount / 1000),
memo=myextension.name, memo=myextension.name,
@ -73,7 +73,7 @@ async def api_lnurl_pay_cb(
}, },
) )
return { return {
"pr": payment_request, "pr": payment.bolt11,
"routes": [], "routes": [],
"successAction": {"tag": "message", "message": f"Paid {myextension.name}"}, "successAction": {"tag": "message", "message": f"Paid {myextension.name}"},
} }