This commit is contained in:
benarc 2024-02-01 17:18:55 +00:00
parent 66d44f95fb
commit 2cac36be17
12 changed files with 272 additions and 311 deletions

View file

@ -8,9 +8,7 @@ from lnbits.tasks import catch_everything_and_restart
db = Database("ext_myextension")
myextension_ext: APIRouter = APIRouter(
prefix="/myextension", tags=["MyExtension"]
)
myextension_ext: APIRouter = APIRouter(prefix="/myextension", tags=["MyExtension"])
temp_static_files = [
{
@ -19,14 +17,17 @@ temp_static_files = [
}
]
def myextension_renderer():
return template_renderer(["myextension/templates"])
from .lnurl import *
from .tasks import wait_for_paid_invoices
from .views import *
from .views_api import *
def myextension_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))

51
crud.py
View file

@ -9,7 +9,10 @@ from fastapi import Request
from lnurl import encode as lnurl_encode
import shortuuid
async def create_myextension(wallet_id: str, data: CreateMyExtensionData, req: Request) -> MyExtension:
async def create_myextension(
wallet_id: str, data: CreateMyExtensionData, req: Request
) -> MyExtension:
myextension_id = urlsafe_short_hash()
await db.execute(
"""
@ -21,7 +24,7 @@ async def create_myextension(wallet_id: str, data: CreateMyExtensionData, req: R
wallet_id,
data.name,
data.lnurlpayamount,
data.lnurlwithdrawamount
data.lnurlwithdrawamount,
),
)
myextension = await get_myextension(myextension_id, req)
@ -29,25 +32,33 @@ async def create_myextension(wallet_id: str, data: CreateMyExtensionData, req: R
return myextension
async def get_myextension(myextension_id: str, req: Optional[Request] = None) -> Optional[MyExtension]:
async def get_myextension(
myextension_id: str, req: Optional[Request] = None
) -> Optional[MyExtension]:
logger.debug(myextension_id)
row = await db.fetchone("SELECT * FROM myextension.maintable WHERE id = ?", (myextension_id,))
row = await db.fetchone(
"SELECT * FROM myextension.maintable WHERE id = ?", (myextension_id,)
)
if not row:
return None
rowAmended = MyExtension(**row)
if req:
rowAmended.lnurlpay = lnurl_encode(
req.url_for("myextension.api_lnurl_pay",
myextension_id=row.id)._url
req.url_for("myextension.api_lnurl_pay", myextension_id=row.id)._url
)
rowAmended.lnurlwithdraw = lnurl_encode(
req.url_for("myextension.api_lnurl_withdraw",
req.url_for(
"myextension.api_lnurl_withdraw",
myextension_id=row.id,
tickerhash=shortuuid.uuid(name=rowAmended.id + str(rowAmended.ticker)))._url
tickerhash=shortuuid.uuid(name=rowAmended.id + str(rowAmended.ticker)),
)._url
)
return rowAmended
async def get_myextensions(wallet_ids: Union[str, List[str]], req: Optional[Request] = None) -> List[MyExtension]:
async def get_myextensions(
wallet_ids: Union[str, List[str]], req: Optional[Request] = None
) -> List[MyExtension]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
@ -59,25 +70,33 @@ async def get_myextensions(wallet_ids: Union[str, List[str]], req: Optional[Requ
if req:
for row in tempRows:
row.lnurlpay = lnurl_encode(
req.url_for("myextension.api_lnurl_pay",
myextension_id=row.id)._url
req.url_for("myextension.api_lnurl_pay", myextension_id=row.id)._url
)
row.lnurlwithdraw = lnurl_encode(
req.url_for("myextension.api_lnurl_withdraw",
req.url_for(
"myextension.api_lnurl_withdraw",
myextension_id=row.id,
tickerhash=shortuuid.uuid(name=row.id + str(row.ticker)))._url
tickerhash=shortuuid.uuid(name=row.id + str(row.ticker)),
)._url
)
return tempRows
async def update_myextension(myextension_id: str, req: Optional[Request] = None, **kwargs) -> MyExtension:
async def update_myextension(
myextension_id: str, req: Optional[Request] = None, **kwargs
) -> MyExtension:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
logger.debug(kwargs.items())
await db.execute(
f"UPDATE myextension.maintable SET {q} WHERE id = ?", (*kwargs.values(), myextension_id)
f"UPDATE myextension.maintable SET {q} WHERE id = ?",
(*kwargs.values(), myextension_id),
)
myextension = await get_myextension(myextension_id, req)
assert myextension, "Newly updated myextension couldn't be retrieved"
return myextension
async def delete_myextension(myextension_id: str) -> None:
await db.execute("DELETE FROM myextension.maintable WHERE id = ?", (myextension_id,))
await db.execute(
"DELETE FROM myextension.maintable WHERE id = ?", (myextension_id,)
)

View file

@ -19,6 +19,7 @@ import shortuuid
#################################################
#################################################
@myextension_ext.get(
"/api/v1/lnurl/pay/{myextension_id}",
status_code=HTTPStatus.OK,
@ -32,13 +33,18 @@ async def api_lnurl_pay(
if not myextension:
return {"status": "ERROR", "reason": "No myextension found"}
return {
"callback": str(request.url_for("myextension.api_lnurl_pay_callback", myextension_id=myextension_id)),
"callback": str(
request.url_for(
"myextension.api_lnurl_pay_callback", myextension_id=myextension_id
)
),
"maxSendable": myextension.lnurlpayamount * 1000,
"minSendable": myextension.lnurlpayamount * 1000,
"metadata":"[[\"text/plain\", \"" + myextension.name + "\"]]",
"tag": "payRequest"
"metadata": '[["text/plain", "' + myextension.name + '"]]',
"tag": "payRequest",
}
@myextension_ext.get(
"/api/v1/lnurl/pay/cb/{myextension_id}",
status_code=HTTPStatus.OK,
@ -58,7 +64,7 @@ async def api_lnurl_pay_cb(
wallet_id=myextension.wallet,
amount=int(amount / 1000),
memo=myextension.name,
unhashed_description=f"[[\"text/plain\", \"{myextension.name}\"]]".encode(),
unhashed_description=f'[["text/plain", "{myextension.name}"]]'.encode(),
extra={
"tag": "MyExtension",
"myextensionId": myextension_id,
@ -68,12 +74,10 @@ async def api_lnurl_pay_cb(
return {
"pr": payment_request,
"routes": [],
"successAction": {
"tag": "message",
"message": f"Paid {myextension.name}"
}
"successAction": {"tag": "message", "message": f"Paid {myextension.name}"},
}
#################################################
######## A very simple LNURLwithdraw ############
# https://github.com/lnurl/luds/blob/luds/03.md #
@ -100,13 +104,18 @@ async def api_lnurl_withdraw(
return {
"tag": "withdrawRequest",
"callback": str(request.url_for("myextension.api_lnurl_withdraw_callback", myextension_id=myextension_id)),
"callback": str(
request.url_for(
"myextension.api_lnurl_withdraw_callback", myextension_id=myextension_id
)
),
"k1": k1,
"defaultDescription": myextension.name,
"maxWithdrawable": myextension.lnurlwithdrawamount * 1000,
"minWithdrawable": myextension.lnurlwithdrawamount * 1000
"minWithdrawable": myextension.lnurlwithdrawamount * 1000,
}
@myextension_ext.get(
"/api/v1/lnurl/withdraw/cb/{myextension_id}",
status_code=HTTPStatus.OK,
@ -130,7 +139,9 @@ async def api_lnurl_withdraw_cb(
if k1Check != k1:
return {"status": "ERROR", "reason": "Wrong k1 check provided"}
await update_myextension(myextension_id=myextension_id, ticker=myextension.ticker + 1)
await update_myextension(
myextension_id=myextension_id, ticker=myextension.ticker + 1
)
logger.debug(myextension.wallet)
logger.debug(pr)
logger.debug(int(myextension.lnurlwithdrawamount * 1000))
@ -138,6 +149,10 @@ async def api_lnurl_withdraw_cb(
wallet_id=myextension.wallet,
payment_request=pr,
max_sat=int(myextension.lnurlwithdrawamount * 1000),
extra={"tag": "MyExtension", "myextensionId": myextension_id, "lnurlwithdraw": True}
extra={
"tag": "MyExtension",
"myextensionId": myextension_id,
"lnurlwithdraw": True,
},
)
return {"status": "OK"}

View file

@ -1,5 +1,6 @@
# The migration file is like a blockchain, never edit only add!
async def m001_initial(db):
"""
Initial templates table.
@ -19,8 +20,10 @@ async def m001_initial(db):
"""
)
# Here we add another field to the database
async def m002_addtip_wallet(db):
"""
Add total to templates table

View file

@ -9,6 +9,7 @@ from fastapi import Request
from lnbits.lnurl import encode as lnurl_encode
from urllib.parse import urlparse
class CreateMyExtensionData(BaseModel):
wallet: Optional[str]
name: Optional[str]
@ -17,6 +18,7 @@ class CreateMyExtensionData(BaseModel):
lnurlwithdrawamount: Optional[int]
ticker: Optional[int]
class MyExtension(BaseModel):
id: str
wallet: Optional[str]

View file

@ -29,6 +29,7 @@ async def wait_for_paid_invoices():
# do somethhing when an invoice related top this extension is paid
async def on_invoice_paid(payment: Payment) -> None:
logger.debug("payment received for myextension extension")
if payment.extra.get("tag") != "MyExtension":
@ -42,9 +43,7 @@ async def on_invoice_paid(payment: Payment) -> None:
total = myextension.total - payment.amount
else:
total = myextension.total + payment.amount
data_to_update = {
"total": total
}
data_to_update = {"total": total}
await update_myextension(myextension_id=myextension_id, **data_to_update)
@ -55,7 +54,7 @@ async def on_invoice_paid(payment: Payment) -> None:
"name": myextension.name,
"amount": payment.amount,
"fee": payment.fee,
"checking_id": payment.checking_id
"checking_id": payment.checking_id,
}
await websocketUpdater(myextension_id, str(some_payment_data))

