diff --git a/lnbits/extensions/copilot/README.md b/lnbits/extensions/copilot/README.md new file mode 100644 index 000000000..323aeddc0 --- /dev/null +++ b/lnbits/extensions/copilot/README.md @@ -0,0 +1,3 @@ +# StreamerCopilot + +Tool to help streamers accept sats for tips diff --git a/lnbits/extensions/copilot/__init__.py b/lnbits/extensions/copilot/__init__.py new file mode 100644 index 000000000..b255282b2 --- /dev/null +++ b/lnbits/extensions/copilot/__init__.py @@ -0,0 +1,17 @@ +from quart import Blueprint +from lnbits.db import Database + +db = Database("ext_copilot") + +copilot_ext: Blueprint = Blueprint( + "copilot", __name__, static_folder="static", template_folder="templates" +) + +from .views_api import * # noqa +from .views import * # noqa +from .lnurl import * # noqa +from .tasks import register_listeners + +from lnbits.tasks import record_async + +copilot_ext.record(record_async(register_listeners)) diff --git a/lnbits/extensions/copilot/config.json b/lnbits/extensions/copilot/config.json new file mode 100644 index 000000000..a4ecb3b5b --- /dev/null +++ b/lnbits/extensions/copilot/config.json @@ -0,0 +1,8 @@ +{ + "name": "Streamer Copilot", + "short_description": "Video tips/animations/webhooks", + "icon": "face", + "contributors": [ + "arcbtc" + ] +} diff --git a/lnbits/extensions/copilot/crud.py b/lnbits/extensions/copilot/crud.py new file mode 100644 index 000000000..d083675e2 --- /dev/null +++ b/lnbits/extensions/copilot/crud.py @@ -0,0 +1,107 @@ +from typing import List, Optional, Union + +# from lnbits.db import open_ext_db +from . import db +from .models import Copilots + +from lnbits.helpers import urlsafe_short_hash + +from quart import jsonify + + +###############COPILOTS########################## + + +async def create_copilot( + title: str, + user: str, + lnurl_toggle: Optional[int] = 0, + wallet: Optional[str] = None, + animation1: Optional[str] = None, + animation2: Optional[str] = None, + animation3: Optional[str] = None, + animation1threshold: Optional[int] = None, + animation2threshold: Optional[int] = None, + animation3threshold: Optional[int] = None, + animation1webhook: Optional[str] = None, + animation2webhook: Optional[str] = None, + animation3webhook: Optional[str] = None, + lnurl_title: Optional[str] = None, + show_message: Optional[int] = 0, + show_ack: Optional[int] = 0, + show_price: Optional[str] = None, + amount_made: Optional[int] = None, +) -> Copilots: + copilot_id = urlsafe_short_hash() + + await db.execute( + """ + INSERT INTO copilot.copilots ( + id, + "user", + lnurl_toggle, + wallet, + title, + animation1, + animation2, + animation3, + animation1threshold, + animation2threshold, + animation3threshold, + animation1webhook, + animation2webhook, + animation3webhook, + lnurl_title, + show_message, + show_ack, + show_price, + amount_made + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + copilot_id, + user, + int(lnurl_toggle), + wallet, + title, + animation1, + animation2, + animation3, + animation1threshold, + animation2threshold, + animation3threshold, + animation1webhook, + animation2webhook, + animation3webhook, + lnurl_title, + int(show_message), + int(show_ack), + show_price, + 0, + ), + ) + return await get_copilot(copilot_id) + + +async def update_copilot(copilot_id: str, **kwargs) -> Optional[Copilots]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE copilot.copilots SET {q} WHERE id = ?", (*kwargs.values(), copilot_id) + ) + row = await db.fetchone("SELECT * FROM copilot.copilots WHERE id = ?", (copilot_id,)) + return Copilots.from_row(row) if row else None + + +async def get_copilot(copilot_id: str) -> Copilots: + row = await db.fetchone("SELECT * FROM copilot.copilots WHERE id = ?", (copilot_id,)) + return Copilots.from_row(row) if row else None + + +async def get_copilots(user: str) -> List[Copilots]: + rows = await db.fetchall("""SELECT * FROM copilot.copilots WHERE "user" = ?""", (user,)) + return [Copilots.from_row(row) for row in rows] + + +async def delete_copilot(copilot_id: str) -> None: + await db.execute("DELETE FROM copilot.copilots WHERE id = ?", (copilot_id,)) diff --git a/lnbits/extensions/copilot/lnurl.py b/lnbits/extensions/copilot/lnurl.py new file mode 100644 index 000000000..0a10e29bc --- /dev/null +++ b/lnbits/extensions/copilot/lnurl.py @@ -0,0 +1,86 @@ +import json +import hashlib +import math +from quart import jsonify, url_for, request +from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore +from lnurl.types import LnurlPayMetadata +from lnbits.core.services import create_invoice + +from . import copilot_ext +from .crud import get_copilot + + +@copilot_ext.route("/lnurl/", methods=["GET"]) +async def lnurl_response(cp_id): + cp = await get_copilot(cp_id) + if not cp: + return jsonify({"status": "ERROR", "reason": "Copilot not found."}) + + resp = LnurlPayResponse( + callback=url_for("copilot.lnurl_callback", cp_id=cp_id, _external=True), + min_sendable=10000, + max_sendable=50000000, + metadata=LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]])), + ) + + params = resp.dict() + if cp.show_message: + params["commentAllowed"] = 300 + + return jsonify(params) + + +@copilot_ext.route("/lnurl/cb/", methods=["GET"]) +async def lnurl_callback(cp_id): + cp = await get_copilot(cp_id) + if not cp: + return jsonify({"status": "ERROR", "reason": "Copilot not found."}) + + amount_received = int(request.args.get("amount")) + + if amount_received < 10000: + return ( + jsonify( + LnurlErrorResponse( + reason=f"Amount {round(amount_received / 1000)} is smaller than minimum 10 sats." + ).dict() + ), + ) + elif amount_received / 1000 > 10000000: + return ( + jsonify( + LnurlErrorResponse( + reason=f"Amount {round(amount_received / 1000)} is greater than maximum 50000." + ).dict() + ), + ) + comment = "" + if request.args.get("comment"): + comment = request.args.get("comment") + if len(comment or "") > 300: + return jsonify( + LnurlErrorResponse( + reason=f"Got a comment with {len(comment)} characters, but can only accept 300" + ).dict() + ) + if len(comment) < 1: + comment = "none" + + payment_hash, payment_request = await create_invoice( + wallet_id=cp.wallet, + amount=int(amount_received / 1000), + memo=cp.lnurl_title, + description_hash=hashlib.sha256( + ( + LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]])) + ).encode("utf-8") + ).digest(), + extra={"tag": "copilot", "copilot": cp.id, "comment": comment}, + ) + resp = LnurlPayActionResponse( + pr=payment_request, + success_action=None, + disposable=False, + routes=[], + ) + return jsonify(resp.dict()) diff --git a/lnbits/extensions/copilot/migrations.py b/lnbits/extensions/copilot/migrations.py new file mode 100644 index 000000000..c1fbfc0dc --- /dev/null +++ b/lnbits/extensions/copilot/migrations.py @@ -0,0 +1,76 @@ +async def m001_initial(db): + """ + Initial copilot table. + """ + + await db.execute( + f""" + CREATE TABLE copilot.copilots ( + id TEXT NOT NULL PRIMARY KEY, + "user" TEXT, + title TEXT, + lnurl_toggle INTEGER, + wallet TEXT, + animation1 TEXT, + animation2 TEXT, + animation3 TEXT, + animation1threshold INTEGER, + animation2threshold INTEGER, + animation3threshold INTEGER, + animation1webhook TEXT, + animation2webhook TEXT, + animation3webhook TEXT, + lnurl_title TEXT, + show_message INTEGER, + show_ack INTEGER, + show_price INTEGER, + amount_made INTEGER, + fullscreen_cam INTEGER, + iframe_url TEXT, + timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + ); + """ + ) + +async def m002_fix_data_types(db): + """ + Fix data types. + """ + + if(db.type != "SQLITE"): + await db.execute("ALTER TABLE copilot.copilots ALTER COLUMN show_price TYPE TEXT;") + + # If needed, migration for SQLite (RENAME not working properly) + # + # await db.execute( + # f""" + # CREATE TABLE copilot.new_copilots ( + # id TEXT NOT NULL PRIMARY KEY, + # "user" TEXT, + # title TEXT, + # lnurl_toggle INTEGER, + # wallet TEXT, + # animation1 TEXT, + # animation2 TEXT, + # animation3 TEXT, + # animation1threshold INTEGER, + # animation2threshold INTEGER, + # animation3threshold INTEGER, + # animation1webhook TEXT, + # animation2webhook TEXT, + # animation3webhook TEXT, + # lnurl_title TEXT, + # show_message INTEGER, + # show_ack INTEGER, + # show_price TEXT, + # amount_made INTEGER, + # fullscreen_cam INTEGER, + # iframe_url TEXT, + # timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + # ); + # """ + # ) + # + # await db.execute("INSERT INTO copilot.new_copilots SELECT * FROM copilot.copilots;") + # await db.execute("DROP TABLE IF EXISTS copilot.copilots;") + # await db.execute("ALTER TABLE copilot.new_copilots RENAME TO copilot.copilots;") diff --git a/lnbits/extensions/copilot/models.py b/lnbits/extensions/copilot/models.py new file mode 100644 index 000000000..70d70cf5f --- /dev/null +++ b/lnbits/extensions/copilot/models.py @@ -0,0 +1,41 @@ +from sqlite3 import Row +from typing import NamedTuple +import time +from quart import url_for +from lnurl import Lnurl, encode as lnurl_encode # type: ignore +from lnurl.types import LnurlPayMetadata # type: ignore +from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore + + +class Copilots(NamedTuple): + id: str + user: str + title: str + lnurl_toggle: int + wallet: str + animation1: str + animation2: str + animation3: str + animation1threshold: int + animation2threshold: int + animation3threshold: int + animation1webhook: str + animation2webhook: str + animation3webhook: str + lnurl_title: str + show_message: int + show_ack: int + show_price: int + amount_made: int + timestamp: int + fullscreen_cam: int + iframe_url: str + + @classmethod + def from_row(cls, row: Row) -> "Copilots": + return cls(**dict(row)) + + @property + def lnurl(self) -> Lnurl: + url = url_for("copilot.lnurl_response", cp_id=self.id, _external=True) + return lnurl_encode(url) diff --git a/lnbits/extensions/copilot/static/bitcoin.gif b/lnbits/extensions/copilot/static/bitcoin.gif new file mode 100644 index 000000000..ef8c2ecd5 Binary files /dev/null and b/lnbits/extensions/copilot/static/bitcoin.gif differ diff --git a/lnbits/extensions/copilot/static/confetti.gif b/lnbits/extensions/copilot/static/confetti.gif new file mode 100644 index 000000000..a3fec9712 Binary files /dev/null and b/lnbits/extensions/copilot/static/confetti.gif differ diff --git a/lnbits/extensions/copilot/static/face.gif b/lnbits/extensions/copilot/static/face.gif new file mode 100644 index 000000000..3e70d779c Binary files /dev/null and b/lnbits/extensions/copilot/static/face.gif differ diff --git a/lnbits/extensions/copilot/static/lnurl.png b/lnbits/extensions/copilot/static/lnurl.png new file mode 100644 index 000000000..ad2c97155 Binary files /dev/null and b/lnbits/extensions/copilot/static/lnurl.png differ diff --git a/lnbits/extensions/copilot/static/martijn.gif b/lnbits/extensions/copilot/static/martijn.gif new file mode 100644 index 000000000..e410677dc Binary files /dev/null and b/lnbits/extensions/copilot/static/martijn.gif differ diff --git a/lnbits/extensions/copilot/static/rick.gif b/lnbits/extensions/copilot/static/rick.gif new file mode 100644 index 000000000..c36c7e197 Binary files /dev/null and b/lnbits/extensions/copilot/static/rick.gif differ diff --git a/lnbits/extensions/copilot/static/rocket.gif b/lnbits/extensions/copilot/static/rocket.gif new file mode 100644 index 000000000..6f19597d0 Binary files /dev/null and b/lnbits/extensions/copilot/static/rocket.gif differ diff --git a/lnbits/extensions/copilot/tasks.py b/lnbits/extensions/copilot/tasks.py new file mode 100644 index 000000000..ff291e9ac --- /dev/null +++ b/lnbits/extensions/copilot/tasks.py @@ -0,0 +1,88 @@ +import trio # type: ignore +import json +import httpx +from quart import g, jsonify, url_for, websocket +from http import HTTPStatus + +from lnbits.core import db as core_db +from lnbits.core.models import Payment +from lnbits.tasks import register_invoice_listener + +from .crud import get_copilot +from .views import updater +import shortuuid + + +async def register_listeners(): + invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2) + register_invoice_listener(invoice_paid_chan_send) + await wait_for_paid_invoices(invoice_paid_chan_recv) + + +async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): + async for payment in invoice_paid_chan: + await on_invoice_paid(payment) + + +async def on_invoice_paid(payment: Payment) -> None: + webhook = None + data = None + if "copilot" != payment.extra.get("tag"): + # not an copilot invoice + return + + if payment.extra.get("wh_status"): + # this webhook has already been sent + return + + copilot = await get_copilot(payment.extra.get("copilot", -1)) + + if not copilot: + return ( + jsonify({"message": "Copilot link link does not exist."}), + HTTPStatus.NOT_FOUND, + ) + if copilot.animation1threshold: + if int(payment.amount / 1000) >= copilot.animation1threshold: + data = copilot.animation1 + webhook = copilot.animation1webhook + if copilot.animation2threshold: + if int(payment.amount / 1000) >= copilot.animation2threshold: + data = copilot.animation2 + webhook = copilot.animation1webhook + if copilot.animation3threshold: + if int(payment.amount / 1000) >= copilot.animation3threshold: + data = copilot.animation3 + webhook = copilot.animation1webhook + if webhook: + async with httpx.AsyncClient() as client: + try: + r = await client.post( + webhook, + json={ + "payment_hash": payment.payment_hash, + "payment_request": payment.bolt11, + "amount": payment.amount, + "comment": payment.extra.get("comment"), + }, + timeout=40, + ) + await mark_webhook_sent(payment, r.status_code) + except (httpx.ConnectError, httpx.RequestError): + await mark_webhook_sent(payment, -1) + if payment.extra.get("comment"): + await updater(copilot.id, data, payment.extra.get("comment")) + else: + await updater(copilot.id, data, "none") + + +async def mark_webhook_sent(payment: Payment, status: int) -> None: + payment.extra["wh_status"] = status + + await core_db.execute( + """ + UPDATE apipayments SET extra = ? + WHERE hash = ? + """, + (json.dumps(payment.extra), payment.payment_hash), + ) diff --git a/lnbits/extensions/copilot/templates/copilot/_api_docs.html b/lnbits/extensions/copilot/templates/copilot/_api_docs.html new file mode 100644 index 000000000..d6289be9c --- /dev/null +++ b/lnbits/extensions/copilot/templates/copilot/_api_docs.html @@ -0,0 +1,172 @@ + + +

+ StreamerCopilot: get tips via static QR (lnurl-pay) and show an + animation
+ + Created by, Ben Arc +

+
+ + + + + POST /copilot/api/v1/copilot +
Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<copilot_object>, ...] +
Curl example
+ curl -X POST {{ request.url_root }}api/v1/copilot -d '{"title": + <string>, "animation": <string>, + "show_message":<string>, "amount": <integer>, + "lnurl_title": <string>}' -H "Content-type: application/json" + -H "X-Api-Key: {{g.user.wallets[0].adminkey }}" + +
+
+
+ + + + PUT + /copilot/api/v1/copilot/<copilot_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<copilot_object>, ...] +
Curl example
+ curl -X POST {{ request.url_root + }}api/v1/copilot/<copilot_id> -d '{"title": <string>, + "animation": <string>, "show_message":<string>, + "amount": <integer>, "lnurl_title": <string>}' -H + "Content-type: application/json" -H "X-Api-Key: + {{g.user.wallets[0].adminkey }}" + +
+
+
+ + + + + GET + /copilot/api/v1/copilot/<copilot_id> +
Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<copilot_object>, ...] +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/copilot/<copilot_id> + -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+ + + + GET /copilot/api/v1/copilots +
Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<copilot_object>, ...] +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/copilots -H "X-Api-Key: {{ + g.user.wallets[0].inkey }}" + +
+
+
+ + + + DELETE + /copilot/api/v1/copilot/<copilot_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
Returns 204 NO CONTENT
+ +
Curl example
+ curl -X DELETE {{ request.url_root + }}api/v1/copilot/<copilot_id> -H "X-Api-Key: {{ + g.user.wallets[0].adminkey }}" + +
+
+
+ + + + GET + /api/v1/copilot/ws/<copilot_id>/<comment>/<data> +
Headers
+ {"X-Api-Key": <admin_key>}
+
Returns 200
+ +
Curl example
+ curl -X GET {{ request.url_root }}/api/v1/copilot/ws/<string, + copilot_id>/<string, comment>/<string, gif name> -H + "X-Api-Key: {{ g.user.wallets[0].adminkey }}" + +
+
+
+
+
diff --git a/lnbits/extensions/copilot/templates/copilot/compose.html b/lnbits/extensions/copilot/templates/copilot/compose.html new file mode 100644 index 000000000..33bffda32 --- /dev/null +++ b/lnbits/extensions/copilot/templates/copilot/compose.html @@ -0,0 +1,289 @@ +{% extends "public.html" %} {% block page %} + + + + +
+
+ +
+ {% raw %}{{ copilot.lnurl_title }}{% endraw %} +
+
+
+ +

+ {% raw %}{{ price }}{% endraw %} +

+

+ Powered by LNbits/StreamerCopilot +

+
+{% endblock %} {% block scripts %} + + + +{% endblock %} diff --git a/lnbits/extensions/copilot/templates/copilot/index.html b/lnbits/extensions/copilot/templates/copilot/index.html new file mode 100644 index 000000000..12d7058a8 --- /dev/null +++ b/lnbits/extensions/copilot/templates/copilot/index.html @@ -0,0 +1,658 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + {% raw %} + New copilot instance + + + + + + +
+
+
Copilots
+
+ +
+ + + + Export to CSV +
+
+ + + + + {% endraw %} + +
+
+
+ +
+ + +
+ {{SITE_TITLE}} StreamCopilot Extension +
+
+ + + {% include "copilot/_api_docs.html" %} + +
+
+ + + + +
+ +
+ +
+ + + + + + + +
+
+ +
+ +
+ + +
+
+ + +
+
+
+
+
+ + + + +
+
+ +
+ +
+ + +
+
+ + +
+
+
+
+
+ + + + +
+
+ +
+ +
+ + +
+
+ + +
+
+
+
+
+ + + +
+ +
+ +
+
+
+ +
+
+
+ Update Copilot + Create Copilot + Cancel +
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + + + +{% endblock %} diff --git a/lnbits/extensions/copilot/templates/copilot/panel.html b/lnbits/extensions/copilot/templates/copilot/panel.html new file mode 100644 index 000000000..904ab104b --- /dev/null +++ b/lnbits/extensions/copilot/templates/copilot/panel.html @@ -0,0 +1,157 @@ +{% extends "public.html" %} {% block page %} +
+ +
+
+
+ +
+
+
+
+ Title: {% raw %} {{ copilot.title }} {% endraw %} +
+
+ +
+
+
+ + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +{% endblock %} {% block scripts %} + + +{% endblock %} diff --git a/lnbits/extensions/copilot/views.py b/lnbits/extensions/copilot/views.py new file mode 100644 index 000000000..ef313a61a --- /dev/null +++ b/lnbits/extensions/copilot/views.py @@ -0,0 +1,61 @@ +from quart import g, abort, render_template, jsonify, websocket +from http import HTTPStatus +import httpx +from collections import defaultdict +from lnbits.decorators import check_user_exists, validate_uuids +from . import copilot_ext +from .crud import get_copilot +from quart import g, abort, render_template, jsonify, websocket +from functools import wraps +import trio +import shortuuid +from . import copilot_ext + + +@copilot_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("copilot/index.html", user=g.user) + + +@copilot_ext.route("/cp/") +async def compose(): + return await render_template("copilot/compose.html") + + +@copilot_ext.route("/pn/") +async def panel(): + return await render_template("copilot/panel.html") + + +##################WEBSOCKET ROUTES######################## + +# socket_relay is a list where the control panel or +# lnurl endpoints can leave a message for the compose window + +connected_websockets = defaultdict(set) + + +@copilot_ext.websocket("/ws//") +async def wss(id): + copilot = await get_copilot(id) + if not copilot: + return "", HTTPStatus.FORBIDDEN + global connected_websockets + send_channel, receive_channel = trio.open_memory_channel(0) + connected_websockets[id].add(send_channel) + try: + while True: + data = await receive_channel.receive() + await websocket.send(data) + finally: + connected_websockets[id].remove(send_channel) + + +async def updater(copilot_id, data, comment): + copilot = await get_copilot(copilot_id) + if not copilot: + return + for queue in connected_websockets[copilot_id]: + await queue.send(f"{data + '-' + comment}") diff --git a/lnbits/extensions/copilot/views_api.py b/lnbits/extensions/copilot/views_api.py new file mode 100644 index 000000000..bf3b4eb75 --- /dev/null +++ b/lnbits/extensions/copilot/views_api.py @@ -0,0 +1,109 @@ +import hashlib +from quart import g, jsonify, url_for, websocket +from http import HTTPStatus +import httpx + +from lnbits.core.crud import get_user +from lnbits.decorators import api_check_wallet_key, api_validate_post_request +from .views import updater + +from . import copilot_ext + +from lnbits.extensions.copilot import copilot_ext +from .crud import ( + create_copilot, + update_copilot, + get_copilot, + get_copilots, + delete_copilot, +) + +#######################COPILOT########################## + + +@copilot_ext.route("/api/v1/copilot", methods=["POST"]) +@copilot_ext.route("/api/v1/copilot/", methods=["PUT"]) +@api_check_wallet_key("admin") +@api_validate_post_request( + schema={ + "title": {"type": "string", "empty": False, "required": True}, + "lnurl_toggle": {"type": "integer", "empty": False}, + "wallet": {"type": "string", "empty": False, "required": False}, + "animation1": {"type": "string", "empty": True, "required": False}, + "animation2": {"type": "string", "empty": True, "required": False}, + "animation3": {"type": "string", "empty": True, "required": False}, + "animation1threshold": {"type": "integer", "empty": True, "required": False}, + "animation2threshold": {"type": "integer", "empty": True, "required": False}, + "animation3threshold": {"type": "integer", "empty": True, "required": False}, + "animation1webhook": {"type": "string", "empty": True, "required": False}, + "animation2webhook": {"type": "string", "empty": True, "required": False}, + "animation3webhook": {"type": "string", "empty": True, "required": False}, + "lnurl_title": {"type": "string", "empty": True, "required": False}, + "show_message": {"type": "integer", "empty": True, "required": False}, + "show_ack": {"type": "integer", "empty": True}, + "show_price": {"type": "string", "empty": True}, + } +) +async def api_copilot_create_or_update(copilot_id=None): + if not copilot_id: + copilot = await create_copilot(user=g.wallet.user, **g.data) + return jsonify(copilot._asdict()), HTTPStatus.CREATED + else: + copilot = await update_copilot(copilot_id=copilot_id, **g.data) + return jsonify(copilot._asdict()), HTTPStatus.OK + + +@copilot_ext.route("/api/v1/copilot", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_copilots_retrieve(): + try: + return ( + jsonify( + [{**copilot._asdict()} for copilot in await get_copilots(g.wallet.user)] + ), + HTTPStatus.OK, + ) + except: + return "" + + +@copilot_ext.route("/api/v1/copilot/", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_copilot_retrieve(copilot_id): + copilot = await get_copilot(copilot_id) + if not copilot: + return jsonify({"message": "copilot does not exist"}), HTTPStatus.NOT_FOUND + if not copilot.lnurl_toggle: + return ( + jsonify({**copilot._asdict()}), + HTTPStatus.OK, + ) + return ( + jsonify({**copilot._asdict(), **{"lnurl": copilot.lnurl}}), + HTTPStatus.OK, + ) + + +@copilot_ext.route("/api/v1/copilot/", methods=["DELETE"]) +@api_check_wallet_key("admin") +async def api_copilot_delete(copilot_id): + copilot = await get_copilot(copilot_id) + + if not copilot: + return jsonify({"message": "Wallet link does not exist."}), HTTPStatus.NOT_FOUND + + await delete_copilot(copilot_id) + + return "", HTTPStatus.NO_CONTENT + + +@copilot_ext.route("/api/v1/copilot/ws///", methods=["GET"]) +async def api_copilot_ws_relay(copilot_id, comment, data): + copilot = await get_copilot(copilot_id) + if not copilot: + return jsonify({"message": "copilot does not exist"}), HTTPStatus.NOT_FOUND + try: + await updater(copilot_id, data, comment) + except: + return "", HTTPStatus.FORBIDDEN + return "", HTTPStatus.OK