mirror of
https://github.com/lnbits/lnbits.git
synced 2025-03-26 17:51:53 +01:00
feat(paywall): extension basics
This commit is contained in:
parent
fb7dfb3a32
commit
768f0427e7
11
lnbits/extensions/paywall/README.md
Normal file
11
lnbits/extensions/paywall/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
<h1>Example Extension</h1>
|
||||
<h2>*tagline*</h2>
|
||||
This is an example extension to help you organise and build you own.
|
||||
|
||||
Try to include an image
|
||||
<img src="https://i.imgur.com/9i4xcQB.png">
|
||||
|
||||
|
||||
<h2>If your extension has API endpoints, include useful ones here</h2>
|
||||
|
||||
<code>curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "Grpc-Metadata-macaroon: YOUR_WALLET-ADMIN/INVOICE-KEY"</code>
|
8
lnbits/extensions/paywall/__init__.py
Normal file
8
lnbits/extensions/paywall/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
paywall_ext = Blueprint("paywall", __name__, static_folder="static", template_folder="templates")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
6
lnbits/extensions/paywall/config.json
Normal file
6
lnbits/extensions/paywall/config.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Paywall",
|
||||
"short_description": "BLah blah blah.",
|
||||
"icon": "vpn_lock",
|
||||
"contributors": ["eillarra"]
|
||||
}
|
44
lnbits/extensions/paywall/crud.py
Normal file
44
lnbits/extensions/paywall/crud.py
Normal file
@ -0,0 +1,44 @@
|
||||
from base64 import urlsafe_b64encode
|
||||
from uuid import uuid4
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
from .models import Paywall
|
||||
|
||||
|
||||
def create_paywall(*, wallet_id: str, url: str, memo: str, amount: int) -> Paywall:
|
||||
with open_ext_db("paywall") as db:
|
||||
paywall_id = urlsafe_b64encode(uuid4().bytes_le).decode('utf-8')
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO paywalls (id, wallet, url, memo, amount)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(paywall_id, wallet_id, url, memo, amount),
|
||||
)
|
||||
|
||||
return get_paywall(paywall_id)
|
||||
|
||||
|
||||
def get_paywall(paywall_id: str) -> Optional[Paywall]:
|
||||
with open_ext_db("paywall") as db:
|
||||
row = db.fetchone("SELECT * FROM paywalls WHERE id = ?", (paywall_id,))
|
||||
|
||||
return Paywall(**row) if row else None
|
||||
|
||||
|
||||
def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("paywall") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM paywalls WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [Paywall(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_paywall(paywall_id: str) -> None:
|
||||
with open_ext_db("paywall") as db:
|
||||
db.execute("DELETE FROM paywalls WHERE id = ?", (paywall_id,))
|
10
lnbits/extensions/paywall/models.py
Normal file
10
lnbits/extensions/paywall/models.py
Normal file
@ -0,0 +1,10 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class Paywall(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
url: str
|
||||
memo: str
|
||||
amount: int
|
||||
time: int
|
8
lnbits/extensions/paywall/schema.sql
Normal file
8
lnbits/extensions/paywall/schema.sql
Normal file
@ -0,0 +1,8 @@
|
||||
CREATE TABLE IF NOT EXISTS paywalls (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
memo TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
28
lnbits/extensions/paywall/templates/paywall/_api_docs.html
Normal file
28
lnbits/extensions/paywall/templates/paywall/_api_docs.html
Normal file
@ -0,0 +1,28 @@
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item group="api" dense expand-separator label="Create a paywall">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="List paywalls">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Delete a paywall">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
222
lnbits/extensions/paywall/templates/paywall/index.html
Normal file
222
lnbits/extensions/paywall/templates/paywall/index.html
Normal file
@ -0,0 +1,222 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
|
||||
|
||||
{% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="paywallDialog.show = true">New Paywall</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Paywalls</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-table dense flat
|
||||
:data="paywalls"
|
||||
row-key="id"
|
||||
:columns="paywallsTable.columns"
|
||||
:pagination.sync="paywallsTable.pagination">
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<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 auto-width>
|
||||
<q-btn unelevated dense size="xs" icon="vpn_lock" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.wall" target="_blank"></q-btn>
|
||||
<q-btn unelevated dense size="xs" icon="link" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.url" target="_blank"></q-btn>
|
||||
</q-td>
|
||||
<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="deletePaywall(props.row.id)" icon="cancel" color="pink"></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits paywall extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "paywall/_api_docs.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="paywallDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-form class="q-gutter-md">
|
||||
<q-select filled dense emit-value v-model="paywallDialog.data.wallet" :options="g.user.walletOptions" label="Wallet *">
|
||||
</q-select>
|
||||
<q-input filled dense
|
||||
v-model.trim="paywallDialog.data.url"
|
||||
type="url"
|
||||
label="Target URL *"></q-input>
|
||||
<q-input filled dense
|
||||
v-model.number="paywallDialog.data.amount"
|
||||
type="number"
|
||||
label="Amount *"></q-input>
|
||||
<q-input filled dense
|
||||
v-model.trim="paywallDialog.data.memo"
|
||||
label="Memo"
|
||||
placeholder="LNbits invoice"></q-input>
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
:disable="paywallDialog.data.amount == null || paywallDialog.data.amount < 0 || paywallDialog.data.url == null"
|
||||
@click="createPaywall">Create paywall</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ window_vars(user) }}
|
||||
<script>
|
||||
var mapPaywall = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm');
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount);
|
||||
obj.wall = ['/paywall/', obj.id].join('');
|
||||
return obj;
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
paywalls: [],
|
||||
paywallsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
|
||||
{name: 'date', align: 'left', label: 'Date', field: 'date', sortable: true},
|
||||
{
|
||||
name: 'amount', align: 'right', label: 'Amount (sat)', field: 'fsat', sortable: true,
|
||||
sort: function (a, b, rowA, rowB) {
|
||||
return rowA.amount - rowB.amount;
|
||||
}
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
paywallDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getPaywalls: function () {
|
||||
var self = this;
|
||||
|
||||
LNbits.api.request(
|
||||
'GET',
|
||||
'/paywall/api/v1/paywalls?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
).then(function (response) {
|
||||
self.paywalls = response.data.map(function (obj) {
|
||||
return mapPaywall(obj);
|
||||
});
|
||||
});
|
||||
},
|
||||
createPaywall: function () {
|
||||
var data = {
|
||||
url: this.paywallDialog.data.url,
|
||||
memo: this.paywallDialog.data.memo,
|
||||
amount: this.paywallDialog.data.amount
|
||||
};
|
||||
var self = this;
|
||||
|
||||
console.log(this.paywallDialog.data.wallet);
|
||||
|
||||
LNbits.api.request(
|
||||
'POST',
|
||||
'/paywall/api/v1/paywalls',
|
||||
_.findWhere(this.g.user.wallets, {id: this.paywallDialog.data.wallet}).inkey,
|
||||
data
|
||||
).then(function (response) {
|
||||
self.paywalls.push(mapPaywall(response.data));
|
||||
self.paywallDialog.show = false;
|
||||
self.paywallDialog.data = {};
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
},
|
||||
deletePaywall: function (paywallId) {
|
||||
var self = this;
|
||||
var paywall = _.findWhere(this.paywalls, {id: paywallId});
|
||||
|
||||
this.$q.dialog({
|
||||
message: 'Are you sure you want to delete this Paywall link?',
|
||||
ok: {
|
||||
flat: true,
|
||||
color: 'orange'
|
||||
},
|
||||
cancel: {
|
||||
flat: true,
|
||||
color: 'grey'
|
||||
}
|
||||
}).onOk(function () {
|
||||
LNbits.api.request(
|
||||
'DELETE',
|
||||
'/paywall/api/v1/paywalls/' + paywallId,
|
||||
_.findWhere(self.g.user.wallets, {id: paywall.wallet}).inkey
|
||||
).then(function (response) {
|
||||
self.paywalls = _.reject(self.paywalls, function (obj) { return obj.id == paywallId; });
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls);
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getPaywalls();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
1
lnbits/extensions/paywall/templates/paywall/wall.html
Normal file
1
lnbits/extensions/paywall/templates/paywall/wall.html
Normal file
@ -0,0 +1 @@
|
||||
{{ paywall.url }}
|
21
lnbits/extensions/paywall/views.py
Normal file
21
lnbits/extensions/paywall/views.py
Normal file
@ -0,0 +1,21 @@
|
||||
from flask import g, abort, render_template
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.extensions.paywall import paywall_ext
|
||||
from lnbits.helpers import Status
|
||||
|
||||
from .crud import get_paywall
|
||||
|
||||
|
||||
@paywall_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def index():
|
||||
return render_template("paywall/index.html", user=g.user)
|
||||
|
||||
|
||||
@paywall_ext.route("/<paywall_id>")
|
||||
def wall(paywall_id):
|
||||
paywall = get_paywall(paywall_id) or abort(Status.NOT_FOUND, "Paywall does not exist.")
|
||||
|
||||
return render_template("paywall/wall.html", paywall=paywall)
|
51
lnbits/extensions/paywall/views_api.py
Normal file
51
lnbits/extensions/paywall/views_api.py
Normal file
@ -0,0 +1,51 @@
|
||||
from flask import g, jsonify, request
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import api_check_wallet_macaroon, api_validate_post_request
|
||||
from lnbits.helpers import Status
|
||||
|
||||
from lnbits.extensions.paywall import paywall_ext
|
||||
from .crud import create_paywall, get_paywall, get_paywalls, delete_paywall
|
||||
|
||||
|
||||
@paywall_ext.route("/api/v1/paywalls", methods=["GET"])
|
||||
@api_check_wallet_macaroon(key_type="invoice")
|
||||
def api_paywalls():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([paywall._asdict() for paywall in get_paywalls(wallet_ids)]), Status.OK
|
||||
|
||||
|
||||
@paywall_ext.route("/api/v1/paywalls", methods=["POST"])
|
||||
@api_check_wallet_macaroon(key_type="invoice")
|
||||
@api_validate_post_request(required_params=["url", "memo", "amount"])
|
||||
def api_paywall_create():
|
||||
if not isinstance(g.data["amount"], int) or g.data["amount"] < 0:
|
||||
return jsonify({"message": "`amount` needs to be a positive integer, or zero."}), Status.BAD_REQUEST
|
||||
|
||||
for var in ["url", "memo"]:
|
||||
if not isinstance(g.data[var], str) or not g.data[var].strip():
|
||||
return jsonify({"message": f"`{var}` needs to be a valid string."}), Status.BAD_REQUEST
|
||||
|
||||
paywall = create_paywall(wallet_id=g.wallet.id, url=g.data["url"], memo=g.data["memo"], amount=g.data["amount"])
|
||||
|
||||
return jsonify(paywall._asdict()), Status.CREATED
|
||||
|
||||
|
||||
@paywall_ext.route("/api/v1/paywalls/<paywall_id>", methods=["DELETE"])
|
||||
@api_check_wallet_macaroon(key_type="invoice")
|
||||
def api_paywall_delete(paywall_id):
|
||||
paywall = get_paywall(paywall_id)
|
||||
|
||||
if not paywall:
|
||||
return jsonify({"message": "Paywall does not exist."}), Status.NOT_FOUND
|
||||
|
||||
if paywall.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your paywall."}), Status.FORBIDDEN
|
||||
|
||||
delete_paywall(paywall_id)
|
||||
|
||||
return '', Status.NO_CONTENT
|
Loading…
x
Reference in New Issue
Block a user