diff --git a/crud.py b/crud.py index 57a15a3..bef2895 100644 --- a/crud.py +++ b/crud.py @@ -1,6 +1,9 @@ +# Description: This file contains the CRUD operations for talking to the database. + from typing import List, Optional, Union from lnbits.db import Database +from loguru import logger 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): wallet_ids = [wallet_ids] q = ",".join([f"'{w}'" for w in wallet_ids]) + logger.debug(q) return await db.fetchall( f"SELECT * FROM myextension.maintable WHERE wallet IN ({q}) ORDER BY id", model=MyExtension, diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000..b7c8c4b --- /dev/null +++ b/helpers.py @@ -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)) diff --git a/models.py b/models.py index a91d415..af878f5 100644 --- a/models.py +++ b/models.py @@ -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 @@ -11,7 +7,6 @@ class CreateMyExtensionData(BaseModel): name: str lnurlpayamount: int lnurlwithdrawamount: int - wallet: Optional[str] = None total: int = 0 @@ -22,32 +17,5 @@ class MyExtension(BaseModel): lnurlwithdrawamount: int wallet: str total: int - - # Below is only needed if you want to add extra calculated fields to the model, - # 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 + lnurlpay: str + lnurlwithdraw: str diff --git a/static/js/index.js b/static/js/index.js index c6ea946..272c376 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -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({ el: '#vue', mixins: [windowMixin], @@ -56,233 +45,225 @@ window.app = Vue.createApp({ /////////////////////////////////////////////////// methods: { - closeFormDialog() { + async closeFormDialog() { this.formDialog.show = false this.formDialog.data = {} }, - getMyExtensions: function () { - var self = this - - LNbits.api + async getMyExtensions() { + await LNbits.api .request( 'GET', - '/myextension/api/v1/myex?all_wallets=true', + '/myextension/api/v1/myex', this.g.user.wallets[0].inkey ) - .then(function (response) { - self.myex = response.data.map(function (obj) { - return mapMyExtension(obj) - }) + .then(response => { + console.log(response.data) + this.myex = response.data }) - }, - sendMyExtensionData() { - const data = { - name: this.formDialog.data.name, - lnurlwithdrawamount: this.formDialog.data.lnurlwithdrawamount, - lnurlpayamount: this.formDialog.data.lnurlpayamount - } - const wallet = _.findWhere(this.g.user.wallets, { - id: this.formDialog.data.wallet + .catch(error => { + console.error('Error fetching data:', error) + }) + } + }, + async sendMyExtensionData() { + const data = { + name: this.formDialog.data.name, + 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) { - data.id = this.formDialog.data.id - data.wallet = wallet.id - data.total = this.formDialog.data.total - this.updateMyExtension(wallet, data) - } else { - this.createMyExtension(wallet, data) - } - }, - updateMyExtensionForm(tempId) { - const myextension = _.findWhere(this.myex, {id: tempId}) + .catch(error => { + LNbits.utils.notifyApiError(error) + }) + }, + async updateMyExtension(wallet, data) { + await 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(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 = { - ...myextension + ...item, + myextension: tempId } - if (this.formDialog.data.tip_wallet != '') { - this.formDialog.advanced.tips = true + } else { + 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 - } - this.formDialog.show = true - }, - 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 + } + await LNbits.api + .request('POST', `/api/v1/payments`, wallet.inkey, dataToSend) + .then(response => { + this.qrValue = response.data.payment_request }) - const dataToSend = { - out: false, - amount: this.invoiceAmount, - memo: 'Invoice created by MyExtension', - extra: { - tag: 'MyExtension', - myextensionId: myextensionId - } - } - LNbits.api - .request('POST', `/api/v1/payments`, wallet.inkey, dataToSend) - .then(response => { - this.qrValue = response.data.payment_request - }) - .catch(error => { - LNbits.utils.notifyApiError(error) - }) - }, - makeItRain() { - document.getElementById('vue').disabled = true - var end = Date.now() + 2 * 1000 - var colors = ['#FFD700', '#ffffff'] - function frame() { - confetti({ - particleCount: 2, - angle: 60, - spread: 55, - 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 + .catch(error => { + LNbits.utils.notifyApiError(error) + }) + }, + async makeItRain() { + document.getElementById('vue').disabled = true + var end = Date.now() + 2 * 1000 + var colors = ['#FFD700', '#ffffff'] + async function frame() { + confetti({ + particleCount: 2, + angle: 60, + spread: 55, + 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 { - localUrl = - 'ws://' + - document.domain + - ':' + - location.port + - '/api/v1/ws/' + - wallet_id - } - this.connection = new WebSocket(localUrl) - this.connection.onmessage = function (e) { - self.makeItRain() + document.getElementById('vue').disabled = false } } + 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///// /////////////////////////////////////////////////// - created: function () { - if (this.g.user.wallets.length) { - this.getMyExtensions() - } + async created() { + await this.getMyExtensions() } }) diff --git a/views.py b/views.py index 39b7831..e7a90f0 100644 --- a/views.py +++ b/views.py @@ -1,3 +1,5 @@ +# Description: Add your page endpoints here. + from http import HTTPStatus from fastapi import APIRouter, Depends, HTTPException, Request diff --git a/views_api.py b/views_api.py index eafe270..9553168 100644 --- a/views_api.py +++ b/views_api.py @@ -1,3 +1,5 @@ +# Description: This file contains the extensions API endpoints. + from http import HTTPStatus from fastapi import APIRouter, Depends, Query, Request @@ -15,15 +17,11 @@ from .crud import ( get_myextensions, update_myextension, ) +from .helpers import lnurler from .models import CreateMyExtensionData, MyExtension myextension_api_router = APIRouter() - -####################################### -##### 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 @@ -32,15 +30,21 @@ myextension_api_router = APIRouter() @myextension_api_router.get("/api/v1/myex") async def api_myextensions( - req: Request, - all_wallets: bool = Query(False), + req: Request, # Withoutthe lnurl stuff this wouldnt be needed wallet: WalletTypeInfo = Depends(require_invoice_key), ) -> list[MyExtension]: wallet_ids = [wallet.wallet.id] - if all_wallets: - user = await get_user(wallet.wallet.user) - wallet_ids = user.wallet_ids if user else [] - return await get_myextensions(wallet_ids) + user = await get_user(wallet.wallet.user) + wallet_ids = user.wallet_ids if user else [] + 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 @@ -51,12 +55,17 @@ async def api_myextensions( dependencies=[Depends(require_invoice_key)], ) async def api_myextension(myextension_id: str, req: Request) -> MyExtension: - myextension = await get_myextension(myextension_id) - if not myextension: + myex = await get_myextension(myextension_id) + if not myex: raise HTTPException( 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 @@ -64,27 +73,33 @@ async def api_myextension(myextension_id: str, req: Request) -> MyExtension: @myextension_api_router.put("/api/v1/myex/{myextension_id}") async def api_myextension_update( + req: Request, # Withoutthe lnurl stuff this wouldnt be needed data: MyExtension, - req: Request, myextension_id: str, wallet: WalletTypeInfo = Depends(require_admin_key), ) -> MyExtension: - myextension = await get_myextension(myextension_id) - if not myextension: + myex = await get_myextension(myextension_id) + if not myex: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist." ) - if wallet.wallet.id != myextension.wallet: + if wallet.wallet.id != myex.wallet: raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension." ) for key, value in data.dict().items(): - setattr(myextension, key, value) + setattr(myex, key, value) - myextension = await update_myextension(data) - return myextension + myex = await update_myextension(data) + + # 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 @@ -92,15 +107,19 @@ async def api_myextension_update( @myextension_api_router.post("/api/v1/myex", status_code=HTTPStatus.CREATED) async def api_myextension_create( + req: Request, # Withoutthe lnurl stuff this wouldnt be needed data: CreateMyExtensionData, - req: Request, wallet: WalletTypeInfo = Depends(require_admin_key), ) -> MyExtension: - myextension = MyExtension( - **data.dict(), wallet=data.wallet or wallet.wallet.id, id=urlsafe_short_hash() - ) - myextension = await create_myextension(myextension) - return myextension + myex = MyExtension(**data.dict(), wallet=wallet.wallet.id, id=urlsafe_short_hash()) + myex = await create_myextension(myex) + + # Populate lnurlpay and lnurlwithdraw. + # 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 @@ -110,14 +129,14 @@ async def api_myextension_create( async def api_myextension_delete( 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( status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist." ) - if myextension.wallet != wallet.wallet.id: + if myex.wallet != wallet.wallet.id: raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension." ) diff --git a/views_lnurl.py b/views_lnurl.py index d915fab..c953fe2 100644 --- a/views_lnurl.py +++ b/views_lnurl.py @@ -1,6 +1,4 @@ -# Maybe your extension needs some LNURL stuff. -# Here is a very simple example of how to do it. -# Feel free to delete this file if you don't need it. +# Description: Extensions that use LNURL usually have a few endpoints in views_lnurl.py. from http import HTTPStatus from typing import Optional