mirror of
https://github.com/lnbits/lnbits.git
synced 2025-09-26 11:56:16 +02:00
Add files via upload
This commit is contained in:
11
lnbits/extensions/lnticket/README.md
Normal file
11
lnbits/extensions/lnticket/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 "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"</code>
|
8
lnbits/extensions/lnticket/__init__.py
Normal file
8
lnbits/extensions/lnticket/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
lnticket_ext: Blueprint = Blueprint("lnticket", __name__, static_folder="static", template_folder="templates")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
6
lnbits/extensions/lnticket/config.json.example
Normal file
6
lnbits/extensions/lnticket/config.json.example
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "LNTicket",
|
||||
"short_description": "Pay-per-word LN ticket system",
|
||||
"icon": "contact_support",
|
||||
"contributors": ["benarc"]
|
||||
}
|
107
lnbits/extensions/lnticket/crud.py
Normal file
107
lnbits/extensions/lnticket/crud.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.db import open_ext_db
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from .models import Tickets, Forms
|
||||
|
||||
|
||||
|
||||
|
||||
#######TICKETS########
|
||||
|
||||
|
||||
def create_ticket(wallet: str, form: str, name: str, email: str, ltext: str, sats: int) -> Tickets:
|
||||
formdata = get_form(form)
|
||||
amount = formdata.amountmade + sats
|
||||
with open_ext_db("lnticket") as db:
|
||||
ticket_id = urlsafe_short_hash()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO tickets (id, form, email, ltext, name, wallet, sats)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(ticket_id, form, email, ltext, name, wallet, sats),
|
||||
)
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE forms
|
||||
SET amountmade = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(amount, form),
|
||||
)
|
||||
return get_ticket(ticket_id)
|
||||
|
||||
|
||||
def get_ticket(ticket_id: str) -> Optional[Tickets]:
|
||||
with open_ext_db("lnticket") as db:
|
||||
row = db.fetchone("SELECT * FROM tickets WHERE id = ?", (ticket_id,))
|
||||
|
||||
return Tickets(**row) if row else None
|
||||
|
||||
|
||||
def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("lnticket") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM tickets WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [Tickets(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_ticket(ticket_id: str) -> None:
|
||||
with open_ext_db("lnticket") as db:
|
||||
db.execute("DELETE FROM tickets WHERE id = ?", (ticket_id,))
|
||||
|
||||
|
||||
|
||||
|
||||
########FORMS#########
|
||||
|
||||
|
||||
def create_form(*, wallet: str, name: str, description: str, costpword: int) -> Forms:
|
||||
with open_ext_db("lnticket") as db:
|
||||
form_id = urlsafe_short_hash()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO forms (id, wallet, name, description, costpword, amountmade)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(form_id, wallet, name, description, costpword, 0 ),
|
||||
)
|
||||
|
||||
return get_form(form_id)
|
||||
|
||||
def update_form(form_id: str, **kwargs) -> Forms:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
with open_ext_db("lnticket") as db:
|
||||
db.execute(f"UPDATE forms SET {q} WHERE id = ?", (*kwargs.values(), form_id))
|
||||
row = db.fetchone("SELECT * FROM forms WHERE id = ?", (form_id,))
|
||||
|
||||
return Forms(**row) if row else None
|
||||
|
||||
|
||||
def get_form(form_id: str) -> Optional[Forms]:
|
||||
with open_ext_db("lnticket") as db:
|
||||
row = db.fetchone("SELECT * FROM forms WHERE id = ?", (form_id,))
|
||||
|
||||
return Forms(**row) if row else None
|
||||
|
||||
|
||||
def get_forms(wallet_ids: Union[str, List[str]]) -> List[Forms]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("lnticket") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM forms WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [Forms(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_form(form_id: str) -> None:
|
||||
with open_ext_db("lnticket") as db:
|
||||
db.execute("DELETE FROM forms WHERE id = ?", (form_id,))
|
36
lnbits/extensions/lnticket/migrations.py
Normal file
36
lnbits/extensions/lnticket/migrations.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
def m001_initial(db):
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS forms (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
costpword INTEGER NOT NULL,
|
||||
amountmade INTEGER NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
form TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
ltext TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
wallet TEXT NOT NULL,
|
||||
sats INTEGER NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
def migrate():
|
||||
with open_ext_db("lnticket") as db:
|
||||
m001_initial(db)
|
23
lnbits/extensions/lnticket/models.py
Normal file
23
lnbits/extensions/lnticket/models.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class Forms(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
name: str
|
||||
description: str
|
||||
costpword: int
|
||||
amountmade: int
|
||||
time: int
|
||||
|
||||
|
||||
class Tickets(NamedTuple):
|
||||
id: str
|
||||
form: str
|
||||
email: str
|
||||
ltext: str
|
||||
name: str
|
||||
wallet: str
|
||||
sats: int
|
||||
time: int
|
||||
|
27
lnbits/extensions/lnticket/templates/lnticket/_api_docs.html
Normal file
27
lnbits/extensions/lnticket/templates/lnticket/_api_docs.html
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-my-none">LNTickets: Get paid sats for questions</h5>
|
||||
<p>LNTickets allow you to charge people per word for contacting you. Applications incude, paid support ticketting, PAYG language services, such as translation, spam protection (people have to pay to contact you).<br/>
|
||||
<small> Created by, <a href="https://github.com/benarc">Ben Arc</a></small></p>
|
||||
</q-card>
|
||||
</q-card-section>
|
||||
|
||||
</q-card-section>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</q-expansion-item>
|
211
lnbits/extensions/lnticket/templates/lnticket/display.html
Normal file
211
lnbits/extensions/lnticket/templates/lnticket/display.html
Normal file
@@ -0,0 +1,211 @@
|
||||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h3 class="q-my-none">{{ form_name }}</h3>
|
||||
<br />
|
||||
<h5 class="q-my-none">{{ form_desc }}</h5>
|
||||
<br />
|
||||
<q-form @submit="Invoice()" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
type="name"
|
||||
label="Your name "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.email"
|
||||
type="email"
|
||||
label="Your email (optional, if you want a reply)"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="formDialog.data.text"
|
||||
type="textarea"
|
||||
label="{{ form_costpword }} sats per word"
|
||||
></q-input>
|
||||
<p>{% raw %}{{amountWords}}{% endraw %}</p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="formDialog.data.name == '' || formDialog.data.text == ''"
|
||||
type="submit"
|
||||
>Submit</q-btn
|
||||
>
|
||||
<q-btn @click="resetForm" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
|
||||
<q-card
|
||||
v-if="!receive.paymentReq"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
</q-card>
|
||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div class="text-center q-mb-lg">
|
||||
<a :href="'lightning:' + receive.paymentReq">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode
|
||||
:value="paymentReq"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
|
||||
>Copy invoice</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
console.log('{{ form_costpword }}')
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
paymentReq: null,
|
||||
redirectUrl: null,
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
name: '',
|
||||
email: '',
|
||||
text: ''
|
||||
}
|
||||
},
|
||||
receive: {
|
||||
show: false,
|
||||
status: 'pending',
|
||||
paymentReq: null
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
amountWords() {
|
||||
var regex = /\s+/gi
|
||||
var char = this.formDialog.data.text
|
||||
.trim()
|
||||
.replace(regex, ' ')
|
||||
.split(' ').length
|
||||
this.formDialog.data.sats = char * parseInt('{{ form_costpword }}')
|
||||
if (this.formDialog.data.sats == parseInt('{{ form_costpword }}')) {
|
||||
return '0 Sats to pay'
|
||||
} else {
|
||||
return this.formDialog.data.sats + ' Sats to pay'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
resetForm: function (e) {
|
||||
e.preventDefault()
|
||||
this.formDialog.data.name = ''
|
||||
this.formDialog.data.email = ''
|
||||
this.formDialog.data.text = ''
|
||||
this.formDialog.data.sats = 0
|
||||
},
|
||||
|
||||
closeReceiveDialog: function () {
|
||||
var checker = this.receive.paymentChecker
|
||||
dismissMsg()
|
||||
|
||||
clearInterval(paymentChecker)
|
||||
setTimeout(function () {}, 10000)
|
||||
},
|
||||
Invoice: function () {
|
||||
var self = this
|
||||
axios
|
||||
|
||||
.get(
|
||||
'/lnticket/api/v1/tickets/' +
|
||||
'{{ form_id }}' +
|
||||
'/' +
|
||||
self.formDialog.data.sats
|
||||
)
|
||||
|
||||
.then(function (response) {
|
||||
self.paymentReq = response.data.payment_request
|
||||
self.paymentCheck = response.data.checking_id
|
||||
|
||||
dismissMsg = self.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Waiting for payment...'
|
||||
})
|
||||
|
||||
self.receive = {
|
||||
show: true,
|
||||
status: 'pending',
|
||||
paymentReq: self.paymentReq
|
||||
}
|
||||
if (self.formDialog.data.email == '') {
|
||||
daemail = 'null'
|
||||
} else {
|
||||
daemail = self.formDialog.data.email
|
||||
}
|
||||
|
||||
paymentChecker = setInterval(function () {
|
||||
axios
|
||||
.post('/lnticket/api/v1/tickets/' + self.paymentCheck, {
|
||||
form: '{{ form_id }}',
|
||||
name: self.formDialog.data.name,
|
||||
email: daemail,
|
||||
ltext: self.formDialog.data.text,
|
||||
sats: self.formDialog.data.sats
|
||||
})
|
||||
.then(function (res) {
|
||||
if (res.data.paid) {
|
||||
clearInterval(paymentChecker)
|
||||
dismissMsg()
|
||||
self.formDialog.data.name = ''
|
||||
self.formDialog.data.email = ''
|
||||
self.formDialog.data.text = ''
|
||||
self.formDialog.data.sats = 0
|
||||
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Sent, thank you!',
|
||||
icon: null
|
||||
})
|
||||
self.receive = {
|
||||
show: false,
|
||||
status: 'complete',
|
||||
paymentReq: null
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
441
lnbits/extensions/lnticket/templates/lnticket/index.html
Normal file
441
lnbits/extensions/lnticket/templates/lnticket/index.html
Normal file
@@ -0,0 +1,441 @@
|
||||
{% 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="formDialog.show = true"
|
||||
>New Form</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">Forms</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportformsCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="forms"
|
||||
row-key="id"
|
||||
:columns="formsTable.columns"
|
||||
:pagination.sync="formsTable.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="link"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="props.row.displayUrl"
|
||||
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="updateformDialog(props.row.id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteForm(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</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">Tickets</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportticketsCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="tickets"
|
||||
row-key="id"
|
||||
:columns="ticketsTable.columns"
|
||||
:pagination.sync="ticketsTable.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-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="email"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'mailto:' + props.row.email"
|
||||
></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="deleteTicket(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 LNTicket extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "lnticket/_api_docs.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="formDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendFormData" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
type="name"
|
||||
label="Form name "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.description"
|
||||
type="textarea"
|
||||
label="Description "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="formDialog.data.costpword"
|
||||
type="number"
|
||||
label="Amount per word"
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="formDialog.data.id"
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
type="submit"
|
||||
>Update Form</q-btn
|
||||
>
|
||||
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="formDialog.data.costpword == null || formDialog.data.costpword < 0 || formDialog.data.name == null"
|
||||
type="submit"
|
||||
>Create Form</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
var mapLNTicket = 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.displayUrl = ['/lnticket/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
forms: [],
|
||||
tickets: [],
|
||||
formsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'},
|
||||
{
|
||||
name: 'description',
|
||||
align: 'left',
|
||||
label: 'Description',
|
||||
field: 'description'
|
||||
},
|
||||
{
|
||||
name: 'costpword',
|
||||
align: 'left',
|
||||
label: 'Cost Per Word',
|
||||
field: 'costpword'
|
||||
},
|
||||
{
|
||||
name: 'amountmade',
|
||||
align: 'left',
|
||||
label: 'Amount Made',
|
||||
field: 'amountmade'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
ticketsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{name: 'email', align: 'left', label: 'Email', field: 'email'},
|
||||
{name: 'ltext', align: 'left', label: 'Ticket', field: 'ltext'},
|
||||
{name: 'sats', align: 'left', label: 'Paid', field: 'sats'}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getTickets: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/lnticket/api/v1/tickets?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tickets = response.data.map(function (obj) {
|
||||
return mapLNTicket(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteTicket: function (ticketId) {
|
||||
var self = this
|
||||
var tickets = _.findWhere(this.tickets, {id: ticketId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this ticket')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/lnticket/api/v1/tickets/' + ticketId,
|
||||
_.findWhere(self.g.user.wallets, {id: tickets.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tickets = _.reject(self.tickets, function (obj) {
|
||||
return obj.id == ticketId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportticketsCSV: function () {
|
||||
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
|
||||
},
|
||||
|
||||
getForms: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/lnticket/api/v1/forms?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.forms = response.data.map(function (obj) {
|
||||
return mapLNTicket(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
sendFormData: function () {
|
||||
var wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: this.formDialog.data.wallet
|
||||
})
|
||||
var data = this.formDialog.data
|
||||
|
||||
if (data.id) {
|
||||
this.updateForm(wallet, data)
|
||||
} else {
|
||||
this.createForm(wallet, data)
|
||||
}
|
||||
},
|
||||
|
||||
createForm: function (wallet, data) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request('POST', '/lnticket/api/v1/forms', wallet.inkey, data)
|
||||
.then(function (response) {
|
||||
self.forms.push(mapLNTicket(response.data))
|
||||
self.formDialog.show = false
|
||||
self.formDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
updateformDialog: function (formId) {
|
||||
var link = _.findWhere(this.forms, {id: formId})
|
||||
console.log(link.id)
|
||||
this.formDialog.data.id = link.id
|
||||
this.formDialog.data.wallet = link.wallet
|
||||
this.formDialog.data.name = link.name
|
||||
this.formDialog.data.description = link.description
|
||||
this.formDialog.data.costpword = link.costpword
|
||||
this.formDialog.show = true
|
||||
},
|
||||
updateForm: function (wallet, data) {
|
||||
var self = this
|
||||
console.log(data)
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/lnticket/api/v1/forms/' + data.id,
|
||||
wallet.inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.forms = _.reject(self.forms, function (obj) {
|
||||
return obj.id == data.id
|
||||
})
|
||||
self.forms.push(mapLNTicket(response.data))
|
||||
self.formDialog.show = false
|
||||
self.formDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteForm: function (formsId) {
|
||||
var self = this
|
||||
var forms = _.findWhere(this.forms, {id: formsId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this form link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/lnticket/api/v1/forms/' + formsId,
|
||||
_.findWhere(self.g.user.wallets, {id: forms.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.forms = _.reject(self.forms, function (obj) {
|
||||
return obj.id == formsId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportformsCSV: function () {
|
||||
LNbits.utils.exportCSV(this.formsTable.columns, this.forms)
|
||||
}
|
||||
},
|
||||
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getTickets()
|
||||
this.getForms()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
22
lnbits/extensions/lnticket/views.py
Normal file
22
lnbits/extensions/lnticket/views.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from flask import g, abort, render_template
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.extensions.lnticket import lnticket_ext
|
||||
from .crud import get_form
|
||||
|
||||
|
||||
@lnticket_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def index():
|
||||
return render_template("lnticket/index.html", user=g.user)
|
||||
|
||||
|
||||
@lnticket_ext.route("/<form_id>")
|
||||
def display(form_id):
|
||||
form = get_form(form_id) or abort(HTTPStatus.NOT_FOUND, "LNTicket does not exist.")
|
||||
print(form.id)
|
||||
|
||||
return render_template("lnticket/display.html", form_id=form.id, form_name=form.name, form_desc=form.description, form_costpword=form.costpword)
|
143
lnbits/extensions/lnticket/views_api.py
Normal file
143
lnbits/extensions/lnticket/views_api.py
Normal file
@@ -0,0 +1,143 @@
|
||||
from flask import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.core.crud import get_user, get_wallet
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
from lnbits.extensions.lnticket import lnticket_ext
|
||||
from .crud import create_ticket, get_ticket, get_tickets, delete_ticket, create_form, update_form, get_form, get_forms, delete_form
|
||||
|
||||
|
||||
#########FORMS##########
|
||||
|
||||
@lnticket_ext.route("/api/v1/forms", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_forms():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([form._asdict() for form in get_forms(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@lnticket_ext.route("/api/v1/forms", methods=["POST"])
|
||||
@lnticket_ext.route("/api/v1/forms/<form_id>", methods=["PUT"])
|
||||
@api_check_wallet_key("invoice")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"wallet": {"type": "string", "empty": False, "required": True},
|
||||
"name": {"type": "string", "empty": False, "required": True},
|
||||
"description": {"type": "string", "min": 0, "required": True},
|
||||
"costpword": {"type": "integer", "min": 0, "required": True}
|
||||
}
|
||||
)
|
||||
def api_form_create(form_id=None):
|
||||
if form_id:
|
||||
form = get_form(form_id)
|
||||
print(g.data)
|
||||
|
||||
if not form:
|
||||
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if form.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your form."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
form = update_form(form_id, **g.data)
|
||||
else:
|
||||
form = create_form(**g.data)
|
||||
return jsonify(form._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@lnticket_ext.route("/api/v1/forms/<form_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_form_delete(form_id):
|
||||
form = get_form(form_id)
|
||||
|
||||
if not form:
|
||||
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if form.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your form."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_form(form_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
|
||||
|
||||
#########tickets##########
|
||||
|
||||
@lnticket_ext.route("/api/v1/tickets", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_tickets():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([form._asdict() for form in get_tickets(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@lnticket_ext.route("/api/v1/tickets/<form_id>/<sats>", methods=["GET"])
|
||||
def api_ticket_create(form_id, sats):
|
||||
form = get_form(form_id)
|
||||
|
||||
try:
|
||||
checking_id, payment_request = create_invoice(
|
||||
wallet_id=form.wallet, amount=sats, memo=f"#lnticket {form_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK
|
||||
|
||||
|
||||
|
||||
@lnticket_ext.route("/api/v1/tickets/<checking_id>", methods=["POST"])
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"form": {"type": "string", "empty": False, "required": True},
|
||||
"name": {"type": "string", "empty": False, "required": True},
|
||||
"email": {"type": "string", "empty": False, "required": True},
|
||||
"ltext": {"type": "string", "empty": False, "required": True},
|
||||
"sats": {"type": "integer", "min": 0, "required": True}
|
||||
})
|
||||
def api_ticket_send_ticket(checking_id):
|
||||
|
||||
form = get_form(g.data['form'])
|
||||
if not form:
|
||||
return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND
|
||||
try:
|
||||
is_paid = not WALLET.get_invoice_status(checking_id).pending
|
||||
except Exception:
|
||||
return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if is_paid:
|
||||
wallet = get_wallet(form.wallet)
|
||||
payment = wallet.get_payment(checking_id)
|
||||
payment.set_pending(False)
|
||||
create_ticket(wallet=form.wallet, **g.data)
|
||||
|
||||
return jsonify({"paid": True}), HTTPStatus.OK
|
||||
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
|
||||
|
||||
@lnticket_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_ticket_delete(ticket_id):
|
||||
ticket = get_ticket(ticket_id)
|
||||
|
||||
if not ticket:
|
||||
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if ticket.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your ticket."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_ticket(ticket_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
Reference in New Issue
Block a user