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,233 +45,225 @@ 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 => {
sendMyExtensionData() { console.error('Error fetching data:', error)
const data = { })
name: this.formDialog.data.name, }
lnurlwithdrawamount: this.formDialog.data.lnurlwithdrawamount, },
lnurlpayamount: this.formDialog.data.lnurlpayamount async sendMyExtensionData() {
} const data = {
const wallet = _.findWhere(this.g.user.wallets, { name: this.formDialog.data.name,
id: this.formDialog.data.wallet lnurlwithdrawamount: this.formDialog.data.lnurlwithdrawamount,
lnurlpayamount: this.formDialog.data.lnurlpayamount
}
const wallet = _.findWhere(this.g.user.wallets, {
id: this.formDialog.data.wallet
})
if (this.formDialog.data.id) {
data.id = this.formDialog.data.id
data.total = this.formDialog.data.total
await this.updateMyExtension(wallet, data)
} else {
await this.createMyExtension(wallet, data)
}
},
async updateMyExtensionForm(tempId) {
const myextension = _.findWhere(this.myex, {id: tempId})
this.formDialog.data = {
...myextension
}
if (this.formDialog.data.tip_wallet != '') {
this.formDialog.advanced.tips = true
}
if (this.formDialog.data.withdrawlimit >= 1) {
this.formDialog.advanced.otc = true
}
this.formDialog.show = true
},
async createMyExtension(wallet, data) {
await LNbits.api
.request('POST', '/myextension/api/v1/myex', wallet.adminkey, data)
.then(response => {
this.myex.push(response.data)
this.closeFormDialog()
}) })
if (this.formDialog.data.id) { .catch(error => {
data.id = this.formDialog.data.id LNbits.utils.notifyApiError(error)
data.wallet = wallet.id })
data.total = this.formDialog.data.total },
this.updateMyExtension(wallet, data) async updateMyExtension(wallet, data) {
} else { await LNbits.api
this.createMyExtension(wallet, data) .request(
} 'PUT',
}, `/myextension/api/v1/myex/${data.id}`,
updateMyExtensionForm(tempId) { wallet.adminkey,
const myextension = _.findWhere(this.myex, {id: tempId}) data
)
.then(response => {
this.myex = _.reject(this.myex, obj => {
return obj.id == data.id
})
this.myex.push(response.data)
this.closeFormDialog()
})
.catch(error => {
LNbits.utils.notifyApiError(error)
})
},
async deleteMyExtension(tempId) {
var myextension = _.findWhere(this.myex, {id: tempId})
await LNbits.utils
.confirmDialog('Are you sure you want to delete this MyExtension?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/myextension/api/v1/myex/' + tempId,
_.findWhere(this.g.user.wallets, {id: myextension.wallet}).adminkey
)
.then(() => {
this.myex = _.reject(this.myex, function (obj) {
return obj.id == tempId
})
})
.catch(error => {
LNbits.utils.notifyApiError(error)
})
})
},
async exportCSV() {
await LNbits.utils.exportCSV(this.myexTable.columns, this.myex)
},
async itemsArray(tempId) {
const myextension = _.findWhere(this.myex, {id: tempId})
return [...myextension.itemsMap.values()]
},
async openformDialog(id) {
const [tempId, itemId] = id.split(':')
const myextension = _.findWhere(this.myex, {id: tempId})
if (itemId) {
const item = myextension.itemsMap.get(id)
this.formDialog.data = { this.formDialog.data = {
...myextension ...item,
myextension: tempId
} }
if (this.formDialog.data.tip_wallet != '') { } else {
this.formDialog.advanced.tips = true this.formDialog.data.myextension = tempId
}
this.formDialog.data.currency = myextension.currency
this.formDialog.show = true
},
async closeformDialog() {
this.formDialog.show = false
this.formDialog.data = {}
},
async openUrlDialog(id) {
this.urlDialog.data = _.findWhere(this.myex, {id})
this.qrValue = this.urlDialog.data.lnurlpay
await this.connectWebocket(this.urlDialog.data.id)
this.urlDialog.show = true
},
async createInvoice(walletId, myextensionId) {
///////////////////////////////////////////////////
///Simple call to the api to create an invoice/////
///////////////////////////////////////////////////
console.log(walletId)
const wallet = _.findWhere(this.g.user.wallets, {
id: walletId
})
const dataToSend = {
out: false,
amount: this.invoiceAmount,
memo: 'Invoice created by MyExtension',
extra: {
tag: 'MyExtension',
myextensionId: myextensionId
} }
if (this.formDialog.data.withdrawlimit >= 1) { }
this.formDialog.advanced.otc = true await LNbits.api
} .request('POST', `/api/v1/payments`, wallet.inkey, dataToSend)
this.formDialog.show = true .then(response => {
}, this.qrValue = response.data.payment_request
createMyExtension(wallet, data) {
LNbits.api
.request('POST', '/myextension/api/v1/myex', wallet.adminkey, data)
.then(response => {
this.myex.push(mapMyExtension(response.data))
this.closeFormDialog()
})
.catch(error => {
LNbits.utils.notifyApiError(error)
})
},
updateMyExtension(wallet, data) {
LNbits.api
.request(
'PUT',
`/myextension/api/v1/myex/${data.id}`,
wallet.adminkey,
data
)
.then(response => {
this.myex = _.reject(this.myex, obj => {
return obj.id == data.id
})
this.myex.push(mapMyExtension(response.data))
this.closeFormDialog()
})
.catch(error => {
LNbits.utils.notifyApiError(error)
})
},
deleteMyExtension: function (tempId) {
var self = this
var myextension = _.findWhere(this.myex, {id: tempId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this MyExtension?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/myextension/api/v1/myex/' + tempId,
_.findWhere(self.g.user.wallets, {id: myextension.wallet})
.adminkey
)
.then(function (response) {
self.myex = _.reject(self.myex, function (obj) {
return obj.id == tempId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportCSV: function () {
LNbits.utils.exportCSV(this.myexTable.columns, this.myex)
},
itemsArray(tempId) {
const myextension = _.findWhere(this.myex, {id: tempId})
return [...myextension.itemsMap.values()]
},
openformDialog(id) {
const [tempId, itemId] = id.split(':')
const myextension = _.findWhere(this.myex, {id: tempId})
if (itemId) {
const item = myextension.itemsMap.get(id)
this.formDialog.data = {
...item,
myextension: tempId
}
} else {
this.formDialog.data.myextension = tempId
}
this.formDialog.data.currency = myextension.currency
this.formDialog.show = true
},
closeformDialog() {
this.formDialog.show = false
this.formDialog.data = {}
},
openUrlDialog(id) {
this.urlDialog.data = _.findWhere(this.myex, {id})
this.qrValue = this.urlDialog.data.lnurlpay
console.log(this.urlDialog.data.id)
this.connectWebocket(this.urlDialog.data.id)
this.urlDialog.show = true
},
createInvoice(walletId, myextensionId) {
///////////////////////////////////////////////////
///Simple call to the api to create an invoice/////
///////////////////////////////////////////////////
console.log(walletId)
const wallet = _.findWhere(this.g.user.wallets, {
id: walletId
}) })
const dataToSend = { .catch(error => {
out: false, LNbits.utils.notifyApiError(error)
amount: this.invoiceAmount, })
memo: 'Invoice created by MyExtension', },
extra: { async makeItRain() {
tag: 'MyExtension', document.getElementById('vue').disabled = true
myextensionId: myextensionId var end = Date.now() + 2 * 1000
} var colors = ['#FFD700', '#ffffff']
} async function frame() {
LNbits.api confetti({
.request('POST', `/api/v1/payments`, wallet.inkey, dataToSend) particleCount: 2,
.then(response => { angle: 60,
this.qrValue = response.data.payment_request spread: 55,
}) origin: {x: 0},
.catch(error => { colors: colors,
LNbits.utils.notifyApiError(error) zIndex: 999999
}) })
}, confetti({
makeItRain() { particleCount: 2,
document.getElementById('vue').disabled = true angle: 120,
var end = Date.now() + 2 * 1000 spread: 55,
var colors = ['#FFD700', '#ffffff'] origin: {x: 1},
function frame() { colors: colors,
confetti({ zIndex: 999999
particleCount: 2, })
angle: 60, if (Date.now() < end) {
spread: 55, requestAnimationFrame(frame)
origin: {x: 0},
colors: colors,
zIndex: 999999
})
confetti({
particleCount: 2,
angle: 120,
spread: 55,
origin: {x: 1},
colors: colors,
zIndex: 999999
})
if (Date.now() < end) {
requestAnimationFrame(frame)
} else {
document.getElementById('vue').disabled = false
}
}
frame()
},
connectWebocket(wallet_id) {
//////////////////////////////////////////////////
///wait for pay action to happen and do a thing////
///////////////////////////////////////////////////
self = this
if (location.protocol !== 'http:') {
localUrl =
'wss://' +
document.domain +
':' +
location.port +
'/api/v1/ws/' +
wallet_id
} else { } else {
localUrl = document.getElementById('vue').disabled = false
'ws://' +
document.domain +
':' +
location.port +
'/api/v1/ws/' +
wallet_id
}
this.connection = new WebSocket(localUrl)
this.connection.onmessage = function (e) {
self.makeItRain()
} }
} }
await frame()
},
async connectWebocket(wallet_id) {
//////////////////////////////////////////////////
///wait for pay action to happen and do a thing////
///////////////////////////////////////////////////
if (location.protocol !== 'http:') {
localUrl =
'wss://' +
document.domain +
':' +
location.port +
'/api/v1/ws/' +
wallet_id
} else {
localUrl =
'ws://' +
document.domain +
':' +
location.port +
'/api/v1/ws/' +
wallet_id
}
this.connection = new WebSocket(localUrl)
this.connection.onmessage = async function (e) {
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 [] myextensions = await get_myextensions(wallet_ids)
return 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