Merge pull request #1423 from lnbits/pullsupport

Removed LNticket
This commit is contained in:
Arc 2023-01-27 12:42:31 +00:00 committed by GitHub
commit b1115e5b11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 0 additions and 1476 deletions

View File

@ -1,29 +0,0 @@
# Support Tickets
## Get paid sats to answer questions
Charge a per word amount for people to contact you.
Possible applications include, paid support ticketing, PAYG language services, contact spam protection.
1. Click "NEW FORM" to create a new contact form\
![new contact form](https://i.imgur.com/kZqWGPe.png)
2. Fill out the contact form
- set the wallet to use
- give your form a name
- set an optional webhook that will get called when the form receives a payment
- give it a small description
- set the amount you want to charge, per **word**, for people to contact you\
![form settings](https://i.imgur.com/AsXeVet.png)
3. Your new contact form will appear on the _Forms_ section. Note that you can create various forms with different rates per word, for different purposes\
![forms section](https://i.imgur.com/gg71HhM.png)
4. When a user wants to reach out to you, they will get to the contact form. They can fill out some information:
- a name
- an optional email if they want you to reply
- and the actual message
- at the bottom, a value in satoshis, will display how much it will cost them to send this message\
![user view of form](https://i.imgur.com/DWGJWQz.png)
- after submiting the Lightning Network invoice will pop up and after payment the message will be sent to you\
![contact form payment](https://i.imgur.com/7heGsiO.png)
5. Back in "Support ticket" extension you'll get the messages your fans, users, haters, etc, sent you on the _Tickets_ section\
![tickets](https://i.imgur.com/dGhJ6Ok.png)

View File

@ -1,35 +0,0 @@
import asyncio
import json
from fastapi import APIRouter
from starlette.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_lnticket")
lnticket_ext: APIRouter = APIRouter(prefix="/lnticket", tags=["LNTicket"])
lnticket_static_files = [
{
"path": "/lnticket/static",
"app": StaticFiles(directory="lnbits/extensions/lnticket/static"),
"name": "lnticket_static",
}
]
def lnticket_renderer():
return template_renderer(["lnbits/extensions/lnticket/templates"])
from .tasks import wait_for_paid_invoices
from .views import * # noqa
from .views_api import * # noqa
def lnticket_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))

View File

@ -1,6 +0,0 @@
{
"name": "Support Tickets",
"short_description": "LN support ticket system",
"tile": "/lnticket/static/image/lntickets.png",
"contributors": ["benarc"]
}

View File

@ -1,162 +0,0 @@
from typing import List, Optional, Union
import httpx
from lnbits.core.models import Wallet
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import CreateFormData, CreateTicketData, Forms, Tickets
async def create_ticket(
payment_hash: str, wallet: str, data: CreateTicketData
) -> Tickets:
await db.execute(
"""
INSERT INTO lnticket.ticket (id, form, email, ltext, name, wallet, sats, paid)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
payment_hash,
data.form,
data.email,
data.ltext,
data.name,
wallet,
data.sats,
False,
),
)
ticket = await get_ticket(payment_hash)
assert ticket, "Newly created ticket couldn't be retrieved"
return ticket
async def set_ticket_paid(payment_hash: str) -> Tickets:
row = await db.fetchone(
"SELECT * FROM lnticket.ticket WHERE id = ?", (payment_hash,)
)
if row[7] == False:
await db.execute(
"""
UPDATE lnticket.ticket
SET paid = true
WHERE id = ?
""",
(payment_hash,),
)
formdata = await get_form(row[1])
assert formdata, "Couldn't get form from paid ticket"
amount = formdata.amountmade + row[7]
await db.execute(
"""
UPDATE lnticket.form2
SET amountmade = ?
WHERE id = ?
""",
(amount, row[1]),
)
ticket = await get_ticket(payment_hash)
assert ticket, "Newly paid ticket could not be retrieved"
if formdata.webhook:
async with httpx.AsyncClient() as client:
await client.post(
formdata.webhook,
json={
"form": ticket.form,
"name": ticket.name,
"email": ticket.email,
"content": ticket.ltext,
},
timeout=40,
)
return ticket
ticket = await get_ticket(payment_hash)
assert ticket, "Newly paid ticket could not be retrieved"
return ticket
async def get_ticket(ticket_id: str) -> Optional[Tickets]:
row = await db.fetchone("SELECT * FROM lnticket.ticket WHERE id = ?", (ticket_id,))
return Tickets(**row) if row else None
async def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM lnticket.ticket WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Tickets(**row) for row in rows]
async def delete_ticket(ticket_id: str) -> None:
await db.execute("DELETE FROM lnticket.ticket WHERE id = ?", (ticket_id,))
# FORMS
async def create_form(data: CreateFormData, wallet: Wallet) -> Forms:
form_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO lnticket.form2 (id, wallet, name, webhook, description, flatrate, amount, amountmade)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
form_id,
wallet.id,
wallet.name,
data.webhook,
data.description,
data.flatrate,
data.amount,
0,
),
)
form = await get_form(form_id)
assert form, "Newly created forms couldn't be retrieved"
return form
async def update_form(form_id: str, **kwargs) -> Forms:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE lnticket.form2 SET {q} WHERE id = ?", (*kwargs.values(), form_id)
)
row = await db.fetchone("SELECT * FROM lnticket.form2 WHERE id = ?", (form_id,))
assert row, "Newly updated form couldn't be retrieved"
return Forms(**row)
async def get_form(form_id: str) -> Optional[Forms]:
row = await db.fetchone("SELECT * FROM lnticket.form2 WHERE id = ?", (form_id,))
return Forms(**row) if row else None
async def get_forms(wallet_ids: Union[str, List[str]]) -> List[Forms]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM lnticket.form2 WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Forms(**row) for row in rows]
async def delete_form(form_id: str) -> None:
await db.execute("DELETE FROM lnticket.form2 WHERE id = ?", (form_id,))

View File

@ -1,177 +0,0 @@
async def m001_initial(db):
await db.execute(
"""
CREATE TABLE lnticket.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 """
+ db.timestamp_now
+ """
);
"""
)
await db.execute(
"""
CREATE TABLE lnticket.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 """
+ db.timestamp_now
+ """
);
"""
)
async def m002_changed(db):
await db.execute(
"""
CREATE TABLE lnticket.ticket (
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,
paid BOOLEAN NOT NULL,
time TIMESTAMP NOT NULL DEFAULT """
+ db.timestamp_now
+ """
);
"""
)
for row in [
list(row) for row in await db.fetchall("SELECT * FROM lnticket.tickets")
]:
usescsv = ""
for i in range(row[5]):
if row[7]:
usescsv += "," + str(i + 1)
else:
usescsv += "," + str(1)
usescsv = usescsv[1:]
await db.execute(
"""
INSERT INTO lnticket.ticket (
id,
form,
email,
ltext,
name,
wallet,
sats,
paid
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(row[0], row[1], row[2], row[3], row[4], row[5], row[6], True),
)
await db.execute("DROP TABLE lnticket.tickets")
async def m003_changed(db):
await db.execute(
"""
CREATE TABLE IF NOT EXISTS lnticket.form (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
name TEXT NOT NULL,
webhook TEXT,
description TEXT NOT NULL,
costpword INTEGER NOT NULL,
amountmade INTEGER NOT NULL,
time TIMESTAMP NOT NULL DEFAULT """
+ db.timestamp_now
+ """
);
"""
)
for row in [list(row) for row in await db.fetchall("SELECT * FROM lnticket.forms")]:
usescsv = ""
for i in range(row[5]):
if row[7]:
usescsv += "," + str(i + 1)
else:
usescsv += "," + str(1)
usescsv = usescsv[1:]
await db.execute(
"""
INSERT INTO lnticket.form (
id,
wallet,
name,
webhook,
description,
costpword,
amountmade
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(row[0], row[1], row[2], row[3], row[4], row[5], row[6]),
)
await db.execute("DROP TABLE lnticket.forms")
async def m004_changed(db):
await db.execute(
"""
CREATE TABLE lnticket.form2 (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
name TEXT NOT NULL,
webhook TEXT,
description TEXT NOT NULL,
flatrate INTEGER DEFAULT 0,
amount INTEGER NOT NULL,
amountmade INTEGER NOT NULL,
time TIMESTAMP NOT NULL DEFAULT """
+ db.timestamp_now
+ """
);
"""
)
for row in [list(row) for row in await db.fetchall("SELECT * FROM lnticket.form")]:
usescsv = ""
for i in range(row[5]):
if row[7]:
usescsv += "," + str(i + 1)
else:
usescsv += "," + str(1)
usescsv = usescsv[1:]
await db.execute(
"""
INSERT INTO lnticket.form2 (
id,
wallet,
name,
webhook,
description,
amount,
amountmade
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(row[0], row[1], row[2], row[3], row[4], row[5], row[6]),
)
await db.execute("DROP TABLE lnticket.form")

View File

@ -1,44 +0,0 @@
from typing import Optional
from fastapi.param_functions import Query
from pydantic import BaseModel
class CreateFormData(BaseModel):
name: str = Query(...)
webhook: str = Query(None)
description: str = Query(..., min_length=0)
amount: int = Query(..., ge=0)
flatrate: int = Query(...)
class CreateTicketData(BaseModel):
form: str = Query(...)
name: str = Query(...)
email: str = Query("")
ltext: str = Query(...)
sats: int = Query(..., ge=0)
class Forms(BaseModel):
id: str
wallet: str
name: str
webhook: Optional[str]
description: str
amount: int
flatrate: int
amountmade: int
time: int
class Tickets(BaseModel):
id: str
form: str
email: str
ltext: str
name: str
wallet: str
sats: int
paid: bool
time: int

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,32 +0,0 @@
import asyncio
from loguru import logger
from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
from .crud import get_ticket, set_ticket_paid
async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue, get_current_extension_name())
while True:
payment = await invoice_queue.get()
await on_invoice_paid(payment)
async def on_invoice_paid(payment: Payment) -> None:
if payment.extra.get("tag") != "lnticket":
# not a lnticket invoice
return
ticket = await get_ticket(payment.checking_id)
if not ticket:
logger.error("this should never happen", payment)
return
await payment.set_pending(False)
await set_ticket_paid(payment.payment_hash)

View File

@ -1,26 +0,0 @@
<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">
Support Tickets: Get paid sats to answer questions
</h5>
<p>
Charge people per word for contacting you. Possible applications incude,
paid support ticketing, PAYG language services, contact spam
protection.<br />
<small>
Created by,
<a class="text-secondary" href="https://github.com/benarc"
>Ben Arc</a
></small
>
</p>
</q-card-section>
</q-card>
<q-btn flat label="Swagger API" type="a" href="../docs#/lnticket"></q-btn>
</q-expansion-item>

View File

@ -1,202 +0,0 @@
{% 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
v-if="flatrate"
filled
dense
v-model.number="formDialog.data.text"
type="textarea"
label="{{ form_amount }} sats"
></q-input>
<q-input
v-else
filled
dense
v-model.number="formDialog.data.text"
type="textarea"
label="{{ form_amount }} sats per word"
></q-input>
<p>{% raw %}{{amountWords}}{% endraw %}</p>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
: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 class="text-secondary" :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="'lightning:' + paymentReq.toUpperCase()"
: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>
//console.log('{{ form_costpword }}')
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
paymentReq: null,
redirectUrl: null,
flatrate: parseInt('{{form_flatrate}}'),
formDialog: {
show: false,
data: {
name: '',
email: '',
text: ''
},
dismissMsg: null,
paymentChecker: null
},
receive: {
show: false,
status: 'pending',
paymentReq: null
},
wallet: {
inkey: ''
},
cancelListener: () => {}
}
},
computed: {
amountWords() {
if (this.formDialog.data.text.length == '') {
return '0 Sats to pay'
} else {
var regex = /\s+/gi
var nwords = this.formDialog.data.text
.trim()
.replace(regex, ' ')
.split(' ').length
if (this.flatrate) {
var sats = parseInt('{{ form_amount }}')
} else {
var sats = nwords * parseInt('{{ form_amount }}')
}
this.formDialog.data.sats = sats
return sats + ' Sats to pay'
}
}
},
methods: {
resetForm: function (e) {
e.preventDefault()
this.formDialog.data.name = ''
this.formDialog.data.email = ''
this.formDialog.data.text = ''
},
Invoice: function () {
var self = this
var dialog = this.formDialog
axios
.post('/lnticket/api/v1/tickets/{{ form_id }}', {
form: '{{ form_id }}',
name: self.formDialog.data.name,
email: self.formDialog.data.email,
ltext: self.formDialog.data.text,
sats: self.formDialog.data.sats
})
.then(function (response) {
self.paymentReq = response.data.payment_request
self.paymentCheck = response.data.payment_hash
dialog.dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
self.receive = {
show: true,
status: 'pending',
paymentReq: self.paymentReq
}
dialog.paymentChecker = setInterval(function () {
axios
.get('/lnticket/api/v1/tickets/' + response.data.payment_hash)
.then(function (res) {
if (res.data.paid) {
clearInterval(dialog.paymentChecker)
dialog.dismissMsg()
self.receive.show = false
self.formDialog.data.name = ''
self.formDialog.data.email = ''
self.formDialog.data.text = ''
self.$q.notify({
type: 'positive',
message: 'Sats received, thanks!',
icon: 'thumb_up'
})
}
})
}, 3000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}
},
created() {
this.wallet.inkey = '{{form_wallet}}'
}
})
</script>
{% endblock %}

View File

@ -1,550 +0,0 @@
{% 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="primary" @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"
icon="autorenew"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="getTickets"
><q-tooltip> Refresh Tickets </q-tooltip></q-btn
>
</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 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" v-if="props.row.paid">
<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 auto-width>
<q-btn
unelevated
dense
size="xs"
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="ticketCard(props)"
><q-tooltip> Click to show ticket </q-tooltip></q-btn
>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label == "Ticket" ? col.value.length > 20 ?
`${col.value.substring(0, 20)}...` : col.value : 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">
{{SITE_TITLE}} Support Tickets 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.webhook"
type="text"
label="Webhook (optional)"
hint="A URL to be called whenever this link receives a payment."
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.description"
type="textarea"
label="Description "
></q-input>
<div class="row">
<div class="col-5">
<q-toggle
:label="`${flatRate}`"
color="primary"
v-model="formDialog.data.flatrate"
></q-toggle>
</div>
<div class="col-7">
<q-input
filled
dense
v-model.number="formDialog.data.amount"
type="number"
label="Amount"
></q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn
v-if="formDialog.data.id"
unelevated
color="primary"
type="submit"
>Update Form</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="formDialog.data.amount == null || formDialog.data.amount < 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>
<!-- Read Ticket Dialog -->
<q-dialog v-model="ticketDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
{% raw %}
<q-card-section>
<h4 class="text-subtitle1 q-my-none">
<i>{{this.ticketDialog.data.name}}</i> sent a ticket
</h4>
<div v-if="this.ticketDialog.data.email">
<small>{{this.ticketDialog.data.email}}</small>
</div>
<small>{{this.ticketDialog.data.date}}</small>
</q-card-section>
<q-separator></q-separator>
<q-card-section>
<p>{{this.ticketDialog.data.content}}</p>
</q-card-section>
{% endraw %}
<q-card-actions align="right">
<q-btn
flat
label="CLOSE"
color="primary"
v-close-popup
@click="resetForm"
/>
</q-card-actions>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
const 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: 'webhook',
align: 'left',
label: 'Webhook',
field: 'webhook'
},
{
name: 'description',
align: 'left',
label: 'Description',
field: 'description'
},
{
name: 'flatrate',
align: 'left',
label: 'Flat Rate',
field: 'flatrate'
},
{
name: 'amount',
align: 'left',
label: 'Amount',
field: 'amount'
}
],
pagination: {
rowsPerPage: 10
}
},
ticketsTable: {
columns: [
{name: 'form', align: 'left', label: 'Form', field: 'form'},
{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: 'Cost', field: 'sats'}
],
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
data: {flatrate: false}
},
ticketDialog: {
show: false,
data: {}
}
}
},
computed: {
flatRate: function () {
if (this.formDialog.data.flatrate) {
return 'Charge flat rate'
} else {
return 'Charge per word'
}
}
},
methods: {
resetForm() {
this.formDialog.data = {flatrate: false}
},
getTickets: function () {
var self = this
LNbits.api
.request(
'GET',
'/lnticket/api/v1/tickets?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.tickets = response.data
.map(function (obj) {
if (!obj?.paid) return
return mapLNTicket(obj)
})
.filter(v => v)
})
},
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)
})
})
},
ticketCard(ticket) {
this.ticketDialog.show = true
let {date, email, ltext, name} = ticket.row
this.ticketDialog.data = {
date,
email,
content: ltext,
name
}
},
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=true',
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
})
this.formDialog.data.inkey = wallet.inkey
var data = this.formDialog.data
if (data.id) {
this.updateForm(wallet, data)
} else {
this.createForm(wallet, data)
}
},
createForm: function (wallet, data) {
var self = this
console.log('create', data)
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.resetForm()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
updateformDialog: function (formId) {
var link = _.findWhere(this.forms, {id: formId})
console.log('LINK', link)
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.flatrate = Boolean(link.flatrate)
this.formDialog.data.amount = link.amount
this.formDialog.show = true
},
updateForm: function (wallet, data) {
var self = this
console.log('update', 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.resetForm()
})
.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 %}

