Paid extensions (#2229)
* fix: download archive file `async` * feat: add `pay_link` property * feat: basic install using internal wallet for payment * fix: pop-up issues * chore: refactor * feat: detect paid extensions * fix: payment check * feat: small stuff * feat: show external invoice * fix: regression for extension install * feat: store previos successful payments * refactor: simplify, almost works * chore: gugu gaga * fix: pay and install * fix: do not pay invoice on the back-end * chore: code clean-up * feat: basic websocker listener * feat: use websocket to watch for invoice payment * feat: remember hanging invoices * refactor: extract `localStorage` methods * chore: code format * chore: code clean-up after test * feat: remember previous payment_hashes * chore: code format * refactor: rename `ExtensionPaymentInfo` to `ReleasePaymentInfo` * refactor: method rename * fix: release version matters now * chore: code format * refactor: method rename * refactor: extract method `_restore_payment_info` * refactor: extract method * chore: rollback `CACHE_VERSION` * chore: code format * feat: i18n * chore: update bundle * refactor: public method name * chore: code format * fix: websocket connection * Update installation.md (#2259) * Update installation.md (#2260) * fix: try to fix `openapi` error * chore: bundle * chore:bundle --------- Co-authored-by: benarc <ben@arc.wales> Co-authored-by: Arc <33088785+arcbtc@users.noreply.github.com>
This commit is contained in:
parent
54c6faa4b6
commit
d6c8ad1d0d
10 changed files with 1102 additions and 333 deletions
|
|
@ -194,7 +194,7 @@ async def check_installed_extensions(app: FastAPI):
|
|||
|
||||
for ext in installed_extensions:
|
||||
try:
|
||||
installed = check_installed_extension_files(ext)
|
||||
installed = await check_installed_extension_files(ext)
|
||||
if not installed:
|
||||
await restore_installed_extension(app, ext)
|
||||
logger.info(
|
||||
|
|
@ -252,14 +252,14 @@ async def build_all_installed_extensions_list(
|
|||
]
|
||||
|
||||
|
||||
def check_installed_extension_files(ext: InstallableExtension) -> bool:
|
||||
async def check_installed_extension_files(ext: InstallableExtension) -> bool:
|
||||
if ext.has_installed_version:
|
||||
return True
|
||||
|
||||
zip_files = glob.glob(os.path.join(settings.lnbits_data_folder, "zips", "*.zip"))
|
||||
|
||||
if f"./{str(ext.zip_path)}" not in zip_files:
|
||||
ext.download_archive()
|
||||
await ext.download_archive()
|
||||
ext.extract_archive()
|
||||
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -397,7 +397,10 @@ async def install_extension(
|
|||
return False, "No release selected"
|
||||
|
||||
data = CreateExtension(
|
||||
ext_id=extension, archive=release.archive, source_repo=release.source_repo
|
||||
ext_id=extension,
|
||||
archive=release.archive,
|
||||
source_repo=release.source_repo,
|
||||
version=release.version,
|
||||
)
|
||||
await _call_install_extension(data, url, admin_user)
|
||||
click.echo(f"Extension '{extension}' ({release.version}) installed.")
|
||||
|
|
@ -445,7 +448,10 @@ async def update_extension(
|
|||
click.echo(f"Updating '{extension}' extension to version: {release.version }")
|
||||
|
||||
data = CreateExtension(
|
||||
ext_id=extension, archive=release.archive, source_repo=release.source_repo
|
||||
ext_id=extension,
|
||||
archive=release.archive,
|
||||
source_repo=release.source_repo,
|
||||
version=release.version,
|
||||
)
|
||||
|
||||
await _call_install_extension(data, url, admin_user)
|
||||
|
|
|
|||
|
|
@ -322,6 +322,7 @@ async def add_installed_extension(
|
|||
dict(ext.installed_release) if ext.installed_release else None
|
||||
),
|
||||
"dependencies": ext.dependencies,
|
||||
"payments": [dict(p) for p in ext.payments] if ext.payments else None,
|
||||
}
|
||||
|
||||
version = ext.installed_release.version if ext.installed_release else ""
|
||||
|
|
|
|||
|
|
@ -307,7 +307,40 @@
|
|||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showUpgradeDialog">
|
||||
<q-card class="q-pa-lg lnbits__dialog-card">
|
||||
<q-card v-if="selectedRelease" class="q-pa-lg lnbits__dialog-card">
|
||||
<q-card-section>
|
||||
<div v-if="selectedRelease.paymentRequest">
|
||||
<a :href="'lightning:' + selectedRelease.paymentRequest">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<lnbits-qrcode
|
||||
:value="'lightning:' + selectedRelease.paymentRequest.toUpperCase()"
|
||||
></lnbits-qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-spinner color="primary" size="2.55em"></q-spinner>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
v-if="selectedRelease.paymentRequest"
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(selectedRelease.paymentRequest)"
|
||||
:label="$t('copy_invoice')"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-btn v-close-popup flat color="grey" class="float-right q-ml-lg">
|
||||
{%raw%}{{ $t('close') }}{%endraw%}</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
<q-card v-else class="q-pa-lg lnbits__dialog-card">
|
||||
<q-card-section>
|
||||
<div class="text-h6" v-text="selectedExtension?.name"></div>
|
||||
</q-card-section>
|
||||
|
|
@ -376,19 +409,104 @@
|
|||
color="pink"
|
||||
size="70px"
|
||||
></q-icon>
|
||||
Cannot get the release details.
|
||||
<span v-text="$t('release_details_error')"></span>
|
||||
</div>
|
||||
<q-card v-else>
|
||||
<q-card-section v-if="release.is_version_compatible">
|
||||
<q-btn
|
||||
v-if="!release.isInstalled"
|
||||
@click="installExtension(release)"
|
||||
color="primary unelevated mt-lg pt-lg"
|
||||
>{%raw%}{{ $t('install') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-btn v-else @click="showUninstall()" flat color="red">
|
||||
{%raw%}{{ $t('uninstall') }}{%endraw%}</q-btn
|
||||
<span
|
||||
v-if="release.requiresPayment && !release.paid_sats"
|
||||
v-text="$t('extension_cost', {cost: release.cost_sats})"
|
||||
class="q-mb-lg"
|
||||
></span>
|
||||
<span
|
||||
v-if="release.requiresPayment && release.paid_sats"
|
||||
class="q-mb-lg"
|
||||
v-text="$t('extension_paid_sats', {paid_sats: release.paid_sats})"
|
||||
></span>
|
||||
<div
|
||||
v-if="!release.requiresPayment || (release.requiresPayment && release.paid_sats)"
|
||||
>
|
||||
<q-btn
|
||||
v-if="!release.isInstalled"
|
||||
@click="installExtension(release)"
|
||||
color="primary unelevated mt-lg pt-lg"
|
||||
:label="$t('install')"
|
||||
></q-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="release.requiresPayment && !release.paid_sats">
|
||||
<div v-if="!release.payment_hash">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="release.paidAmount"
|
||||
type="number"
|
||||
:min="release.cost_sats"
|
||||
suffix="sat"
|
||||
class="q-mt-sm"
|
||||
>
|
||||
</q-input>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="release.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
class="q-mt-sm"
|
||||
>
|
||||
</q-select>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="payAndInstall(release)"
|
||||
:disabled="!release.wallet"
|
||||
class="q-mt-sm"
|
||||
:label="$t('pay_from_wallet')"
|
||||
></q-btn>
|
||||
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="showQRCode(release)"
|
||||
class="q-mt-sm float-right"
|
||||
:label="$t('show_qr')"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<br />
|
||||
<span
|
||||
class="q-mb-lg q-mt-lg"
|
||||
v-text="'There is a previous pending invoice for this release.'"
|
||||
></span>
|
||||
<br />
|
||||
<q-btn
|
||||
unelevated
|
||||
@click="installExtension(release)"
|
||||
color="primary"
|
||||
class="q-mt-sm"
|
||||
:label="$t('retry_install')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
@click="clearHangingInvoice(release)"
|
||||
color="primary"
|
||||
class="q-mt-sm float-right"
|
||||
:label="$t('new_payment')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<q-btn
|
||||
v-if="release.isInstalled"
|
||||
@click="showUninstall()"
|
||||
:label="$t('uninstall')"
|
||||
flat
|
||||
color="red"
|
||||
></q-btn>
|
||||
</div>
|
||||
|
||||
<a
|
||||
v-if="release.html_url"
|
||||
class="text-secondary float-right"
|
||||
|
|
@ -464,8 +582,10 @@
|
|||
dropDbExtensionId: '',
|
||||
selectedExtension: null,
|
||||
selectedExtensionRepos: null,
|
||||
selectedRelease: null,
|
||||
uninstallAndDropDb: false,
|
||||
maxStars: 5,
|
||||
paylinkWebsocket: null,
|
||||
user: null
|
||||
}
|
||||
},
|
||||
|
|
@ -504,10 +624,18 @@
|
|||
.filter(extensionNameContains(term))
|
||||
this.tab = tab
|
||||
},
|
||||
|
||||
installExtension: async function (release) {
|
||||
// no longer required to check if the invoice was paid
|
||||
// the install logic has been triggered one way or another
|
||||
this.unsubscribeFromPaylinkWs()
|
||||
|
||||
const extension = this.selectedExtension
|
||||
extension.inProgress = true
|
||||
this.showUpgradeDialog = false
|
||||
release.payment_hash =
|
||||
release.payment_hash || this.getPaylinkHash(release.pay_link)
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
|
|
@ -516,7 +644,9 @@
|
|||
{
|
||||
ext_id: extension.id,
|
||||
archive: release.archive,
|
||||
source_repo: release.source_repo
|
||||
source_repo: release.source_repo,
|
||||
payment_hash: release.payment_hash,
|
||||
version: release.version
|
||||
}
|
||||
)
|
||||
.then(response => {
|
||||
|
|
@ -530,8 +660,9 @@
|
|||
this.tab = 'installed'
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
console.warn(err)
|
||||
extension.inProgress = false
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
uninstallExtension: async function () {
|
||||
|
|
@ -623,8 +754,10 @@
|
|||
|
||||
showUpgrade: async function (extension) {
|
||||
this.selectedExtension = extension
|
||||
this.showUpgradeDialog = true
|
||||
this.selectedRelease = null
|
||||
this.selectedExtensionRepos = null
|
||||
this.showUpgradeDialog = true
|
||||
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
|
|
@ -648,6 +781,12 @@
|
|||
if (release.isInstalled) {
|
||||
repos[release.source_repo].isInstalled = true
|
||||
}
|
||||
if (release.pay_link) {
|
||||
release.requiresPayment = true
|
||||
release.paidAmount = release.cost_sats
|
||||
release.payment_hash = this.getPaylinkHash(release.pay_link)
|
||||
}
|
||||
|
||||
repos[release.source_repo].releases.push(release)
|
||||
return repos
|
||||
}, {})
|
||||
|
|
@ -656,6 +795,122 @@
|
|||
extension.inProgress = false
|
||||
}
|
||||
},
|
||||
|
||||
async payAndInstall(release) {
|
||||
try {
|
||||
this.selectedExtension.inProgress = true
|
||||
this.showUpgradeDialog = false
|
||||
const paymentInfo = await this.requestPayment(
|
||||
this.selectedExtension.id,
|
||||
release
|
||||
)
|
||||
this.rememberPaylinkHash(release.pay_link, paymentInfo.payment_hash)
|
||||
const wallet = this.g.user.wallets.find(w => w.id === release.wallet)
|
||||
const {data} = await LNbits.api.payInvoice(
|
||||
wallet,
|
||||
paymentInfo.payment_request
|
||||
)
|
||||
|
||||
release.payment_hash = data.payment_hash
|
||||
|
||||
await this.installExtension(release)
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
LNbits.utils.notifyApiError(err)
|
||||
} finally {
|
||||
this.selectedExtension.inProgress = false
|
||||
}
|
||||
},
|
||||
async showQRCode(release) {
|
||||
this.selectedRelease = release
|
||||
|
||||
try {
|
||||
const data = await this.requestPayment(
|
||||
this.selectedExtension.id,
|
||||
release
|
||||
)
|
||||
|
||||
this.selectedRelease.paymentRequest = data.payment_request
|
||||
this.selectedRelease.payment_hash = data.payment_hash
|
||||
this.selectedRelease = _.clone(this.selectedRelease)
|
||||
this.rememberPaylinkHash(
|
||||
this.selectedRelease.pay_link,
|
||||
this.selectedRelease.payment_hash
|
||||
)
|
||||
|
||||
this.subscribeToPaylinkWs(
|
||||
this.selectedRelease.pay_link,
|
||||
data.payment_hash
|
||||
)
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
LNbits.utils.notifyApiError(err)
|
||||
}
|
||||
},
|
||||
|
||||
async requestPayment(extId, release) {
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
`/api/v1/extension/invoice`,
|
||||
this.g.user.wallets[0].adminkey,
|
||||
{
|
||||
ext_id: extId,
|
||||
archive: release.archive,
|
||||
source_repo: release.source_repo,
|
||||
cost_sats: release.paidAmount,
|
||||
version: release.version
|
||||
}
|
||||
)
|
||||
return data
|
||||
},
|
||||
|
||||
clearHangingInvoice(release) {
|
||||
this.forgetPaylinkHash(release.pay_link)
|
||||
release.payment_hash = null
|
||||
},
|
||||
|
||||
rememberPaylinkHash(pay_link, payment_hash) {
|
||||
this.$q.localStorage.set(
|
||||
`lnbits.extensions.paylink.${pay_link}`,
|
||||
payment_hash
|
||||
)
|
||||
},
|
||||
getPaylinkHash(pay_link) {
|
||||
return this.$q.localStorage.getItem(
|
||||
`lnbits.extensions.paylink.${pay_link}`
|
||||
)
|
||||
},
|
||||
forgetPaylinkHash(pay_link) {
|
||||
this.$q.localStorage.remove(`lnbits.extensions.paylink.${pay_link}`)
|
||||
},
|
||||
subscribeToPaylinkWs(pay_link, payment_hash) {
|
||||
const url = new URL(`${pay_link}/${payment_hash}`)
|
||||
url.protocol = url.protocol === 'https:' ? 'wss' : 'ws'
|
||||
this.paylinkWebsocket = new WebSocket(url)
|
||||
this.paylinkWebsocket.addEventListener('message', async ({data}) => {
|
||||
const resp = JSON.parse(data)
|
||||
if (resp.paid) {
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Invoice Paid!'
|
||||
})
|
||||
this.installExtension(this.selectedRelease)
|
||||
} else {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Invoice tracking lost!'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
unsubscribeFromPaylinkWs() {
|
||||
try {
|
||||
this.paylinkWebsocket && this.paylinkWebsocket.close()
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
},
|
||||
|
||||
hasNewVersion: function (extension) {
|
||||
if (extension.installedRelease && extension.latestRelease) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ from lnbits.extension_manager import (
|
|||
ExtensionRelease,
|
||||
InstallableExtension,
|
||||
fetch_github_release_config,
|
||||
fetch_release_payment_info,
|
||||
get_valid_extensions,
|
||||
)
|
||||
from lnbits.helpers import generate_filter_params_openapi, url_for
|
||||
|
|
@ -83,6 +84,7 @@ from ..crud import (
|
|||
delete_wallet,
|
||||
drop_extension_db,
|
||||
get_dbversions,
|
||||
get_installed_extension,
|
||||
get_payments,
|
||||
get_payments_history,
|
||||
get_payments_paginated,
|
||||
|
|
@ -796,7 +798,7 @@ async def api_install_extension(
|
|||
access_token: Optional[str] = Depends(check_access_token),
|
||||
):
|
||||
release = await InstallableExtension.get_extension_release(
|
||||
data.ext_id, data.source_repo, data.archive
|
||||
data.ext_id, data.source_repo, data.archive, data.version
|
||||
)
|
||||
if not release:
|
||||
raise HTTPException(
|
||||
|
|
@ -808,13 +810,17 @@ async def api_install_extension(
|
|||
status_code=HTTPStatus.BAD_REQUEST, detail="Incompatible extension version"
|
||||
)
|
||||
|
||||
release.payment_hash = data.payment_hash
|
||||
ext_info = InstallableExtension(
|
||||
id=data.ext_id, name=data.ext_id, installed_release=release, icon=release.icon
|
||||
)
|
||||
|
||||
ext_info.download_archive()
|
||||
|
||||
try:
|
||||
installed_ext = await get_installed_extension(data.ext_id)
|
||||
ext_info.payments = installed_ext.payments if installed_ext else []
|
||||
|
||||
await ext_info.download_archive()
|
||||
|
||||
ext_info.extract_archive()
|
||||
|
||||
extension = Extension.from_installable_ext(ext_info)
|
||||
|
|
@ -838,7 +844,8 @@ async def api_install_extension(
|
|||
ext_info.nofiy_upgrade()
|
||||
|
||||
return extension
|
||||
|
||||
except AssertionError as e:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, str(e))
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
ext_info.clean_extension_files()
|
||||
|
|
@ -907,6 +914,15 @@ async def get_extension_releases(ext_id: str):
|
|||
ExtensionRelease
|
||||
] = await InstallableExtension.get_extension_releases(ext_id)
|
||||
|
||||
installed_ext = await get_installed_extension(ext_id)
|
||||
if not installed_ext:
|
||||
return extension_releases
|
||||
|
||||
for release in extension_releases:
|
||||
payment_info = installed_ext.find_existing_payment(release.pay_link)
|
||||
if payment_info:
|
||||
release.paid_sats = payment_info.amount
|
||||
|
||||
return extension_releases
|
||||
|
||||
except Exception as ex:
|
||||
|
|
@ -915,6 +931,40 @@ async def get_extension_releases(ext_id: str):
|
|||
)
|
||||
|
||||
|
||||
@api_router.put("/api/v1/extension/invoice", dependencies=[Depends(check_admin)])
|
||||
async def get_extension_invoice(data: CreateExtension):
|
||||
try:
|
||||
assert data.cost_sats, "A non-zero amount must be specified"
|
||||
release = await InstallableExtension.get_extension_release(
|
||||
data.ext_id, data.source_repo, data.archive, data.version
|
||||
)
|
||||
assert release, "Release not found"
|
||||
assert release.pay_link, "Pay link not found for release"
|
||||
|
||||
payment_info = await fetch_release_payment_info(
|
||||
release.pay_link, data.cost_sats
|
||||
)
|
||||
assert payment_info and payment_info.payment_request, "Cannot request invoice"
|
||||
invoice = bolt11.decode(payment_info.payment_request)
|
||||
|
||||
assert invoice.amount_msat is not None, "Invoic amount is missing"
|
||||
invoice_amount = int(invoice.amount_msat / 1000)
|
||||
assert (
|
||||
invoice_amount == data.cost_sats
|
||||
), f"Wrong invoice amount: {invoice_amount}."
|
||||
assert (
|
||||
payment_info.payment_hash == invoice.payment_hash
|
||||
), "Wroong invoice payment hash"
|
||||
|
||||
return payment_info
|
||||
|
||||
except AssertionError as e:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, str(e))
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot request invoice")
|
||||
|
||||
|
||||
@api_router.get(
|
||||
"/api/v1/extension/release/{org}/{repo}/{tag_name}",
|
||||
dependencies=[Depends(check_admin)],
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import zipfile
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import Any, List, NamedTuple, Optional, Tuple
|
||||
from urllib import request
|
||||
|
||||
import httpx
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
from packaging import version
|
||||
from pydantic import BaseModel
|
||||
|
|
@ -33,6 +32,7 @@ class ExplicitRelease(BaseModel):
|
|||
warning: Optional[str]
|
||||
info_notification: Optional[str]
|
||||
critical_notification: Optional[str]
|
||||
pay_link: Optional[str]
|
||||
|
||||
def is_version_compatible(self):
|
||||
if not self.min_lnbits_version:
|
||||
|
|
@ -78,8 +78,15 @@ class ExtensionConfig(BaseModel):
|
|||
return version_parse(self.min_lnbits_version) <= version_parse(settings.version)
|
||||
|
||||
|
||||
class ReleasePaymentInfo(BaseModel):
|
||||
amount: Optional[int] = None
|
||||
pay_link: Optional[str] = None
|
||||
payment_hash: Optional[str] = None
|
||||
payment_request: Optional[str] = None
|
||||
|
||||
|
||||
def download_url(url, save_path):
|
||||
with request.urlopen(url) as dl_file:
|
||||
with request.urlopen(url, timeout=60) as dl_file:
|
||||
with open(save_path, "wb") as out_file:
|
||||
out_file.write(dl_file.read())
|
||||
|
||||
|
|
@ -155,6 +162,21 @@ async def github_api_get(url: str, error_msg: Optional[str]) -> Any:
|
|||
return resp.json()
|
||||
|
||||
|
||||
async def fetch_release_payment_info(
|
||||
url: str, amount: Optional[int] = None
|
||||
) -> Optional[ReleasePaymentInfo]:
|
||||
if amount:
|
||||
url = f"{url}?amount={amount}"
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(url)
|
||||
resp.raise_for_status()
|
||||
return ReleasePaymentInfo(**resp.json())
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return None
|
||||
|
||||
|
||||
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
|
||||
if not path:
|
||||
return ""
|
||||
|
|
@ -260,6 +282,26 @@ class ExtensionRelease(BaseModel):
|
|||
repo: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
|
||||
pay_link: Optional[str] = None
|
||||
cost_sats: Optional[int] = None
|
||||
paid_sats: Optional[int] = 0
|
||||
payment_hash: Optional[str] = None
|
||||
|
||||
@property
|
||||
def archive_url(self) -> str:
|
||||
if not self.pay_link:
|
||||
return self.archive
|
||||
return (
|
||||
f"{self.archive}?version=v{self.version}&payment_hash={self.payment_hash}"
|
||||
)
|
||||
|
||||
async def check_payment_requirements(self):
|
||||
if not self.pay_link:
|
||||
return
|
||||
|
||||
payment_info = await fetch_release_payment_info(self.pay_link)
|
||||
self.cost_sats = payment_info.amount if payment_info else None
|
||||
|
||||
@classmethod
|
||||
def from_github_release(
|
||||
cls, source_repo: str, r: "GitHubRepoRelease"
|
||||
|
|
@ -290,12 +332,13 @@ class ExtensionRelease(BaseModel):
|
|||
is_version_compatible=e.is_version_compatible(),
|
||||
warning=e.warning,
|
||||
html_url=e.html_url,
|
||||
pay_link=e.pay_link,
|
||||
repo=e.repo,
|
||||
icon=e.icon,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def all_releases(cls, org: str, repo: str) -> List["ExtensionRelease"]:
|
||||
async def get_github_releases(cls, org: str, repo: str) -> List["ExtensionRelease"]:
|
||||
try:
|
||||
github_releases = await fetch_github_releases(org, repo)
|
||||
return [
|
||||
|
|
@ -318,6 +361,7 @@ class InstallableExtension(BaseModel):
|
|||
featured = False
|
||||
latest_release: Optional[ExtensionRelease] = None
|
||||
installed_release: Optional[ExtensionRelease] = None
|
||||
payments: List[ReleasePaymentInfo] = []
|
||||
archive: Optional[str] = None
|
||||
|
||||
@property
|
||||
|
|
@ -368,30 +412,32 @@ class InstallableExtension(BaseModel):
|
|||
return self.installed_release.version
|
||||
return ""
|
||||
|
||||
def download_archive(self):
|
||||
async def download_archive(self):
|
||||
logger.info(f"Downloading extension {self.name} ({self.installed_version}).")
|
||||
ext_zip_file = self.zip_path
|
||||
if ext_zip_file.is_file():
|
||||
os.remove(ext_zip_file)
|
||||
try:
|
||||
assert self.installed_release, "installed_release is none."
|
||||
download_url(self.installed_release.archive, ext_zip_file)
|
||||
|
||||
self._restore_payment_info()
|
||||
|
||||
await asyncio.to_thread(
|
||||
download_url, self.installed_release.archive_url, ext_zip_file
|
||||
)
|
||||
|
||||
self._remember_payment_info()
|
||||
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Cannot fetch extension archive file",
|
||||
)
|
||||
raise AssertionError("Cannot fetch extension archive file")
|
||||
|
||||
archive_hash = file_hash(ext_zip_file)
|
||||
if self.installed_release.hash and self.installed_release.hash != archive_hash:
|
||||
# remove downloaded archive
|
||||
if ext_zip_file.is_file():
|
||||
os.remove(ext_zip_file)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="File hash missmatch. Will not install.",
|
||||
)
|
||||
raise AssertionError("File hash missmatch. Will not install.")
|
||||
|
||||
def extract_archive(self):
|
||||
logger.info(f"Extracting extension {self.name} ({self.installed_version}).")
|
||||
|
|
@ -468,14 +514,54 @@ class InstallableExtension(BaseModel):
|
|||
if version_parse(self.latest_release.version) < version_parse(release.version):
|
||||
self.latest_release = release
|
||||
|
||||
def find_existing_payment(
|
||||
self, pay_link: Optional[str]
|
||||
) -> Optional[ReleasePaymentInfo]:
|
||||
if not pay_link:
|
||||
return None
|
||||
return next(
|
||||
(p for p in self.payments if p.pay_link == pay_link),
|
||||
None,
|
||||
)
|
||||
|
||||
def _restore_payment_info(self):
|
||||
if not self.installed_release:
|
||||
return
|
||||
if not self.installed_release.pay_link:
|
||||
return
|
||||
if self.installed_release.payment_hash:
|
||||
return
|
||||
payment_info = self.find_existing_payment(self.installed_release.pay_link)
|
||||
if payment_info:
|
||||
self.installed_release.payment_hash = payment_info.payment_hash
|
||||
|
||||
def _remember_payment_info(self):
|
||||
if not self.installed_release or not self.installed_release.pay_link:
|
||||
return
|
||||
payment_info = ReleasePaymentInfo(
|
||||
amount=self.installed_release.cost_sats,
|
||||
pay_link=self.installed_release.pay_link,
|
||||
payment_hash=self.installed_release.payment_hash,
|
||||
)
|
||||
self.payments = [
|
||||
p for p in self.payments if p.pay_link != payment_info.pay_link
|
||||
]
|
||||
self.payments.append(payment_info)
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, data: dict) -> "InstallableExtension":
|
||||
meta = json.loads(data["meta"])
|
||||
ext = InstallableExtension(**data)
|
||||
if "installed_release" in meta:
|
||||
ext.installed_release = ExtensionRelease(**meta["installed_release"])
|
||||
if meta.get("payments"):
|
||||
ext.payments = [ReleasePaymentInfo(**p) for p in meta["payments"]]
|
||||
return ext
|
||||
|
||||
@classmethod
|
||||
def from_rows(cls, rows: List[Any] = []) -> List["InstallableExtension"]:
|
||||
return [InstallableExtension.from_row(row) for row in rows]
|
||||
|
||||
@classmethod
|
||||
async def from_github_release(
|
||||
cls, github_release: GitHubRelease
|
||||
|
|
@ -565,17 +651,19 @@ class InstallableExtension(BaseModel):
|
|||
try:
|
||||
manifest = await fetch_manifest(url)
|
||||
for r in manifest.repos:
|
||||
if r.id == ext_id:
|
||||
repo_releases = await ExtensionRelease.all_releases(
|
||||
r.organisation, r.repository
|
||||
)
|
||||
extension_releases += repo_releases
|
||||
if r.id != ext_id:
|
||||
continue
|
||||
repo_releases = await ExtensionRelease.get_github_releases(
|
||||
r.organisation, r.repository
|
||||
)
|
||||
extension_releases += repo_releases
|
||||
|
||||
for e in manifest.extensions:
|
||||
if e.id == ext_id:
|
||||
extension_releases += [
|
||||
ExtensionRelease.from_explicit_release(url, e)
|
||||
]
|
||||
if e.id != ext_id:
|
||||
continue
|
||||
explicit_release = ExtensionRelease.from_explicit_release(url, e)
|
||||
await explicit_release.check_payment_requirements()
|
||||
extension_releases.append(explicit_release)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Manifest {url} failed with '{str(e)}'")
|
||||
|
|
@ -584,7 +672,7 @@ class InstallableExtension(BaseModel):
|
|||
|
||||
@classmethod
|
||||
async def get_extension_release(
|
||||
cls, ext_id: str, source_repo: str, archive: str
|
||||
cls, ext_id: str, source_repo: str, archive: str, version: str
|
||||
) -> Optional["ExtensionRelease"]:
|
||||
all_releases: List[
|
||||
ExtensionRelease
|
||||
|
|
@ -592,7 +680,9 @@ class InstallableExtension(BaseModel):
|
|||
selected_release = [
|
||||
r
|
||||
for r in all_releases
|
||||
if r.archive == archive and r.source_repo == source_repo
|
||||
if r.archive == archive
|
||||
and r.source_repo == source_repo
|
||||
and r.version == version
|
||||
]
|
||||
|
||||
return selected_release[0] if len(selected_release) != 0 else None
|
||||
|
|
@ -602,6 +692,9 @@ class CreateExtension(BaseModel):
|
|||
ext_id: str
|
||||
archive: str
|
||||
source_repo: str
|
||||
version: str
|
||||
cost_sats: Optional[int] = 0
|
||||
payment_hash: Optional[str] = None
|
||||
|
||||
|
||||
def get_valid_extensions(include_deactivated: Optional[bool] = True) -> List[Extension]:
|
||||
|
|
|
|||
18
lnbits/static/bundle.min.js
vendored
18
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -239,5 +239,12 @@ This service is in BETA. LNbits holds no responsibility for loss of access to fu
|
|||
logout: 'Logout',
|
||||
look_and_feel: 'Look and Feel',
|
||||
language: 'Language',
|
||||
color_scheme: 'Color Scheme'
|
||||
color_scheme: 'Color Scheme',
|
||||
extension_cost: 'This release requires a payment of minimum %{cost} sats.',
|
||||
extension_paid_sats: 'You have already paid %{paid_sats} sats.',
|
||||
release_details_error: 'Cannot get the release details.',
|
||||
pay_from_wallet: 'Pay from Wallet',
|
||||
show_qr: 'Show QR',
|
||||
retry_install: 'Retry Install',
|
||||
new_payment: 'Make New Payment'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// update cache version every time there is a new deployment
|
||||
// so the service worker reinitializes the cache
|
||||
const CACHE_VERSION = 117
|
||||
const CACHE_VERSION = 121
|
||||
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
|
||||
|
||||
const getApiKey = request => {
|
||||
|
|
|
|||
907
package-lock.json
generated
907
package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue