mirror of https://github.com/lukechilds/lnbits.git
committed by
GitHub
11 changed files with 1035 additions and 0 deletions
@ -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> |
@ -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 |
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"name": "LNTicket", |
||||
|
"short_description": "Pay-per-word LN ticket system", |
||||
|
"icon": "contact_support", |
||||
|
"contributors": ["benarc"] |
||||
|
} |
@ -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,)) |
@ -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) |
@ -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 |
||||
|
|
@ -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> |
@ -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 %} |
@ -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 %} |
@ -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) |
@ -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 |
Loading…
Reference in new issue