Added frontend pages and started making lnurl
This commit is contained in:
parent
9ec208c434
commit
df5e4cb9db
9 changed files with 330 additions and 98 deletions
|
|
@ -21,9 +21,13 @@ async def create_copilot(
|
||||||
animation1threshold: Optional[int] = None,
|
animation1threshold: Optional[int] = None,
|
||||||
animation2threshold: Optional[int] = None,
|
animation2threshold: Optional[int] = None,
|
||||||
animation3threshold: Optional[int] = None,
|
animation3threshold: Optional[int] = None,
|
||||||
show_message: Optional[str] = None,
|
animation1webhook: Optional[str] = None,
|
||||||
amount: Optional[int] = None,
|
animation2webhook: Optional[str] = None,
|
||||||
|
animation3webhook: Optional[str] = None,
|
||||||
lnurl_title: Optional[str] = None,
|
lnurl_title: Optional[str] = None,
|
||||||
|
show_message: Optional[int] = None,
|
||||||
|
show_ack: Optional[int] = None,
|
||||||
|
amount_made: Optional[int] = None,
|
||||||
) -> Copilots:
|
) -> Copilots:
|
||||||
copilot_id = urlsafe_short_hash()
|
copilot_id = urlsafe_short_hash()
|
||||||
|
|
||||||
|
|
@ -39,11 +43,16 @@ async def create_copilot(
|
||||||
animation1threshold,
|
animation1threshold,
|
||||||
animation2threshold,
|
animation2threshold,
|
||||||
animation3threshold,
|
animation3threshold,
|
||||||
|
animation1webhook,
|
||||||
|
animation2webhook,
|
||||||
|
animation3webhook,
|
||||||
|
lnurl_title,
|
||||||
show_message,
|
show_message,
|
||||||
amount,
|
show_ack,
|
||||||
lnurl_title
|
lnurl_title,
|
||||||
|
amount_made
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
copilot_id,
|
copilot_id,
|
||||||
|
|
@ -55,9 +64,14 @@ async def create_copilot(
|
||||||
animation1threshold,
|
animation1threshold,
|
||||||
animation2threshold,
|
animation2threshold,
|
||||||
animation3threshold,
|
animation3threshold,
|
||||||
|
animation1webhook,
|
||||||
|
animation2webhook,
|
||||||
|
animation3webhook,
|
||||||
|
lnurl_title,
|
||||||
show_message,
|
show_message,
|
||||||
amount,
|
show_ack,
|
||||||
lnurl_title
|
lnurl_title,
|
||||||
|
0
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return await get_copilot(copilot_id)
|
return await get_copilot(copilot_id)
|
||||||
|
|
|
||||||
86
lnbits/extensions/copilot/lnurl.py
Normal file
86
lnbits/extensions/copilot/lnurl.py
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import hashlib
|
||||||
|
import math
|
||||||
|
from quart import jsonify, url_for, request
|
||||||
|
from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore
|
||||||
|
|
||||||
|
from lnbits.core.services import create_invoice
|
||||||
|
|
||||||
|
from . import copilot_ext
|
||||||
|
from .crud import get_copilot
|
||||||
|
|
||||||
|
|
||||||
|
@copilot_ext.route("/lnurl/<copilot_id>", methods=["GET"])
|
||||||
|
async def lnurl_response(copilot_id):
|
||||||
|
copilot = await get_copilot(copilot_id)
|
||||||
|
if not copilot:
|
||||||
|
return jsonify({"status": "ERROR", "reason": "Copilot not found."})
|
||||||
|
|
||||||
|
resp = LnurlPayResponse(
|
||||||
|
callback=url_for(
|
||||||
|
"copilot.lnurl_callback", _external=True
|
||||||
|
),
|
||||||
|
min_sendable=copilot.amount,
|
||||||
|
max_sendable=copilot.amount,
|
||||||
|
metadata=copilot.lnurl_title,
|
||||||
|
)
|
||||||
|
|
||||||
|
params = resp.dict()
|
||||||
|
params["commentAllowed"] = 300
|
||||||
|
|
||||||
|
return jsonify(params)
|
||||||
|
|
||||||
|
|
||||||
|
@copilot_ext.route("/lnurl/cb", methods=["GET"])
|
||||||
|
async def lnurl_callback():
|
||||||
|
|
||||||
|
amount_received = int(request.args.get("amount"))
|
||||||
|
|
||||||
|
if amount_received < track.amount:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
LnurlErrorResponse(
|
||||||
|
reason=f"Amount {round(amount_received / 1000)} is smaller than minimum {math.floor(track.min_sendable)}."
|
||||||
|
).dict()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
elif track.max_sendable < amount_received:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
LnurlErrorResponse(
|
||||||
|
reason=f"Amount {round(amount_received / 1000)} is greater than maximum {math.floor(track.max_sendable)}."
|
||||||
|
).dict()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
comment = request.args.get("comment")
|
||||||
|
if len(comment or "") > 300:
|
||||||
|
return jsonify(
|
||||||
|
LnurlErrorResponse(
|
||||||
|
reason=f"Got a comment with {len(comment)} characters, but can only accept 300"
|
||||||
|
).dict()
|
||||||
|
)
|
||||||
|
|
||||||
|
copilot = await get_copilot_by_track(track_id)
|
||||||
|
|
||||||
|
payment_hash, payment_request = await create_invoice(
|
||||||
|
wallet_id=copilot.wallet,
|
||||||
|
amount=int(amount_received / 1000),
|
||||||
|
memo=await track.fullname(),
|
||||||
|
description_hash=hashlib.sha256(
|
||||||
|
(await track.lnurlpay_metadata()).encode("utf-8")
|
||||||
|
).digest(),
|
||||||
|
extra={"tag": "copilot", "track": track.id, "comment": comment},
|
||||||
|
)
|
||||||
|
|
||||||
|
if amount_received < track.price_msat:
|
||||||
|
success_action = None
|
||||||
|
ecopilote:
|
||||||
|
success_action = track.success_action(payment_hash)
|
||||||
|
|
||||||
|
resp = LnurlPayActionResponse(
|
||||||
|
pr=payment_request,
|
||||||
|
success_action=success_action,
|
||||||
|
routes=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify(resp.dict())
|
||||||
|
|
@ -15,9 +15,13 @@ async def m001_initial(db):
|
||||||
animation1threshold INTEGER,
|
animation1threshold INTEGER,
|
||||||
animation2threshold INTEGER,
|
animation2threshold INTEGER,
|
||||||
animation3threshold INTEGER,
|
animation3threshold INTEGER,
|
||||||
show_message TEXT,
|
animation1webhook TEXT,
|
||||||
amount INTEGER,
|
animation2webhook TEXT,
|
||||||
|
animation3webhook TEXT,
|
||||||
lnurl_title TEXT,
|
lnurl_title TEXT,
|
||||||
|
show_message INTEGER,
|
||||||
|
show_ack INTEGER,
|
||||||
|
amount_made INTEGER,
|
||||||
timestamp TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
timestamp TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,16 @@ class Copilots(NamedTuple):
|
||||||
animation1threshold: int
|
animation1threshold: int
|
||||||
animation2threshold: int
|
animation2threshold: int
|
||||||
animation3threshold: int
|
animation3threshold: int
|
||||||
|
animation1webhook: str
|
||||||
|
animation2webhook: str
|
||||||
|
animation3webhook: str
|
||||||
show_message: bool
|
show_message: bool
|
||||||
amount: int
|
amount: int
|
||||||
lnurl_title: str
|
lnurl_title: str
|
||||||
|
show_message: int
|
||||||
|
show_ack: int
|
||||||
|
amount_made: int
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "Copilots":
|
def from_row(cls, row: Row) -> "Copilots":
|
||||||
return cls(**dict(row))
|
return cls(**dict(row))
|
||||||
|
|
||||||
@property
|
|
||||||
def paid(self):
|
|
||||||
if self.balance >= self.amount:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
|
||||||
|
|
@ -133,9 +133,17 @@
|
||||||
dense
|
dense
|
||||||
v-model.trim="formDialogCopilot.data.title"
|
v-model.trim="formDialogCopilot.data.title"
|
||||||
type="text"
|
type="text"
|
||||||
label="*Title"
|
label="Title"
|
||||||
></q-input>
|
></q-input>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
group="api"
|
||||||
|
dense
|
||||||
|
expand-separator
|
||||||
|
label="Payment threshold 1"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-select
|
<q-select
|
||||||
|
|
@ -143,7 +151,7 @@
|
||||||
dense
|
dense
|
||||||
v-model.trim="formDialogCopilot.data.animation1"
|
v-model.trim="formDialogCopilot.data.animation1"
|
||||||
:options="options"
|
:options="options"
|
||||||
label="Animation 1"
|
label="Animation"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -153,12 +161,33 @@
|
||||||
dense
|
dense
|
||||||
v-model.trim="formDialogCopilot.data.animation1threshold"
|
v-model.trim="formDialogCopilot.data.animation1threshold"
|
||||||
type="number"
|
type="number"
|
||||||
label="Threshold (at least)"
|
label="From *sats"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-pl-xs">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialogCopilot.data.animation1webhook"
|
||||||
|
type="number"
|
||||||
|
label="Webhook"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
group="api"
|
||||||
|
dense
|
||||||
|
expand-separator
|
||||||
|
label="Payment threshold 2"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-select
|
<q-select
|
||||||
|
|
@ -166,21 +195,43 @@
|
||||||
dense
|
dense
|
||||||
v-model.trim="formDialogCopilot.data.animation2"
|
v-model.trim="formDialogCopilot.data.animation2"
|
||||||
:options="options"
|
:options="options"
|
||||||
label="Animation 2"
|
label="Animation"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col q-pl-sm">
|
|
||||||
|
<div class="col q-pl-xs">
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model.trim="formDialogCopilot.data.animation2threshold"
|
v-model.trim="formDialogCopilot.data.animation2threshold"
|
||||||
type="number"
|
type="number"
|
||||||
label="Threshold (at least)"
|
label="From *sats"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-pl-xs">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialogCopilot.data.animation2webhook"
|
||||||
|
type="number"
|
||||||
|
label="Webhook"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
group="api"
|
||||||
|
dense
|
||||||
|
expand-separator
|
||||||
|
label="Payment threshold 3"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-select
|
<q-select
|
||||||
|
|
@ -188,28 +239,34 @@
|
||||||
dense
|
dense
|
||||||
v-model.trim="formDialogCopilot.data.animation3"
|
v-model.trim="formDialogCopilot.data.animation3"
|
||||||
:options="options"
|
:options="options"
|
||||||
label="Animation 3"
|
label="Animation"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col q-pl-sm">
|
|
||||||
|
<div class="col q-pl-xs">
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model.trim="formDialogCopilot.data.animation3threshold"
|
v-model.trim="formDialogCopilot.data.animation3threshold"
|
||||||
type="number"
|
type="number"
|
||||||
label="Threshold (at least)"
|
label="From *sats"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-pl-xs">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialogCopilot.data.animation3webhook"
|
||||||
|
type="number"
|
||||||
|
label="Webhook"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</q-card-section>
|
||||||
<q-input
|
</q-card>
|
||||||
filled
|
</q-expansion-item>
|
||||||
dense
|
|
||||||
v-model.trim="formDialogCopilot.data.amount"
|
|
||||||
type="number"
|
|
||||||
label="*Amount (sats)"
|
|
||||||
></q-input>
|
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
|
|
@ -220,13 +277,29 @@
|
||||||
label="Lnurl title (message with QR code)"
|
label="Lnurl title (message with QR code)"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
<div class="q-gutter-sm">
|
<div class="q-gutter-sm">
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
v-model="formDialogCopilot.data.show_message"
|
v-model="formDialogCopilot.data.show_message"
|
||||||
left-label
|
left-label
|
||||||
label="Show lnurl-pay messages? (available in some wallets)"
|
label="Show lnurl-pay messages?"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col q-pl-xs">
|
||||||
|
<div class="q-gutter-sm">
|
||||||
|
<q-checkbox
|
||||||
|
v-model="formDialogCopilot.data.show_ack"
|
||||||
|
left-label
|
||||||
|
label="Show 'powered by LNbits'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
|
|
@ -370,6 +443,7 @@
|
||||||
show: false,
|
show: false,
|
||||||
data: {
|
data: {
|
||||||
show_message: false,
|
show_message: false,
|
||||||
|
show_ack: true,
|
||||||
description: '',
|
description: '',
|
||||||
time: null,
|
time: null,
|
||||||
amount: null
|
amount: null
|
||||||
|
|
|
||||||
51
lnbits/extensions/copilot/templates/copilot/panel.html
Normal file
51
lnbits/extensions/copilot/templates/copilot/panel.html
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
{% extends "public.html" %} {% block page %}
|
||||||
|
<div class="q-pa-sm theCard">
|
||||||
|
<q-card class="my-card">
|
||||||
|
<div class="column">
|
||||||
|
<center>
|
||||||
|
<div class="col theHeading">{{ copilot.title }}</div>
|
||||||
|
</center>
|
||||||
|
<div class="col">say what</div>
|
||||||
|
<div class="col" style="margin: 2px 15px; max-height: 100px">
|
||||||
|
<center>
|
||||||
|
<q-btn flat dense outline>Open compose window</q-btn>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<div class="col">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"></div>
|
||||||
|
<div class="col"></div>
|
||||||
|
</div>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %} {% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
newProgress: 0.4,
|
||||||
|
counter: 1,
|
||||||
|
newTimeLeft: '',
|
||||||
|
lnbtc: true,
|
||||||
|
onbtc: false,
|
||||||
|
charge_time_elapsed: '{{charge.time_elapsed}}',
|
||||||
|
charge_amount: '{{charge.amount}}',
|
||||||
|
charge_balance: '{{charge.balance}}',
|
||||||
|
charge_paid: '{{charge.paid}}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
created: function () {}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -19,4 +19,4 @@ async def display(copilot_id):
|
||||||
copilot = await get_copilot(copilot_id) or abort(
|
copilot = await get_copilot(copilot_id) or abort(
|
||||||
HTTPStatus.NOT_FOUND, "Charge link does not exist."
|
HTTPStatus.NOT_FOUND, "Charge link does not exist."
|
||||||
)
|
)
|
||||||
return await render_template("copilot/display.html", copilot=copilot)
|
return await render_template("copilot/panel.html", copilot=copilot)
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,12 @@ from .crud import (
|
||||||
"animation1threshold": {"type": "integer"},
|
"animation1threshold": {"type": "integer"},
|
||||||
"animation2threshold": {"type": "integer"},
|
"animation2threshold": {"type": "integer"},
|
||||||
"animation3threshold": {"type": "integer"},
|
"animation3threshold": {"type": "integer"},
|
||||||
|
"animation1webhook": {"type": "string"},
|
||||||
|
"animation2webhook": {"type": "string"},
|
||||||
|
"animation3webhook": {"type": "string"},
|
||||||
|
"lnurl_title": {"type": "string", "empty": False, "required": True},
|
||||||
"show_message": {"type": "integer", "empty": False, "required": True},
|
"show_message": {"type": "integer", "empty": False, "required": True},
|
||||||
"amount": {"type": "integer", "empty": False, "required": True},
|
"show_ack": {"type": "integer", "empty": False, "required": True},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
async def api_copilot_create_or_update(copilot_id=None):
|
async def api_copilot_create_or_update(copilot_id=None):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue