Merge branch 'v1' into feat_lnurl_v1

This commit is contained in:
Arc 2024-11-18 13:44:41 +00:00
commit 5bdc334249
8 changed files with 1422 additions and 1038 deletions

View file

@ -19,3 +19,15 @@ async def m001_initial(db):
);
"""
)
async def m002_add_timestamp(db):
"""
Add timestamp to templates table.
"""
await db.execute(
f"""
ALTER TABLE myextension.maintable
ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now};
"""
)

View file

@ -1,5 +1,6 @@
# Data models for your extension
from datetime import datetime, timezone
from typing import Optional
from fastapi import Request

2115
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,7 @@ authors = ["benarc", "dni <dni@lnbits.com>"]
[tool.poetry.dependencies]
python = "^3.10 | ^3.9"
lnbits = "*"
lnbits = {version = "*", allow-prereleases = true}
[tool.poetry.group.dev.dependencies]
black = "^24.3.0"

280
static/js/index.js Normal file
View file

@ -0,0 +1,280 @@
///////////////////////////////////////////////////
//////////an object we can update with data////////
///////////////////////////////////////////////////
const mapMyExtension = obj => {
obj.date = Quasar.date.formatDate(
new Date(obj.created_at),
'YYYY-MM-DD HH:mm'
)
obj.myextension = ['/myextension/', obj.id].join('')
return obj
}
window.app = Vue.createApp({
el: '#vue',
mixins: [windowMixin],
delimiters: ['${', '}'],
data() {
return {
invoiceAmount: 10,
qrValue: 'lnurlpay',
myex: [],
myexTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{
name: 'wallet',
align: 'left',
label: 'Wallet',
field: 'wallet'
},
{
name: 'total',
align: 'left',
label: 'Total sent/received',
field: 'total'
}
],
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
data: {},
advanced: {}
},
urlDialog: {
show: false,
data: {}
}
}
},
///////////////////////////////////////////////////
////////////////METHODS FUNCTIONS//////////////////
///////////////////////////////////////////////////
methods: {
closeFormDialog() {
this.formDialog.show = false
this.formDialog.data = {}
},
getMyExtensions() {
LNbits.api
.request(
'GET',
'/myextension/api/v1/myex?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(response => {
this.myex = response.data.map(obj => {
return mapMyExtension(obj)
})
})
},
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.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})
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
},
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(tempId) {
const myextension = _.findWhere(this.myex, {id: tempId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this MyExtension?')
.onOk(() => {
LNbits.api
.request(
'DELETE',
'/myextension/api/v1/myex/' + tempId,
_.findWhere(this.g.user.wallets, {id: myextension.wallet})
.adminkey
)
.then(response => {
this.myex = _.reject(this.myex, obj => {
return obj.id == tempId
})
})
.catch(LNbits.utils.notifyApiError)
})
},
exportCSV() {
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 = {
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
const end = Date.now() + 2 * 1000
const colors = ['#FFD700', '#ffffff']
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
}
},
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 = e => {
this.makeItRain()
}
}
},
///////////////////////////////////////////////////
//////LIFECYCLE FUNCTIONS RUNNING ON PAGE LOAD/////
///////////////////////////////////////////////////
created() {
if (this.g.user.wallets.length) {
this.getMyExtensions()
}
}
})

View file

@ -3,7 +3,9 @@
<!--/////////////////////////////////////////////////-->
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
%} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('myextension/static', path='js/index.js') }}"></script>
{% endblock %} {% block page %}
<div class="row q-col-gutter-md" id="makeItRain">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>

View file

@ -1,12 +1,11 @@
from http import HTTPStatus
from fastapi import APIRouter, Depends, Request
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from lnbits.helpers import template_renderer
from lnbits.settings import settings
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from .crud import get_myextension

View file

@ -29,7 +29,7 @@ myextension_api_router = APIRouter()
## 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")
async def api_myextensions(
req: Request,
all_wallets: bool = Query(False),
@ -37,7 +37,7 @@ async def api_myextensions(
):
wallet_ids = [wallet.wallet.id]
if all_wallets:
user = await get_user(wallet.wallet.user)
user = await get_user(key_info.wallet.user)
wallet_ids = user.wallet_ids if user else []
return [
{
@ -54,7 +54,6 @@ async def api_myextensions(
@myextension_api_router.get(
"/api/v1/myex/{myextension_id}",
status_code=HTTPStatus.OK,
dependencies=[Depends(require_invoice_key)],
)
async def api_myextension(myextension_id: str, req: Request):
@ -80,11 +79,13 @@ async def api_myextension_update(
myextension_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> MyExtension:
myextension = await get_myextension(myextension_id)
assert myextension, "MyExtension couldn't be retrieved"
if not myextension:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
)
if wallet.wallet.id != myextension.wallet:
if key_info.wallet.id != myextension.wallet:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension."
)
@ -123,7 +124,7 @@ async def api_myextension_create(
@myextension_api_router.delete("/api/v1/myex/{myextension_id}")
async def api_myextension_delete(
myextension_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
myextension_id: str, key_info: WalletTypeInfo = Depends(require_admin_key)
):
myextension = await get_myextension(myextension_id)
@ -132,13 +133,12 @@ async def api_myextension_delete(
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
)
if myextension.wallet != wallet.wallet.id:
if myextension.wallet != key_info.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension."
)
await delete_myextension(myextension_id)
return "", HTTPStatus.NO_CONTENT
# ANY OTHER ENDPOINTS YOU NEED
@ -162,19 +162,14 @@ async def api_myextension_create_invoice(
# we create a payment and add some tags,
# so tasks.py can grab the payment once its paid
try:
payment = await create_invoice(
wallet_id=myextension.wallet,
amount=amount,
memo=f"{memo} to {myextension.name}" if memo else f"{myextension.name}",
extra={
"tag": "myextension",
"amount": amount,
},
)
except Exception as exc:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
) from exc
payment = await create_invoice(
wallet_id=myextension.wallet,
amount=amount,
memo=f"{memo} to {myextension.name}" if memo else f"{myextension.name}",
extra={
"tag": "myextension",
"amount": amount,
},
)
return {"payment_hash": payment.payment_hash, "payment_request": payment.bolt11}