diff --git a/lnbits/extensions/lnticket/README.md b/lnbits/extensions/lnticket/README.md deleted file mode 100644 index bd0714506..000000000 --- a/lnbits/extensions/lnticket/README.md +++ /dev/null @@ -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) diff --git a/lnbits/extensions/lnticket/__init__.py b/lnbits/extensions/lnticket/__init__.py deleted file mode 100644 index 3c52fd2a2..000000000 --- a/lnbits/extensions/lnticket/__init__.py +++ /dev/null @@ -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)) diff --git a/lnbits/extensions/lnticket/config.json b/lnbits/extensions/lnticket/config.json deleted file mode 100644 index e8e55f2ff..000000000 --- a/lnbits/extensions/lnticket/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Support Tickets", - "short_description": "LN support ticket system", - "tile": "/lnticket/static/image/lntickets.png", - "contributors": ["benarc"] -} diff --git a/lnbits/extensions/lnticket/crud.py b/lnbits/extensions/lnticket/crud.py deleted file mode 100644 index 3254ad438..000000000 --- a/lnbits/extensions/lnticket/crud.py +++ /dev/null @@ -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,)) diff --git a/lnbits/extensions/lnticket/migrations.py b/lnbits/extensions/lnticket/migrations.py deleted file mode 100644 index 44c2e0f10..000000000 --- a/lnbits/extensions/lnticket/migrations.py +++ /dev/null @@ -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") diff --git a/lnbits/extensions/lnticket/models.py b/lnbits/extensions/lnticket/models.py deleted file mode 100644 index a7a3cf8c3..000000000 --- a/lnbits/extensions/lnticket/models.py +++ /dev/null @@ -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 diff --git a/lnbits/extensions/lnticket/static/image/lntickets.png b/lnbits/extensions/lnticket/static/image/lntickets.png deleted file mode 100644 index 875b41547..000000000 Binary files a/lnbits/extensions/lnticket/static/image/lntickets.png and /dev/null differ diff --git a/lnbits/extensions/lnticket/tasks.py b/lnbits/extensions/lnticket/tasks.py deleted file mode 100644 index 746ebea94..000000000 --- a/lnbits/extensions/lnticket/tasks.py +++ /dev/null @@ -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) diff --git a/lnbits/extensions/lnticket/templates/lnticket/_api_docs.html b/lnbits/extensions/lnticket/templates/lnticket/_api_docs.html deleted file mode 100644 index 97a0fe6b1..000000000 --- a/lnbits/extensions/lnticket/templates/lnticket/_api_docs.html +++ /dev/null @@ -1,26 +0,0 @@ - - - -
- Support Tickets: Get paid sats to answer questions -
-

- Charge people per word for contacting you. Possible applications incude, - paid support ticketing, PAYG language services, contact spam - protection.
- - Created by, - Ben Arc -

-
-
- -
diff --git a/lnbits/extensions/lnticket/templates/lnticket/display.html b/lnbits/extensions/lnticket/templates/lnticket/display.html deleted file mode 100644 index 7d6694b9b..000000000 --- a/lnbits/extensions/lnticket/templates/lnticket/display.html +++ /dev/null @@ -1,202 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -

{{ form_name }}

-
-
{{ form_desc }}
-
- - - - - -

{% raw %}{{amountWords}}{% endraw %}

-
- Submit - Cancel -
-
-
-
-
- - - - - - -
- Copy invoice - Close -
-
-
-
- -{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/lnticket/templates/lnticket/index.html b/lnbits/extensions/lnticket/templates/lnticket/index.html deleted file mode 100644 index 9329be7b2..000000000 --- a/lnbits/extensions/lnticket/templates/lnticket/index.html +++ /dev/null @@ -1,550 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - New Form - - - - - -
-
-
Forms
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
- - - -
-
-
Tickets
-
- -
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
-
-
- - -
- {{SITE_TITLE}} Support Tickets extension -
-
- - - {% include "lnticket/_api_docs.html" %} - -
-
- - - - - - - - - -
-
- -
-
- -
-
- -
- Update Form - - Create Form - Cancel -
-
-
-
- - - - {% raw %} - -

- {{this.ticketDialog.data.name}} sent a ticket -

-
- {{this.ticketDialog.data.email}} -
- {{this.ticketDialog.data.date}} -
- - -

{{this.ticketDialog.data.content}}

-
- {% endraw %} - - - -
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/lnticket/views.py b/lnbits/extensions/lnticket/views.py deleted file mode 100644 index 9bb1d9b3a..000000000 --- a/lnbits/extensions/lnticket/views.py +++ /dev/null @@ -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, - }, - ) diff --git a/lnbits/extensions/lnticket/views_api.py b/lnbits/extensions/lnticket/views_api.py deleted file mode 100644 index 4462688b6..000000000 --- a/lnbits/extensions/lnticket/views_api.py +++ /dev/null @@ -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 diff --git a/tests/data/mock_data.zip b/tests/data/mock_data.zip index fb6e51221..90c1393b2 100644 Binary files a/tests/data/mock_data.zip and b/tests/data/mock_data.zip differ