Added lnurl helper, and I hate mypy

This commit is contained in:
Arc 2024-11-18 20:49:24 +00:00
parent a8e05612da
commit 5a2ba743cf
7 changed files with 275 additions and 286 deletions

View file

@ -1,6 +1,9 @@
# Description: This file contains the CRUD operations for talking to the database.
from typing import List, Optional, Union from typing import List, Optional, Union
from lnbits.db import Database from lnbits.db import Database
from loguru import logger
from .models import MyExtension from .models import MyExtension
@ -24,6 +27,7 @@ async def get_myextensions(wallet_ids: Union[str, List[str]]) -> List[MyExtensio
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([f"'{w}'" for w in wallet_ids])
logger.debug(q)
return await db.fetchall( return await db.fetchall(
f"SELECT * FROM myextension.maintable WHERE wallet IN ({q}) ORDER BY id", f"SELECT * FROM myextension.maintable WHERE wallet IN ({q}) ORDER BY id",
model=MyExtension, model=MyExtension,

17
helpers.py Normal file
View file

@ -0,0 +1,17 @@
# Description: A place for helper functions.
from fastapi import Request
from lnurl.core import encode as lnurl_encode
# The lnurler function is used to generate the lnurlpay and lnurlwithdraw links
# from the lnurl api endpoints in views_lnurl.py.
# It needs the Request object to know the url of the LNbits.
# Lnurler is used in views_api.py
def lnurler(myex_id: str, route_name: str, req: Request) -> str:
url = req.url_for(route_name, myextension_id=myex_id)
url_str = str(url)
if url.netloc.endswith(".onion"):
url_str = url_str.replace("https://", "http://")
return str(lnurl_encode(url_str))

View file

@ -1,9 +1,5 @@
# Data models for your extension # Description: Pydantic data models dictate what is passed between frontend and backend.
from typing import Any, Dict, Optional
from fastapi import Request
from lnurl.core import encode as lnurl_encode
from pydantic import BaseModel from pydantic import BaseModel
@ -11,7 +7,6 @@ class CreateMyExtensionData(BaseModel):
name: str name: str
lnurlpayamount: int lnurlpayamount: int
lnurlwithdrawamount: int lnurlwithdrawamount: int
wallet: Optional[str] = None
total: int = 0 total: int = 0
@ -22,32 +17,5 @@ class MyExtension(BaseModel):
lnurlwithdrawamount: int lnurlwithdrawamount: int
wallet: str wallet: str
total: int total: int
lnurlpay: str
# Below is only needed if you want to add extra calculated fields to the model, lnurlwithdraw: str
# like getting the links for lnurlpay and lnurlwithdraw fields in this case.
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)
def serialize_with_extra_fields(self, req: Request) -> Dict[str, Any]:
"""Serialize the model and add extra fields."""
base_dict = self.dict()
base_dict.update(
{
"lnurlpay": self.lnurlpay(req),
"lnurlwithdraw": self.lnurlwithdraw(req),
}
)
return base_dict

View file

@ -1,14 +1,3 @@
///////////////////////////////////////////////////
//////////an object we can update with data////////
///////////////////////////////////////////////////
const mapMyExtension = obj => {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.myextension = ['/myextension/', obj.id].join('')
return obj
}
window.app = Vue.createApp({ window.app = Vue.createApp({
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
@ -56,26 +45,27 @@ window.app = Vue.createApp({
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
methods: { methods: {
closeFormDialog() { async closeFormDialog() {
this.formDialog.show = false this.formDialog.show = false
this.formDialog.data = {} this.formDialog.data = {}
}, },
getMyExtensions: function () { async getMyExtensions() {
var self = this await LNbits.api
LNbits.api
.request( .request(
'GET', 'GET',
'/myextension/api/v1/myex?all_wallets=true', '/myextension/api/v1/myex',
this.g.user.wallets[0].inkey this.g.user.wallets[0].inkey
) )
.then(function (response) { .then(response => {
self.myex = response.data.map(function (obj) { console.log(response.data)
return mapMyExtension(obj) this.myex = response.data
}) })
.catch(error => {
console.error('Error fetching data:', error)
}) })
}
}, },
sendMyExtensionData() { async sendMyExtensionData() {
const data = { const data = {
name: this.formDialog.data.name, name: this.formDialog.data.name,
lnurlwithdrawamount: this.formDialog.data.lnurlwithdrawamount, lnurlwithdrawamount: this.formDialog.data.lnurlwithdrawamount,
@ -86,14 +76,13 @@ window.app = Vue.createApp({
}) })
if (this.formDialog.data.id) { if (this.formDialog.data.id) {
data.id = this.formDialog.data.id data.id = this.formDialog.data.id
data.wallet = wallet.id
data.total = this.formDialog.data.total data.total = this.formDialog.data.total
this.updateMyExtension(wallet, data) await this.updateMyExtension(wallet, data)
} else { } else {
this.createMyExtension(wallet, data) await this.createMyExtension(wallet, data)
} }
}, },
updateMyExtensionForm(tempId) { async updateMyExtensionForm(tempId) {
const myextension = _.findWhere(this.myex, {id: tempId}) const myextension = _.findWhere(this.myex, {id: tempId})
this.formDialog.data = { this.formDialog.data = {
...myextension ...myextension
@ -106,19 +95,19 @@ window.app = Vue.createApp({
} }
this.formDialog.show = true this.formDialog.show = true
}, },
createMyExtension(wallet, data) { async createMyExtension(wallet, data) {
LNbits.api await LNbits.api
.request('POST', '/myextension/api/v1/myex', wallet.adminkey, data) .request('POST', '/myextension/api/v1/myex', wallet.adminkey, data)
.then(response => { .then(response => {
this.myex.push(mapMyExtension(response.data)) this.myex.push(response.data)
this.closeFormDialog() this.closeFormDialog()
}) })
.catch(error => { .catch(error => {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
}) })
}, },
updateMyExtension(wallet, data) { async updateMyExtension(wallet, data) {
LNbits.api await LNbits.api
.request( .request(
'PUT', 'PUT',
`/myextension/api/v1/myex/${data.id}`, `/myextension/api/v1/myex/${data.id}`,
@ -129,45 +118,42 @@ window.app = Vue.createApp({
this.myex = _.reject(this.myex, obj => { this.myex = _.reject(this.myex, obj => {
return obj.id == data.id return obj.id == data.id
}) })
this.myex.push(mapMyExtension(response.data)) this.myex.push(response.data)
this.closeFormDialog() this.closeFormDialog()
}) })
.catch(error => { .catch(error => {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
}) })
}, },
deleteMyExtension: function (tempId) { async deleteMyExtension(tempId) {
var self = this
var myextension = _.findWhere(this.myex, {id: tempId}) var myextension = _.findWhere(this.myex, {id: tempId})
await LNbits.utils
LNbits.utils
.confirmDialog('Are you sure you want to delete this MyExtension?') .confirmDialog('Are you sure you want to delete this MyExtension?')
.onOk(function () { .onOk(function () {
LNbits.api LNbits.api
.request( .request(
'DELETE', 'DELETE',
'/myextension/api/v1/myex/' + tempId, '/myextension/api/v1/myex/' + tempId,
_.findWhere(self.g.user.wallets, {id: myextension.wallet}) _.findWhere(this.g.user.wallets, {id: myextension.wallet}).adminkey
.adminkey
) )
.then(function (response) { .then(() => {
self.myex = _.reject(self.myex, function (obj) { this.myex = _.reject(this.myex, function (obj) {
return obj.id == tempId return obj.id == tempId
}) })
}) })
.catch(function (error) { .catch(error => {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
}) })
}) })
}, },
exportCSV: function () { async exportCSV() {
LNbits.utils.exportCSV(this.myexTable.columns, this.myex) await LNbits.utils.exportCSV(this.myexTable.columns, this.myex)
}, },
itemsArray(tempId) { async itemsArray(tempId) {
const myextension = _.findWhere(this.myex, {id: tempId}) const myextension = _.findWhere(this.myex, {id: tempId})
return [...myextension.itemsMap.values()] return [...myextension.itemsMap.values()]
}, },
openformDialog(id) { async openformDialog(id) {
const [tempId, itemId] = id.split(':') const [tempId, itemId] = id.split(':')
const myextension = _.findWhere(this.myex, {id: tempId}) const myextension = _.findWhere(this.myex, {id: tempId})
if (itemId) { if (itemId) {
@ -182,18 +168,17 @@ window.app = Vue.createApp({
this.formDialog.data.currency = myextension.currency this.formDialog.data.currency = myextension.currency
this.formDialog.show = true this.formDialog.show = true
}, },
closeformDialog() { async closeformDialog() {
this.formDialog.show = false this.formDialog.show = false
this.formDialog.data = {} this.formDialog.data = {}
}, },
openUrlDialog(id) { async openUrlDialog(id) {
this.urlDialog.data = _.findWhere(this.myex, {id}) this.urlDialog.data = _.findWhere(this.myex, {id})
this.qrValue = this.urlDialog.data.lnurlpay this.qrValue = this.urlDialog.data.lnurlpay
console.log(this.urlDialog.data.id) await this.connectWebocket(this.urlDialog.data.id)
this.connectWebocket(this.urlDialog.data.id)
this.urlDialog.show = true this.urlDialog.show = true
}, },
createInvoice(walletId, myextensionId) { async createInvoice(walletId, myextensionId) {
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
///Simple call to the api to create an invoice///// ///Simple call to the api to create an invoice/////
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
@ -210,7 +195,7 @@ window.app = Vue.createApp({
myextensionId: myextensionId myextensionId: myextensionId
} }
} }
LNbits.api await LNbits.api
.request('POST', `/api/v1/payments`, wallet.inkey, dataToSend) .request('POST', `/api/v1/payments`, wallet.inkey, dataToSend)
.then(response => { .then(response => {
this.qrValue = response.data.payment_request this.qrValue = response.data.payment_request
@ -219,11 +204,11 @@ window.app = Vue.createApp({
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
}) })
}, },
makeItRain() { async makeItRain() {
document.getElementById('vue').disabled = true document.getElementById('vue').disabled = true
var end = Date.now() + 2 * 1000 var end = Date.now() + 2 * 1000
var colors = ['#FFD700', '#ffffff'] var colors = ['#FFD700', '#ffffff']
function frame() { async function frame() {
confetti({ confetti({
particleCount: 2, particleCount: 2,
angle: 60, angle: 60,
@ -246,13 +231,12 @@ window.app = Vue.createApp({
document.getElementById('vue').disabled = false document.getElementById('vue').disabled = false
} }
} }
frame() await frame()
}, },
connectWebocket(wallet_id) { async connectWebocket(wallet_id) {
////////////////////////////////////////////////// //////////////////////////////////////////////////
///wait for pay action to happen and do a thing//// ///wait for pay action to happen and do a thing////
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
self = this
if (location.protocol !== 'http:') { if (location.protocol !== 'http:') {
localUrl = localUrl =
'wss://' + 'wss://' +
@ -271,18 +255,15 @@ window.app = Vue.createApp({
wallet_id wallet_id
} }
this.connection = new WebSocket(localUrl) this.connection = new WebSocket(localUrl)
this.connection.onmessage = function (e) { this.connection.onmessage = async function (e) {
self.makeItRain() await this.makeItRain()
}
} }
}, },
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
//////LIFECYCLE FUNCTIONS RUNNING ON PAGE LOAD///// //////LIFECYCLE FUNCTIONS RUNNING ON PAGE LOAD/////
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
created: function () { async created() {
if (this.g.user.wallets.length) { await this.getMyExtensions()
this.getMyExtensions()
}
} }
}) })

View file

@ -1,3 +1,5 @@
# Description: Add your page endpoints here.
from http import HTTPStatus from http import HTTPStatus
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request

View file

@ -1,3 +1,5 @@
# Description: This file contains the extensions API endpoints.
from http import HTTPStatus from http import HTTPStatus
from fastapi import APIRouter, Depends, Query, Request from fastapi import APIRouter, Depends, Query, Request
@ -15,15 +17,11 @@ from .crud import (
get_myextensions, get_myextensions,
update_myextension, update_myextension,
) )
from .helpers import lnurler
from .models import CreateMyExtensionData, MyExtension from .models import CreateMyExtensionData, MyExtension
myextension_api_router = APIRouter() myextension_api_router = APIRouter()
#######################################
##### ADD YOUR API ENDPOINTS HERE #####
#######################################
# Note: we add the lnurl params to returns so the links # Note: we add the lnurl params to returns so the links
# are generated in the MyExtension model in models.py # are generated in the MyExtension model in models.py
@ -32,15 +30,21 @@ myextension_api_router = APIRouter()
@myextension_api_router.get("/api/v1/myex") @myextension_api_router.get("/api/v1/myex")
async def api_myextensions( async def api_myextensions(
req: Request, req: Request, # Withoutthe lnurl stuff this wouldnt be needed
all_wallets: bool = Query(False),
wallet: WalletTypeInfo = Depends(require_invoice_key), wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> list[MyExtension]: ) -> list[MyExtension]:
wallet_ids = [wallet.wallet.id] wallet_ids = [wallet.wallet.id]
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 await get_myextensions(wallet_ids) myextensions = await get_myextensions(wallet_ids)
# Populate lnurlpay and lnurlwithdraw for each instance.
# Without the lnurl stuff this wouldnt be needed.
for myex in myextensions:
myex.lnurlpay = lnurler(myex.id, "myextension.api_lnurl_pay", req)
myex.lnurlwithdraw = lnurler(myex.id, "myextension.api_lnurl_withdraw", req)
return myextensions
## Get a single record ## Get a single record
@ -51,12 +55,17 @@ async def api_myextensions(
dependencies=[Depends(require_invoice_key)], dependencies=[Depends(require_invoice_key)],
) )
async def api_myextension(myextension_id: str, req: Request) -> MyExtension: async def api_myextension(myextension_id: str, req: Request) -> MyExtension:
myextension = await get_myextension(myextension_id) myex = await get_myextension(myextension_id)
if not myextension: if not myex:
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 # Populate lnurlpay and lnurlwithdraw.
# Without the lnurl stuff this wouldnt be needed.
myex.lnurlpay = lnurler(myex.id, "myextension.api_lnurl_pay", req)
myex.lnurlwithdraw = lnurler(myex.id, "myextension.api_lnurl_withdraw", req)
return myex
## update a record ## update a record
@ -64,27 +73,33 @@ async def api_myextension(myextension_id: str, req: Request) -> MyExtension:
@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(
req: Request, # Withoutthe lnurl stuff this wouldnt be needed
data: MyExtension, data: MyExtension,
req: Request,
myextension_id: str, myextension_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(require_admin_key),
) -> MyExtension: ) -> MyExtension:
myextension = await get_myextension(myextension_id) myex = await get_myextension(myextension_id)
if not myextension: if not myex:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist." status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
) )
if wallet.wallet.id != myextension.wallet: if wallet.wallet.id != myex.wallet:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension." status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension."
) )
for key, value in data.dict().items(): for key, value in data.dict().items():
setattr(myextension, key, value) setattr(myex, key, value)
myextension = await update_myextension(data) myex = await update_myextension(data)
return myextension
# Populate lnurlpay and lnurlwithdraw.
# Without the lnurl stuff this wouldnt be needed.
myex.lnurlpay = lnurler(myex.id, "myextension.api_lnurl_pay", req)
myex.lnurlwithdraw = lnurler(myex.id, "myextension.api_lnurl_withdraw", req)
return myex
## Create a new record ## Create a new record
@ -92,15 +107,19 @@ 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(
req: Request, # Withoutthe lnurl stuff this wouldnt be needed
data: CreateMyExtensionData, data: CreateMyExtensionData,
req: Request,
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(require_admin_key),
) -> MyExtension: ) -> MyExtension:
myextension = MyExtension( myex = MyExtension(**data.dict(), wallet=wallet.wallet.id, id=urlsafe_short_hash())
**data.dict(), wallet=data.wallet or wallet.wallet.id, id=urlsafe_short_hash() myex = await create_myextension(myex)
)
myextension = await create_myextension(myextension) # Populate lnurlpay and lnurlwithdraw.
return myextension # Withoutthe lnurl stuff this wouldnt be needed.
myex.lnurlpay = lnurler(myex.id, "myextension.api_lnurl_pay", req)
myex.lnurlwithdraw = lnurler(myex.id, "myextension.api_lnurl_withdraw", req)
return myex
## Delete a record ## Delete a record
@ -110,14 +129,14 @@ async def api_myextension_create(
async def api_myextension_delete( async def api_myextension_delete(
myextension_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) myextension_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
): ):
myextension = await get_myextension(myextension_id) myex = await get_myextension(myextension_id)
if not myextension: if not myex:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist." status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
) )
if myextension.wallet != wallet.wallet.id: if myex.wallet != wallet.wallet.id:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension." status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension."
) )

View file

@ -1,6 +1,4 @@
# Maybe your extension needs some LNURL stuff. # Description: Extensions that use LNURL usually have a few endpoints in views_lnurl.py.
# Here is a very simple example of how to do it.
# Feel free to delete this file if you don't need it.
from http import HTTPStatus from http import HTTPStatus
from typing import Optional from typing import Optional