Merge branch 'main' of githubblackcoffeexbt:blackcoffeexbt/lnbits-legend

This commit is contained in:
Black Coffee 2022-11-30 14:10:45 +00:00
commit be9b4bf5b8
20 changed files with 663 additions and 127 deletions

View file

@ -12,7 +12,7 @@ from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
import async_timeout import async_timeout
import httpx import httpx
import pyqrcode import pyqrcode
from fastapi import Depends, Header, Query, Request from fastapi import Depends, Header, Query, Request, Response
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from fastapi.params import Body from fastapi.params import Body
from loguru import logger from loguru import logger
@ -584,8 +584,8 @@ class DecodePayment(BaseModel):
data: str data: str
@core_app.post("/api/v1/payments/decode") @core_app.post("/api/v1/payments/decode", status_code=HTTPStatus.OK)
async def api_payments_decode(data: DecodePayment): async def api_payments_decode(data: DecodePayment, response: Response):
payment_str = data.data payment_str = data.data
try: try:
if payment_str[:5] == "LNURL": if payment_str[:5] == "LNURL":
@ -606,6 +606,7 @@ async def api_payments_decode(data: DecodePayment):
"min_final_cltv_expiry": invoice.min_final_cltv_expiry, "min_final_cltv_expiry": invoice.min_final_cltv_expiry,
} }
except: except:
response.status_code = HTTPStatus.BAD_REQUEST
return {"message": "Failed to decode"} return {"message": "Failed to decode"}

View file

@ -1,6 +1,7 @@
import asyncio import asyncio
import datetime import datetime
import os import os
import re
import time import time
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Optional from typing import Optional
@ -73,18 +74,39 @@ class Connection(Compat):
query = query.replace("?", "%s") query = query.replace("?", "%s")
return query return query
def rewrite_values(self, values):
# strip html
CLEANR = re.compile("<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});")
def cleanhtml(raw_html):
if isinstance(raw_html, str):
cleantext = re.sub(CLEANR, "", raw_html)
return cleantext
else:
return raw_html
# tuple to list and back to tuple
values = tuple([cleanhtml(l) for l in list(values)])
return values
async def fetchall(self, query: str, values: tuple = ()) -> list: async def fetchall(self, query: str, values: tuple = ()) -> list:
result = await self.conn.execute(self.rewrite_query(query), values) result = await self.conn.execute(
self.rewrite_query(query), self.rewrite_values(values)
)
return await result.fetchall() return await result.fetchall()
async def fetchone(self, query: str, values: tuple = ()): async def fetchone(self, query: str, values: tuple = ()):
result = await self.conn.execute(self.rewrite_query(query), values) result = await self.conn.execute(
self.rewrite_query(query), self.rewrite_values(values)
)
row = await result.fetchone() row = await result.fetchone()
await result.close() await result.close()
return row return row
async def execute(self, query: str, values: tuple = ()): async def execute(self, query: str, values: tuple = ()):
return await self.conn.execute(self.rewrite_query(query), values) return await self.conn.execute(
self.rewrite_query(query), self.rewrite_values(values)
)
class Database(Compat): class Database(Compat):

View file

@ -8,7 +8,7 @@ async def m001_initial(db):
id {db.serial_primary_key}, id {db.serial_primary_key},
wallet TEXT NOT NULL, wallet TEXT NOT NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
amount INTEGER NOT NULL, amount {db.big_int} NOT NULL,
served_meta INTEGER NOT NULL, served_meta INTEGER NOT NULL,
served_pr INTEGER NOT NULL served_pr INTEGER NOT NULL
); );

View file

@ -22,7 +22,7 @@ async def m001_initial(db):
description TEXT NOT NULL, description TEXT NOT NULL,
image TEXT, -- image/png;base64,... image TEXT, -- image/png;base64,...
enabled BOOLEAN NOT NULL DEFAULT true, enabled BOOLEAN NOT NULL DEFAULT true,
price INTEGER NOT NULL, price {db.big_int} NOT NULL,
unit TEXT NOT NULL DEFAULT 'sat' unit TEXT NOT NULL DEFAULT 'sat'
); );
""" """

View file