View File

@ -1,49 +0,0 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Depends
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.crud import get_wallet
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import lnticket_ext, lnticket_renderer
from .crud import get_form
templates = Jinja2Templates(directory="templates")
@lnticket_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return lnticket_renderer().TemplateResponse(
"lnticket/index.html", {"request": request, "user": user.dict()}
)
@lnticket_ext.get("/{form_id}")
async def display(request: Request, form_id):
form = await get_form(form_id)
if not form:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="LNTicket does not exist."
)
wallet = await get_wallet(form.wallet)
assert wallet
return lnticket_renderer().TemplateResponse(
"lnticket/display.html",
{
"request": request,
"form_id": form.id,
"form_name": form.name,
"form_desc": form.description,
"form_amount": form.amount,
"form_flatrate": form.flatrate,
"form_wallet": wallet.inkey,
},
)

View File

@ -1,164 +0,0 @@
import re
from http import HTTPStatus
from fastapi import Depends, Query
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.core.services import create_invoice
from lnbits.core.views.api import api_payment
from lnbits.decorators import WalletTypeInfo, get_key_type
from . import lnticket_ext
from .crud import (
create_form,
create_ticket,
delete_form,
delete_ticket,
get_form,
get_forms,
get_ticket,
get_tickets,
set_ticket_paid,
update_form,
)
from .models import CreateFormData, CreateTicketData
# FORMS
@lnticket_ext.get("/api/v1/forms")
async def api_forms_get(
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
):
wallet_ids = [wallet.wallet.id]
if all_wallets:
user = await get_user(wallet.wallet.user)
wallet_ids = user.wallet_ids if user else []
return [form.dict() for form in await get_forms(wallet_ids)]
@lnticket_ext.post("/api/v1/forms", status_code=HTTPStatus.CREATED)
@lnticket_ext.put("/api/v1/forms/{form_id}")
async def api_form_create(
data: CreateFormData, form_id=None, wallet: WalletTypeInfo = Depends(get_key_type)
):
if form_id:
form = await get_form(form_id)
if not form:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"Form does not exist."
)
if form.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail=f"Not your form."
)
form = await update_form(form_id, **data.dict())
else:
form = await create_form(data, wallet.wallet)
return form.dict()
@lnticket_ext.delete("/api/v1/forms/{form_id}")
async def api_form_delete(form_id, wallet: WalletTypeInfo = Depends(get_key_type)):
form = await get_form(form_id)
if not form:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"Form does not exist."
)
if form.wallet != wallet.wallet.id:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail=f"Not your form.")
await delete_form(form_id)
return "", HTTPStatus.NO_CONTENT
#########tickets##########
@lnticket_ext.get("/api/v1/tickets")
async def api_tickets(
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
):
wallet_ids = [wallet.wallet.id]
if all_wallets:
user = await get_user(wallet.wallet.user)
wallet_ids = user.wallet_ids if user else []
return [form.dict() for form in await get_tickets(wallet_ids)]
@lnticket_ext.post("/api/v1/tickets/{form_id}", status_code=HTTPStatus.CREATED)
async def api_ticket_make_ticket(data: CreateTicketData, form_id):
form = await get_form(form_id)
if not form:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"LNTicket does not exist."
)
if data.sats < 1:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"0 invoices not allowed."
)
nwords = len(re.split(r"\s+", data.ltext))
try:
payment_hash, payment_request = await create_invoice(
wallet_id=form.wallet,
amount=data.sats,
memo=f"ticket with {nwords} words on {form_id}",
extra={"tag": "lnticket"},
)
except Exception as e:
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
ticket = await create_ticket(
payment_hash=payment_hash, wallet=form.wallet, data=data
)
if not ticket:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="LNTicket could not be fetched."
)
return {"payment_hash": payment_hash, "payment_request": payment_request}
@lnticket_ext.get("/api/v1/tickets/{payment_hash}", status_code=HTTPStatus.OK)
async def api_ticket_send_ticket(payment_hash):
ticket = await get_ticket(payment_hash)
try:
status = await api_payment(payment_hash)
if status["paid"]:
await set_ticket_paid(payment_hash=payment_hash)
return {"paid": True}
except Exception:
return {"paid": False}
return {"paid": False}
@lnticket_ext.delete("/api/v1/tickets/{ticket_id}")
async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_type)):
ticket = await get_ticket(ticket_id)
if not ticket:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"LNTicket does not exist."
)
if ticket.wallet != wallet.wallet.id:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your ticket.")
await delete_ticket(ticket_id)
return "", HTTPStatus.NO_CONTENT

Binary file not shown.