View file

@ -1,8 +1,3 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item group="extras" icon="swap_vertical_circle" label="API info" :content-inset-level="0.5">
<q-btn flat label="Swagger API" type="a" href="../docs#/MyExtension"></q-btn>
</q-expansion-item>

View file

@ -4,24 +4,10 @@
<p>
Some more info about my excellent extension.
</p>
<small
>Created by
<a
class="text-secondary"
href="https://github.com/benarc"
target="_blank"
>Ben Arc</a
>.</small
>
<small
>Repo
<a
class="text-secondary"
href="https://github.com/lnbits/myextension"
target="_blank"
>MyExtension</a
>.</small
>
<small>Created by
<a class="text-secondary" href="https://github.com/benarc" target="_blank">Ben Arc</a>.</small>
<small>Repo
<a class="text-secondary" href="https://github.com/lnbits/myextension" target="_blank">MyExtension</a>.</small>
</q-card-section>
</q-card>
</q-expansion-item>

View file

@ -4,9 +4,7 @@
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="primary" @click="formDialog.show = true"
>New MyExtension</q-btn
>
<q-btn unelevated color="primary" @click="formDialog.show = true">New MyExtension</q-btn>
</q-card-section>
</q-card>
@ -20,14 +18,8 @@
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:data="temps"
row-key="id"
:columns="tempsTable.columns"
:pagination.sync="tempsTable.pagination"
>
<q-table dense flat :data="temps" row-key="id" :columns="tempsTable.columns"
:pagination.sync="tempsTable.pagination">
<myextension v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
@ -38,59 +30,26 @@
<template v-slot:body="props">
<q-tr :props="props">
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<div v-if="col.field == 'total'">${ col.value / 1000} sats</div>
<div v-else>${ col.value }</div>
</q-td>
<q-td auto-width>
<q-btn
unelevated
dense
size="sm"
icon="qr_code"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
class="q-mr-sm"
@click="openUrlDialog(props.row.id)"
></q-btn></q-td>
<q-btn unelevated dense size="sm" icon="qr_code" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
class="q-mr-sm" @click="openUrlDialog(props.row.id)"></q-btn></q-td>
<q-td auto-width>
<q-btn
unelevated
dense
size="sm"
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.myextension"
target="_blank"
><q-tooltip>Open public page</q-tooltip></q-btn
></q-td>
<q-btn unelevated dense size="sm" icon="launch" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a" :href="props.row.myextension" target="_blank"><q-tooltip>Open public
page</q-tooltip></q-btn></q-td>
<q-td>
<q-btn
flat
dense
size="xs"
@click="updateMyExtensionForm(props.row.id)"
icon="edit"
color="light-blue"
>
<q-btn flat dense size="xs" @click="updateMyExtensionForm(props.row.id)" icon="edit" color="light-blue">
<q-tooltip> Edit copilot </q-tooltip>
</q-btn>
</q-td>
<q-td>
<q-btn
flat
dense
size="xs"
@click="deleteMyExtension(props.row.id)"
icon="cancel"
color="pink"
>
<q-btn flat dense size="xs" @click="deleteMyExtension(props.row.id)" icon="cancel" color="pink">
<q-tooltip> Delete copilot </q-tooltip>
</q-btn>
</q-td>
@ -107,7 +66,8 @@
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} MyExtension extension</h6>
<p>Simple extension you can use as a base for your own extension. <br/> Includes very simple LNURL-pay and LNURL-withdraw example.</p>
<p>Simple extension you can use as a base for your own extension. <br /> Includes very simple LNURL-pay and
LNURL-withdraw example.</p>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
@ -123,54 +83,20 @@
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendMyExtensionData" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialog.data.name"
label="Name"
placeholder="Name for your record"
></q-input>
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
></q-select>
<q-input
filled
dense
type="number"
v-model.trim="formDialog.data.lnurlwithdrawamount"
label="LNURL-withdraw amount"
></q-input>
<q-input
filled
dense
type="number"
v-model.trim="formDialog.data.lnurlpayamount"
label="LNURL-pay amount"
></q-input>
<q-input filled dense v-model.trim="formDialog.data.name" label="Name"
placeholder="Name for your record"></q-input>
<q-select filled dense emit-value v-model="formDialog.data.wallet" :options="g.user.walletOptions"
label="Wallet *"></q-select>
<q-input filled dense type="number" v-model.trim="formDialog.data.lnurlwithdrawamount"
label="LNURL-withdraw amount"></q-input>
<q-input filled dense type="number" v-model.trim="formDialog.data.lnurlpayamount"
label="LNURL-pay amount"></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="formDialog.data.id"
unelevated
color="primary"
type="submit"
>Update MyExtension</q-btn
>
<q-btn
v-else
unelevated
color="primary"
<q-btn v-if="formDialog.data.id" unelevated color="primary" type="submit">Update MyExtension</q-btn>
<q-btn v-else unelevated color="primary"
:disable="formDialog.data.name == null || formDialog.data.wallet == null || formDialog.data.lnurlwithdrawamount == null || formDialog.data.lnurlpayamount == null"
type="submit"
>Create MyExtension</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
type="submit">Create MyExtension</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-form>
</q-card>
@ -181,9 +107,7 @@
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<lnbits-qrcode
:value="qrValue"
></lnbits-qrcode>
<lnbits-qrcode :value="qrValue"></lnbits-qrcode>
</q-responsive>
<center><q-btn label="copy" @click="copyText(qrValue)"></q-btn>
</center>
@ -202,7 +126,8 @@
<div class="col q-pl-md">
<q-input filled bottom-slots dense v-model="invoiceAmount">
<template v-slot:append>
<q-btn round @click="createInvoice(urlDialog.data.wallet, urlDialog.data.id)" color="primary" flat icon="add_circle" />
<q-btn round @click="createInvoice(urlDialog.data.wallet, urlDialog.data.id)" color="primary" flat
icon="add_circle" />
</template>
<template v-slot:hint>
Create an invoice