@ -3,14 +3,14 @@ async def m001_initial(db):
Initial paywalls table. Initial paywalls table.
""" """
await db.execute( await db.execute(
""" f"""
CREATE TABLE paywall.paywalls ( CREATE TABLE paywall.paywalls (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
wallet TEXT NOT NULL, wallet TEXT NOT NULL,
secret TEXT NOT NULL, secret TEXT NOT NULL,
url TEXT NOT NULL, url TEXT NOT NULL,
memo TEXT NOT NULL, memo TEXT NOT NULL,
amount INTEGER NOT NULL, amount {db.big_int} NOT NULL,
time TIMESTAMP NOT NULL DEFAULT """ time TIMESTAMP NOT NULL DEFAULT """
+ db.timestamp_now + db.timestamp_now
+ """ + """
@ -25,14 +25,14 @@ async def m002_redux(db):
""" """
await db.execute("ALTER TABLE paywall.paywalls RENAME TO paywalls_old") await db.execute("ALTER TABLE paywall.paywalls RENAME TO paywalls_old")
await db.execute( await db.execute(
""" f"""
CREATE TABLE paywall.paywalls ( CREATE TABLE paywall.paywalls (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
wallet TEXT NOT NULL, wallet TEXT NOT NULL,
url TEXT NOT NULL, url TEXT NOT NULL,
memo TEXT NOT NULL, memo TEXT NOT NULL,
description TEXT NULL, description TEXT NULL,
amount INTEGER DEFAULT 0, amount {db.big_int} DEFAULT 0,
time TIMESTAMP NOT NULL DEFAULT """ time TIMESTAMP NOT NULL DEFAULT """
+ db.timestamp_now + db.timestamp_now
+ """, + """,

View file

@ -3,14 +3,14 @@ async def m001_initial(db):
Creates an improved satsdice table and migrates the existing data. Creates an improved satsdice table and migrates the existing data.
""" """
await db.execute( await db.execute(
""" f"""
CREATE TABLE satsdice.satsdice_pay ( CREATE TABLE satsdice.satsdice_pay (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
wallet TEXT, wallet TEXT,
title TEXT, title TEXT,
min_bet INTEGER, min_bet INTEGER,
max_bet INTEGER, max_bet INTEGER,
amount INTEGER DEFAULT 0, amount {db.big_int} DEFAULT 0,
served_meta INTEGER NOT NULL, served_meta INTEGER NOT NULL,
served_pr INTEGER NOT NULL, served_pr INTEGER NOT NULL,
multiplier FLOAT, multiplier FLOAT,
@ -28,11 +28,11 @@ async def m002_initial(db):
Creates an improved satsdice table and migrates the existing data. Creates an improved satsdice table and migrates the existing data.
""" """
await db.execute( await db.execute(
""" f"""
CREATE TABLE satsdice.satsdice_withdraw ( CREATE TABLE satsdice.satsdice_withdraw (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
satsdice_pay TEXT, satsdice_pay TEXT,
value INTEGER DEFAULT 1, value {db.big_int} DEFAULT 1,
unique_hash TEXT UNIQUE, unique_hash TEXT UNIQUE,
k1 TEXT, k1 TEXT,
open_time INTEGER, open_time INTEGER,
@ -47,11 +47,11 @@ async def m003_initial(db):
Creates an improved satsdice table and migrates the existing data. Creates an improved satsdice table and migrates the existing data.
""" """
await db.execute( await db.execute(
""" f"""
CREATE TABLE satsdice.satsdice_payment ( CREATE TABLE satsdice.satsdice_payment (
payment_hash TEXT PRIMARY KEY, payment_hash TEXT PRIMARY KEY,
satsdice_pay TEXT, satsdice_pay TEXT,
value INTEGER, value {db.big_int},
paid BOOL DEFAULT FALSE, paid BOOL DEFAULT FALSE,
lost BOOL DEFAULT FALSE lost BOOL DEFAULT FALSE
); );

View file

@ -2,7 +2,5 @@
"name": "SatsPay Server", "name": "SatsPay Server",
"short_description": "Create onchain and LN charges", "short_description": "Create onchain and LN charges",
"icon": "payment", "icon": "payment",
"contributors": [ "contributors": ["arcbtc"]
"arcbtc"
]
} }

View file

@ -10,7 +10,7 @@ from lnbits.helpers import urlsafe_short_hash
from ..watchonly.crud import get_config, get_fresh_address from ..watchonly.crud import get_config, get_fresh_address
from . import db from . import db
from .helpers import fetch_onchain_balance from .helpers import fetch_onchain_balance
from .models import Charges, CreateCharge from .models import Charges, CreateCharge, SatsPayThemes
###############CHARGES########################## ###############CHARGES##########################
@ -53,9 +53,10 @@ async def create_charge(user: str, data: CreateCharge) -> Charges:
time, time,
amount, amount,
balance, balance,
extra extra,
custom_css
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
charge_id, charge_id,
@ -73,6 +74,7 @@ async def create_charge(user: str, data: CreateCharge) -> Charges:
data.amount, data.amount,
0, 0,
data.extra, data.extra,
data.custom_css,
), ),
) )
return await get_charge(charge_id) return await get_charge(charge_id)
@ -121,3 +123,101 @@ async def check_address_balance(charge_id: str) -> Optional[Charges]:
if invoice_status["paid"]: if invoice_status["paid"]:
return await update_charge(charge_id=charge_id, balance=charge.amount) return await update_charge(charge_id=charge_id, balance=charge.amount)
return await get_charge(charge_id) return await get_charge(charge_id)
################## SETTINGS ###################
async def save_theme(data: SatsPayThemes, css_id: str = None):
# insert or update
if css_id:
await db.execute(
"""
UPDATE satspay.themes SET custom_css = ?, title = ? WHERE css_id = ?
""",
(data.custom_css, data.title, css_id),
)
else:
css_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO satspay.themes (
css_id,
title,
user,
custom_css
)
VALUES (?, ?, ?, ?)
""",
(
css_id,
data.title,
data.user,
data.custom_css,
),
)
return await get_theme(css_id)
async def get_theme(css_id: str) -> SatsPayThemes:
row = await db.fetchone("SELECT * FROM satspay.themes WHERE css_id = ?", (css_id,))
return SatsPayThemes.from_row(row) if row else None
async def get_themes(user_id: str) -> List[SatsPayThemes]:
rows = await db.fetchall(
"""SELECT * FROM satspay.themes WHERE "user" = ? ORDER BY "timestamp" DESC """,
(user_id,),
)
return await get_config(row.user)
################## SETTINGS ###################
async def save_theme(data: SatsPayThemes, css_id: str = None):
# insert or update
if css_id:
await db.execute(
"""
UPDATE satspay.themes SET custom_css = ?, title = ? WHERE css_id = ?
""",
(data.custom_css, data.title, css_id),
)
else:
css_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO satspay.themes (
css_id,
title,
"user",
custom_css
)
VALUES (?, ?, ?, ?)
""",
(
css_id,
data.title,
data.user,
data.custom_css,
),
)
return await get_theme(css_id)
async def get_theme(css_id: str) -> SatsPayThemes:
row = await db.fetchone("SELECT * FROM satspay.themes WHERE css_id = ?", (css_id,))
return SatsPayThemes.from_row(row) if row else None
async def get_themes(user_id: str) -> List[SatsPayThemes]:
rows = await db.fetchall(
"""SELECT * FROM satspay.themes WHERE "user" = ? ORDER BY "title" DESC """,
(user_id,),
)
return [SatsPayThemes.from_row(row) for row in rows]
async def delete_theme(theme_id: str) -> None:
await db.execute("DELETE FROM satspay.themes WHERE css_id = ?", (theme_id,))

View file

@ -19,10 +19,12 @@ def public_charge(charge: Charges):
"time_elapsed": charge.time_elapsed, "time_elapsed": charge.time_elapsed,
"time_left": charge.time_left, "time_left": charge.time_left,
"paid": charge.paid, "paid": charge.paid,
"custom_css": charge.custom_css,
} }
if charge.paid: if charge.paid:
c["completelink"] = charge.completelink c["completelink"] = charge.completelink
c["completelinktext"] = charge.completelinktext
return c return c

View file

@ -4,7 +4,7 @@ async def m001_initial(db):
""" """
await db.execute( await db.execute(
""" f"""
CREATE TABLE satspay.charges ( CREATE TABLE satspay.charges (
id TEXT NOT NULL PRIMARY KEY, id TEXT NOT NULL PRIMARY KEY,
"user" TEXT, "user" TEXT,
@ -18,8 +18,8 @@ async def m001_initial(db):
completelink TEXT, completelink TEXT,
completelinktext TEXT, completelinktext TEXT,
time INTEGER, time INTEGER,
amount INTEGER, amount {db.big_int},
balance INTEGER DEFAULT 0, balance {db.big_int} DEFAULT 0,
timestamp TIMESTAMP NOT NULL DEFAULT """ timestamp TIMESTAMP NOT NULL DEFAULT """
+ db.timestamp_now + db.timestamp_now
+ """ + """
@ -37,3 +37,28 @@ async def m002_add_charge_extra_data(db):
ADD COLUMN extra TEXT DEFAULT '{"mempool_endpoint": "https://mempool.space", "network": "Mainnet"}'; ADD COLUMN extra TEXT DEFAULT '{"mempool_endpoint": "https://mempool.space", "network": "Mainnet"}';
""" """
) )
async def m003_add_themes_table(db):
"""
Themes table
"""
await db.execute(
"""
CREATE TABLE satspay.themes (
css_id TEXT NOT NULL PRIMARY KEY,
"user" TEXT,
title TEXT,
custom_css TEXT
);
"""
)
async def m004_add_custom_css_to_charges(db):
"""
Add custom css option column to the 'charges' table
"""
await db.execute("ALTER TABLE satspay.charges ADD COLUMN custom_css TEXT;")

View file

@ -14,6 +14,7 @@ class CreateCharge(BaseModel):
webhook: str = Query(None) webhook: str = Query(None)
completelink: str = Query(None) completelink: str = Query(None)
completelinktext: str = Query(None) completelinktext: str = Query(None)
custom_css: Optional[str]
time: int = Query(..., ge=1) time: int = Query(..., ge=1)
amount: int = Query(..., ge=1) amount: int = Query(..., ge=1)
extra: str = "{}" extra: str = "{}"
@ -38,6 +39,7 @@ class Charges(BaseModel):
completelink: Optional[str] completelink: Optional[str]
completelinktext: Optional[str] = "Back to Merchant" completelinktext: Optional[str] = "Back to Merchant"
extra: str = "{}" extra: str = "{}"
custom_css: Optional[str]
time: int time: int
amount: int amount: int
balance: int balance: int
@ -72,3 +74,14 @@ class Charges(BaseModel):
def must_call_webhook(self): def must_call_webhook(self):
return self.webhook and self.paid and self.config.webhook_success == False return self.webhook and self.paid and self.config.webhook_success == False
class SatsPayThemes(BaseModel):
css_id: str = Query(None)
title: str = Query(None)
custom_css: str = Query(None)
user: Optional[str]
@classmethod
def from_row(cls, row: Row) -> "SatsPayThemes":
return cls(**dict(row))

View file

@ -26,5 +26,10 @@ const mapCharge = (obj, oldObj = {}) => {
return charge return charge
} }
const mapCSS = (obj, oldObj = {}) => {
const theme = _.clone(obj)
return theme
}
const minutesToTime = min => const minutesToTime = min =>
min > 0 ? new Date(min * 1000).toISOString().substring(14, 19) : '' min > 0 ? new Date(min * 1000).toISOString().substring(14, 19) : ''

View file

@ -5,7 +5,13 @@
WatchOnly extension, we highly reccomend using a fresh extended public Key WatchOnly extension, we highly reccomend using a fresh extended public Key
specifically for SatsPayServer!<br /> specifically for SatsPayServer!<br />
<small> <small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small Created by, <a href="https://github.com/benarc">Ben Arc</a>,
<a
target="_blank"
style="color: unset"
href="https://github.com/motorina0"
>motorina0</a
></small
> >
</p> </p>
<br /> <br />

View file

@ -174,7 +174,7 @@
<div class="col text-center"> <div class="col text-center">
<q-btn <q-btn
outline outline
v-if="charge.webhook" v-if="charge.completelink"
type="a" type="a"
:href="charge.completelink" :href="charge.completelink"
:label="charge.completelinktext" :label="charge.completelinktext"
@ -297,7 +297,17 @@
</div> </div>
<div class="col-lg- 4 col-md-3 col-sm-1"></div> <div class="col-lg- 4 col-md-3 col-sm-1"></div>
</div> </div>
{% endblock %} {% block styles %}
<link
href="/satspay/css/{{ charge_data.custom_css }}"
rel="stylesheet"
type="text/css"
/>
<style>
header button.q-btn-dropdown {
display: none;
}
</style>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script src="https://mempool.space/mempool.js"></script> <script src="https://mempool.space/mempool.js"></script>
@ -438,6 +448,10 @@
} }
}, },
created: async function () { created: async function () {
// Remove a user defined theme
if (this.charge.custom_css) {
document.body.setAttribute('data-theme', '')
}
if (this.charge.payment_request) this.payInvoice() if (this.charge.payment_request) this.payInvoice()
else this.payOnchain() else this.payOnchain()

View file

@ -8,6 +8,26 @@
<q-btn unelevated color="primary" @click="formDialogCharge.show = true" <q-btn unelevated color="primary" @click="formDialogCharge.show = true"
>New charge >New charge
</q-btn> </q-btn>
<q-btn
v-if="admin == 'True'"
unelevated
color="primary"
@click="getThemes();formDialogThemes.show = true"
>New CSS Theme
</q-btn>
<q-btn
v-else
disable
unelevated
color="primary"
@click="getThemes();formDialogThemes.show = true"
>New CSS Theme
<q-tooltip
>For security reason, custom css is only available to server
admins.</q-tooltip
></q-btn
>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -259,6 +279,63 @@
</q-table> </q-table>
</q-card-section> </q-card-section>
</q-card> </q-card>
<q-card v-if="admin == 'True'">
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Themes</h5>
</div>
</div>
<q-table
dense
flat
:data="themeLinks"
row-key="id"
:columns="customCSSTable.columns"
:pagination.sync="customCSSTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="updateformDialog(props.row.css_id)"
icon="edit"
color="light-blue"
></q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteTheme(props.row.css_id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div> </div>
<div class="col-12 col-md-5 q-gutter-y-md"> <div class="col-12 col-md-5 q-gutter-y-md">
@ -303,32 +380,6 @@
> >
</q-input> </q-input>
<q-input
filled
dense
v-model.trim="formDialogCharge.data.webhook"
type="url"
label="Webhook (URL to send transaction data to once paid)"
>
</q-input>
<q-input
filled
dense
v-model.trim="formDialogCharge.data.completelink"
type="url"
label="Completed button URL"
>
</q-input>
<q-input
filled
dense
v-model.trim="formDialogCharge.data.completelinktext"
type="text"
label="Completed button text (ie 'Back to merchant')"
>
</q-input>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div v-if="walletLinks.length > 0"> <div v-if="walletLinks.length > 0">
@ -377,6 +428,52 @@
label="Wallet *" label="Wallet *"
> >
</q-select> </q-select>
<q-toggle
v-model="showAdvanced"
label="Show advanced options"
></q-toggle>
<div v-if="showAdvanced" class="row">
<div class="col">
<q-input
filled
dense
v-model.trim="formDialogCharge.data.webhook"
type="url"
label="Webhook (URL to send transaction data to once paid)"
class="q-mt-lg"
>
</q-input>
<q-input
filled
dense
v-model.trim="formDialogCharge.data.completelink"
type="url"
label="Completed button URL"
class="q-mt-lg"
>
</q-input>
<q-input
filled
dense
v-model.trim="formDialogCharge.data.completelinktext"
type="text"
label="Completed button text (ie 'Back to merchant')"
class="q-mt-lg"
>
</q-input>
<q-select
filled
dense
emit-value
v-model="formDialogCharge.data.custom_css"
:options="themeOptions"
label="Custom CSS theme (optional)"
class="q-mt-lg"
>
</q-select>
</div>
</div>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn <q-btn
unelevated unelevated
@ -394,6 +491,43 @@
</q-form> </q-form>
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-dialog v-model="formDialogThemes.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormDataThemes" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialogThemes.data.title"
type="text"
label="*Title"
></q-input>
<q-input
filled
dense
v-model.trim="formDialogThemes.data.custom_css"
type="textarea"
label="Custom CSS"
>
</q-input>
<div class="row q-mt-lg">
<q-btn
v-if="formDialogThemes.data.css_id"
unelevated
color="primary"
type="submit"
>Update CSS theme</q-btn
>
<q-btn v-else unelevated color="primary" type="submit"
>Save CSS theme</q-btn
>
<q-btn @click="cancelThemes" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div> </div>
{% endblock %} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block scripts %} {{ window_vars(user) }}
<!-- lnbits/static/vendor <!-- lnbits/static/vendor
@ -410,16 +544,21 @@
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
return { return {
settings: {},
filter: '', filter: '',
admin: '{{ admin }}',
balance: null, balance: null,
walletLinks: [], walletLinks: [],
chargeLinks: [], chargeLinks: [],
onchainwallet: null, themeLinks: [],
themeOptions: [],
onchainwallet: '',
rescanning: false, rescanning: false,
mempool: { mempool: {
endpoint: '', endpoint: '',
network: 'Mainnet' network: 'Mainnet'
}, },
showAdvanced: false,
chargesTable: { chargesTable: {
columns: [ columns: [
@ -494,7 +633,25 @@
rowsPerPage: 10 rowsPerPage: 10
} }
}, },
customCSSTable: {
columns: [
{
name: 'css_id',
align: 'left',
label: 'ID',
field: 'css_id'
},
{
name: 'title',
align: 'left',
label: 'Title',
field: 'title'
}
],
pagination: {
rowsPerPage: 10
}
},
formDialogCharge: { formDialogCharge: {
show: false, show: false,
data: { data: {
@ -502,13 +659,24 @@
onchainwallet: '', onchainwallet: '',
lnbits: false, lnbits: false,
description: '', description: '',
custom_css: '',
time: null, time: null,
amount: null amount: null
} }
},
formDialogThemes: {
show: false,
data: {
custom_css: ''
}
} }
} }
}, },
methods: { methods: {
cancelThemes: function (data) {
this.formDialogCharge.data.custom_css = ''
this.formDialogThemes.show = false
},
cancelCharge: function (data) { cancelCharge: function (data) {
this.formDialogCharge.data.description = '' this.formDialogCharge.data.description = ''
this.formDialogCharge.data.onchain = false this.formDialogCharge.data.onchain = false
@ -517,6 +685,7 @@
this.formDialogCharge.data.time = null this.formDialogCharge.data.time = null
this.formDialogCharge.data.amount = null this.formDialogCharge.data.amount = null
this.formDialogCharge.data.webhook = '' this.formDialogCharge.data.webhook = ''
this.formDialogCharge.data.custom_css = ''
this.formDialogCharge.data.completelink = '' this.formDialogCharge.data.completelink = ''
this.formDialogCharge.show = false this.formDialogCharge.show = false
}, },
@ -580,9 +749,39 @@
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
} }
}, },
sendFormDataCharge: function () {
getThemes: async function () {
try {
const {data} = await LNbits.api.request(
'GET',
'/satspay/api/v1/themes',
this.g.user.wallets[0].inkey
)
console.log(data)
this.themeLinks = data.map(c =>
mapCSS(
c,
this.themeLinks.find(old => old.css_id === c.css_id)
)
)
this.themeOptions = data.map(w => ({
id: w.css_id,
label: w.title + ' - ' + w.css_id
}))
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
sendFormDataThemes: function () {
const wallet = this.g.user.wallets[0].inkey const wallet = this.g.user.wallets[0].inkey
const data = this.formDialogThemes.data
this.createTheme(wallet, data)
},
sendFormDataCharge: function () {
this.formDialogCharge.data.custom_css = this.formDialogCharge.data.custom_css?.id
const data = this.formDialogCharge.data const data = this.formDialogCharge.data
const wallet = this.g.user.wallets[0].inkey
data.amount = parseInt(data.amount) data.amount = parseInt(data.amount)
data.time = parseInt(data.time) data.time = parseInt(data.time)
data.lnbitswallet = data.lnbits ? data.lnbitswallet : null data.lnbitswallet = data.lnbits ? data.lnbitswallet : null
@ -651,6 +850,68 @@
this.rescanning = false this.rescanning = false
} }
}, },
updateformDialog: function (themeId) {
const theme = _.findWhere(this.themeLinks, {css_id: themeId})
console.log(theme.css_id)
this.formDialogThemes.data.css_id = theme.css_id
this.formDialogThemes.data.title = theme.title
this.formDialogThemes.data.custom_css = theme.custom_css
this.formDialogThemes.show = true
},
createTheme: async function (wallet, data) {
console.log(data.css_id)
try {
if (data.css_id) {
const resp = await LNbits.api.request(
'POST',
'/satspay/api/v1/themes/' + data.css_id,
wallet,
data
)
this.themeLinks = _.reject(this.themeLinks, function (obj) {
return obj.css_id === data.css_id
})
this.themeLinks.unshift(mapCSS(resp.data))
} else {
const resp = await LNbits.api.request(
'POST',
'/satspay/api/v1/themes',
wallet,
data
)
this.themeLinks.unshift(mapCSS(resp.data))
}
this.formDialogThemes.show = false
this.formDialogThemes.data = {
title: '',
custom_css: ''
}
} catch (error) {
console.log('cun')
LNbits.utils.notifyApiError(error)
}
},
deleteTheme: function (themeId) {
const theme = _.findWhere(this.themeLinks, {id: themeId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this theme?')
.onOk(async () => {
try {
const response = await LNbits.api.request(
'DELETE',
'/satspay/api/v1/themes/' + themeId,
this.g.user.wallets[0].adminkey
)
this.themeLinks = _.reject(this.themeLinks, function (obj) {
return obj.css_id === themeId
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
})
},
createCharge: async function (wallet, data) { createCharge: async function (wallet, data) {
try { try {
const resp = await LNbits.api.request( const resp = await LNbits.api.request(
@ -703,6 +964,9 @@
} }
}, },
created: async function () { created: async function () {
if (this.admin == 'True') {
await this.getThemes()
}
await this.getCharges() await this.getCharges()
await this.getWalletConfig() await this.getWalletConfig()
await this.getWalletLinks() await this.getWalletLinks()

View file

@ -1,7 +1,10 @@
import json
from http import HTTPStatus from http import HTTPStatus
from fastapi import Response
from fastapi.param_functions import Depends from fastapi.param_functions import Depends
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from loguru import logger
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
@ -9,17 +12,21 @@ from starlette.responses import HTMLResponse
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.decorators import check_user_exists from lnbits.decorators import check_user_exists
from lnbits.extensions.satspay.helpers import public_charge from lnbits.extensions.satspay.helpers import public_charge
from lnbits.settings import LNBITS_ADMIN_USERS
from . import satspay_ext, satspay_renderer from . import satspay_ext, satspay_renderer
from .crud import get_charge from .crud import get_charge, get_theme
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
@satspay_ext.get("/", response_class=HTMLResponse) @satspay_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)): async def index(request: Request, user: User = Depends(check_user_exists)):
admin = False
if LNBITS_ADMIN_USERS and user.id in LNBITS_ADMIN_USERS:
admin = True
return satspay_renderer().TemplateResponse( return satspay_renderer().TemplateResponse(
"satspay/index.html", {"request": request, "user": user.dict()} "satspay/index.html", {"request": request, "user": user.dict(), "admin": admin}
) )
@ -40,3 +47,11 @@ async def display(request: Request, charge_id: str):
"network": charge.config.network, "network": charge.config.network,
}, },
) )
@satspay_ext.get("/css/{css_id}")
async def display(css_id: str, response: Response):
theme = await get_theme(css_id)
if theme:
return Response(content=theme.custom_css, media_type="text/css")
return None

View file

@ -1,9 +1,12 @@
import json import json
from http import HTTPStatus from http import HTTPStatus
import httpx
from fastapi.params import Depends from fastapi.params import Depends
from loguru import logger
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from lnbits.core.crud import get_wallet
from lnbits.decorators import ( from lnbits.decorators import (
WalletTypeInfo, WalletTypeInfo,
get_key_type, get_key_type,
@ -11,17 +14,22 @@ from lnbits.decorators import (
require_invoice_key, require_invoice_key,
) )
from lnbits.extensions.satspay import satspay_ext from lnbits.extensions.satspay import satspay_ext
from lnbits.settings import LNBITS_ADMIN_EXTENSIONS, LNBITS_ADMIN_USERS
from .crud import ( from .crud import (
check_address_balance, check_address_balance,
create_charge, create_charge,
delete_charge, delete_charge,
delete_theme,
get_charge, get_charge,
get_charges, get_charges,
get_theme,
get_themes,
save_theme,
update_charge, update_charge,
) )
from .helpers import call_webhook, public_charge from .helpers import call_webhook, public_charge
from .models import CreateCharge from .models import CreateCharge, SatsPayThemes
#############################CHARGES########################## #############################CHARGES##########################
@ -126,3 +134,49 @@ async def api_charge_balance(charge_id):
await update_charge(charge_id=charge.id, extra=json.dumps(extra)) await update_charge(charge_id=charge.id, extra=json.dumps(extra))
return {**public_charge(charge)} return {**public_charge(charge)}
#############################THEMES##########################
@satspay_ext.post("/api/v1/themes")
@satspay_ext.post("/api/v1/themes/{css_id}")
async def api_themes_save(
data: SatsPayThemes,
wallet: WalletTypeInfo = Depends(require_invoice_key),
css_id: str = None,
):
if LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Only server admins can create themes.",
)
if css_id:
theme = await save_theme(css_id=css_id, data=data)
else:
data.user = wallet.wallet.user
theme = await save_theme(data=data)
return theme
@satspay_ext.get("/api/v1/themes")
async def api_themes_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
try:
return await get_themes(wallet.wallet.user)
except HTTPException:
logger.error("Error loading satspay themes")
logger.error(HTTPException)
return ""
@satspay_ext.delete("/api/v1/themes/{theme_id}")
async def api_charge_delete(theme_id, wallet: WalletTypeInfo = Depends(get_key_type)):
theme = await get_theme(theme_id)
if not theme:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Theme does not exist."
)
await delete_theme(theme_id)
return "", HTTPStatus.NO_CONTENT

View file

@ -3,25 +3,25 @@ async def m001_initial(db):
Initial wallet table. Initial wallet table.
""" """
await db.execute( await db.execute(
""" f"""
CREATE TABLE watchonly.wallets ( CREATE TABLE watchonly.wallets (
id TEXT NOT NULL PRIMARY KEY, id TEXT NOT NULL PRIMARY KEY,
"user" TEXT, "user" TEXT,
masterpub TEXT NOT NULL, masterpub TEXT NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
address_no INTEGER NOT NULL DEFAULT 0, address_no INTEGER NOT NULL DEFAULT 0,
balance INTEGER NOT NULL balance {db.big_int} NOT NULL
); );
""" """
) )
await db.execute( await db.execute(
""" f"""
CREATE TABLE watchonly.addresses ( CREATE TABLE watchonly.addresses (
id TEXT NOT NULL PRIMARY KEY, id TEXT NOT NULL PRIMARY KEY,
address TEXT NOT NULL, address TEXT NOT NULL,
wallet TEXT NOT NULL, wallet TEXT NOT NULL,
amount INTEGER NOT NULL amount {db.big_int} NOT NULL
); );
""" """
) )

View file

@ -3,13 +3,13 @@ async def m001_initial(db):
Creates an improved withdraw table and migrates the existing data. Creates an improved withdraw table and migrates the existing data.
""" """
await db.execute( await db.execute(
""" f"""
CREATE TABLE withdraw.withdraw_links ( CREATE TABLE withdraw.withdraw_links (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
wallet TEXT, wallet TEXT,
title TEXT, title TEXT,
min_withdrawable INTEGER DEFAULT 1, min_withdrawable {db.big_int} DEFAULT 1,
max_withdrawable INTEGER DEFAULT 1, max_withdrawable {db.big_int} DEFAULT 1,
uses INTEGER DEFAULT 1, uses INTEGER DEFAULT 1,
wait_time INTEGER, wait_time INTEGER,
is_unique INTEGER DEFAULT 0, is_unique INTEGER DEFAULT 0,
@ -28,13 +28,13 @@ async def m002_change_withdraw_table(db):
Creates an improved withdraw table and migrates the existing data. Creates an improved withdraw table and migrates the existing data.
""" """
await db.execute( await db.execute(
""" f"""
CREATE TABLE withdraw.withdraw_link ( CREATE TABLE withdraw.withdraw_link (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
wallet TEXT, wallet TEXT,
title TEXT, title TEXT,
min_withdrawable INTEGER DEFAULT 1, min_withdrawable {db.big_int} DEFAULT 1,
max_withdrawable INTEGER DEFAULT 1, max_withdrawable {db.big_int} DEFAULT 1,
uses INTEGER DEFAULT 1, uses INTEGER DEFAULT 1,
wait_time INTEGER, wait_time INTEGER,
is_unique INTEGER DEFAULT 0, is_unique INTEGER DEFAULT 0,

View file

@ -1,55 +1,72 @@
aiofiles==0.8.0 aiofiles==0.8.0 ; python_version >= "3.7" and python_version < "4.0"
anyio==3.6.1 anyio==3.6.1 ; python_version >= "3.7" and python_version < "4.0"
asyncio==3.4.3 asgiref==3.4.1 ; python_version >= "3.7" and python_version < "4.0"
attrs==21.4.0 asn1crypto==1.5.1 ; python_version >= "3.7" and python_version < "4.0"
bech32==1.2.0 async-timeout==4.0.2 ; python_version >= "3.7" and python_version < "4.0"
bitstring==3.1.9 attrs==21.2.0 ; python_version >= "3.7" and python_version < "4.0"
cerberus==1.3.4 base58==2.1.1 ; python_version >= "3.7" and python_version < "4.0"
certifi==2022.6.15 bech32==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
cffi==1.15.0 bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0"
click==8.1.3 cerberus==1.3.4 ; python_version >= "3.7" and python_version < "4.0"
ecdsa==0.18.0 certifi==2021.5.30 ; python_version >= "3.7" and python_version < "4.0"
embit==0.5.0 cffi==1.15.0 ; python_version >= "3.7" and python_version < "4.0"
environs==9.5.0 charset-normalizer==2.0.6 ; python_version >= "3.7" and python_version < "4.0"
fastapi==0.79.0 click==8.0.1 ; python_version >= "3.7" and python_version < "4.0"
h11==0.12.0 coincurve==17.0.0 ; python_version >= "3.7" and python_version < "4.0"
httpcore==0.15.0 colorama==0.4.5 ; python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" or python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
httptools==0.4.0 cryptography==36.0.2 ; python_version >= "3.7" and python_version < "4.0"
httpx==0.23.0 ecdsa==0.17.0 ; python_version >= "3.7" and python_version < "4.0"
idna==3.3 embit==0.4.9 ; python_version >= "3.7" and python_version < "4.0"
jinja2==3.0.1 enum34==1.1.10 ; python_version >= "3.7" and python_version < "4.0"
lnurl==0.3.6 environs==9.3.3 ; python_version >= "3.7" and python_version < "4.0"
loguru==0.6.0 fastapi==0.78.0 ; python_version >= "3.7" and python_version < "4.0"
markupsafe==2.1.1 grpcio==1.49.1 ; python_version >= "3.7" and python_version < "4.0"
marshmallow==3.17.0 h11==0.12.0 ; python_version >= "3.7" and python_version < "4.0"
outcome==1.2.0 httpcore==0.15.0 ; python_version >= "3.7" and python_version < "4.0"
packaging==21.3 httptools==0.4.0 ; python_version >= "3.7" and python_version < "4.0"
psycopg2-binary==2.9.3 httpx==0.23.0 ; python_version >= "3.7" and python_version < "4.0"
pycparser==2.21 idna==3.2 ; python_version >= "3.7" and python_version < "4.0"
pycryptodomex==3.15.0 importlib-metadata==4.8.1 ; python_version >= "3.7" and python_version < "4.0"
pydantic==1.9.1 jinja2==3.0.1 ; python_version >= "3.7" and python_version < "4.0"
pyngrok==5.1.0 lnurl==0.3.6 ; python_version >= "3.7" and python_version < "4.0"
pyparsing==3.0.9 loguru==0.5.3 ; python_version >= "3.7" and python_version < "4.0"
pypng==0.20220715.0 markupsafe==2.0.1 ; python_version >= "3.7" and python_version < "4.0"
pyqrcode==1.2.1 marshmallow==3.17.0 ; python_version >= "3.7" and python_version < "4.0"
pyscss==1.4.0 outcome==1.1.0 ; python_version >= "3.7" and python_version < "4.0"
python-dotenv==0.20.0 packaging==21.3 ; python_version >= "3.7" and python_version < "4.0"
pyyaml==6.0 pathlib2==2.3.7.post1 ; python_version >= "3.7" and python_version < "4.0"
represent==1.6.0.post0 protobuf==4.21.7 ; python_version >= "3.7" and python_version < "4.0"
rfc3986==1.5.0 psycopg2-binary==2.9.1 ; python_version >= "3.7" and python_version < "4.0"
secp256k1==0.14.0 pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0"
shortuuid==1.0.9 pycryptodomex==3.14.1 ; python_version >= "3.7" and python_version < "4.0"
six==1.16.0 pydantic==1.8.2 ; python_version >= "3.7" and python_version < "4.0"
sniffio==1.2.0 pyln-bolt7==1.0.246 ; python_version >= "3.7" and python_version < "4.0"
sqlalchemy-aio==0.17.0 pyln-client==0.11.1 ; python_version >= "3.7" and python_version < "4.0"
sqlalchemy==1.3.23 pyln-proto==0.11.1 ; python_version >= "3.7" and python_version < "4.0"
sse-starlette==0.10.3 pyparsing==3.0.9 ; python_version >= "3.7" and python_version < "4.0"
starlette==0.19.1 pypng==0.0.21 ; python_version >= "3.7" and python_version < "4.0"
typing-extensions==4.3.0 pyqrcode==1.2.1 ; python_version >= "3.7" and python_version < "4.0"
uvicorn==0.18.2 pyscss==1.4.0 ; python_version >= "3.7" and python_version < "4.0"
uvloop==0.16.0 pysocks==1.7.1 ; python_version >= "3.7" and python_version < "4.0"
watchfiles==0.16.0 python-dotenv==0.19.0 ; python_version >= "3.7" and python_version < "4.0"
websockets==10.3 pyyaml==5.4.1 ; python_version >= "3.7" and python_version < "4.0"
websocket-client==1.3.3 represent==1.6.0.post0 ; python_version >= "3.7" and python_version < "4.0"
async-timeout==4.0.2 rfc3986==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
setuptools==65.4.0 rfc3986[idna2008]==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
secp256k1==0.14.0 ; python_version >= "3.7" and python_version < "4.0"
setuptools==65.4.1 ; python_version >= "3.7" and python_version < "4.0"
shortuuid==1.0.1 ; python_version >= "3.7" and python_version < "4.0"
six==1.16.0 ; python_version >= "3.7" and python_version < "4.0"
sniffio==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
sqlalchemy-aio==0.17.0 ; python_version >= "3.7" and python_version < "4.0"
sqlalchemy==1.3.23 ; python_version >= "3.7" and python_version < "4.0"
sse-starlette==0.6.2 ; python_version >= "3.7" and python_version < "4.0"
starlette==0.19.1 ; python_version >= "3.7" and python_version < "4.0"
typing-extensions==3.10.0.2 ; python_version >= "3.7" and python_version < "4.0"
uvicorn==0.18.1 ; python_version >= "3.7" and python_version < "4.0"
uvloop==0.16.0 ; python_version >= "3.7" and python_version < "4.0"
watchgod==0.7 ; python_version >= "3.7" and python_version < "4.0"
websocket-client==1.3.3 ; python_version >= "3.7" and python_version < "4.0"
websockets==10.0 ; python_version >= "3.7" and python_version < "4.0"
win32-setctime==1.1.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
zipp==3.5.0 ; python_version >= "3.7" and python_version < "4.0"