View file

@ -6,18 +6,12 @@
<div class="text-center">
<a class="text-secondary" href="lightning:{{ lnurl }}">
<q-responsive :ratio="1" class="q-mx-md">
<qrcode
:value="qrValue"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
<qrcode :value="qrValue" :options="{width: 800}" class="rounded-borders"></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg q-gutter-sm">
<q-btn outline color="grey" @click="copyText(qrValue)"
>Copy LNURL </q-btn
>
<q-btn outline color="grey" @click="copyText(qrValue)">Copy LNURL </q-btn>
</div>
</q-card-section>

View file

@ -22,6 +22,7 @@ temps = Jinja2Templates(directory="temps")
# Backend admin page
@myextension_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return myextension_renderer().TemplateResponse(
@ -31,6 +32,7 @@ async def index(request: Request, user: User = Depends(check_user_exists)):
# Frontend shareable page
@myextension_ext.get("/{myextension_id}")
async def myextension(request: Request, myextension_id):
myextension = await get_myextension(myextension_id, request)
@ -52,6 +54,7 @@ async def myextension(request: Request, myextension_id):
# Manifest for public page, customise or remove manifest completely
@myextension_ext.get("/manifest/{myextension_id}.webmanifest")
async def manifest(myextension_id: str):
myextension = await get_myextension(myextension_id)

View file

@ -25,7 +25,7 @@ from .crud import (
update_myextension,
delete_myextension,
get_myextension,
get_myextensions
get_myextensions,
)
from .models import CreateMyExtensionData
@ -36,25 +36,29 @@ from .models import CreateMyExtensionData
## Get all the records belonging to the user
@myextension_ext.get("/api/v1/temps", status_code=HTTPStatus.OK)
async def api_myextensions(
req: Request, all_wallets:
bool = Query(False),
wallet: WalletTypeInfo = Depends(get_key_type)
req: Request,
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 [myextension.dict() for myextension in await get_myextensions(wallet_ids, req)]
return [
myextension.dict() for myextension in await get_myextensions(wallet_ids, req)
]
## Get a single record
@myextension_ext.get("/api/v1/temps/{myextension_id}", status_code=HTTPStatus.OK)
async def api_myextension(
req: Request,
myextension_id: str,
WalletTypeInfo = Depends(get_key_type)):
req: Request, myextension_id: str, WalletTypeInfo=Depends(get_key_type)
):
myextension = await get_myextension(myextension_id, req)
if not myextension:
raise HTTPException(
@ -62,8 +66,10 @@ async def api_myextension(
)
return myextension.dict()
## update a record
@myextension_ext.put("/api/v1/temps/{myextension_id}")
async def api_myextension_update(
req: Request,
@ -79,25 +85,33 @@ async def api_myextension_update(
assert myextension, "MyExtension couldn't be retrieved"
if wallet.wallet.id != myextension.wallet:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension.")
myextension = await update_myextension(myextension_id=myextension_id, **data.dict(), req=req)
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension."
)
myextension = await update_myextension(
myextension_id=myextension_id, **data.dict(), req=req
)
return myextension.dict()
## Create a new record
@myextension_ext.post("/api/v1/temps", status_code=HTTPStatus.CREATED)
async def api_myextension_create(
req: Request,
data: CreateMyExtensionData,
wallet: WalletTypeInfo = Depends(get_key_type)
wallet: WalletTypeInfo = Depends(get_key_type),
):
myextension = await create_myextension(wallet_id=wallet.wallet.id, data=data, req=req)
myextension = await create_myextension(
wallet_id=wallet.wallet.id, data=data, req=req
)
return myextension.dict()
## Delete a record
@myextension_ext.delete("/api/v1/temps/{myextension_id}")
async def api_myextension_delete(
myextension_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
@ -110,7 +124,9 @@ async def api_myextension_delete(
)
if myextension.wallet != wallet.wallet.id:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension.")
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension."
)
await delete_myextension(myextension_id)
return "", HTTPStatus.NO_CONTENT
@ -120,7 +136,10 @@ async def api_myextension_delete(
## This endpoint creates a payment
@myextension_ext.post("/api/v1/temps/payment/{myextension_id}", status_code=HTTPStatus.CREATED)
@myextension_ext.post(
"/api/v1/temps/payment/{myextension_id}", status_code=HTTPStatus.CREATED
)
async def api_tpos_create_invoice(
myextension_id: str, amount: int = Query(..., ge=1), memo: str = ""
) -> dict: