diff --git a/lnbits/extensions/copilot/README.md b/lnbits/extensions/copilot/README.md new file mode 100644 index 00000000..323aeddc --- /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 00000000..94d1e74c --- /dev/null +++ b/lnbits/extensions/copilot/__init__.py @@ -0,0 +1,37 @@ +import asyncio +from fastapi import APIRouter, FastAPI +from fastapi.staticfiles import StaticFiles +from starlette.routing import Mount +from lnbits.db import Database +from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart + +db = Database("ext_copilot") + +copilot_static_files = [ + { + "path": "/copilot/static", + "app": StaticFiles(directory="lnbits/extensions/copilot/static"), + "name": "copilot_static", + } +] +copilot_ext: APIRouter = APIRouter(prefix="/copilot", tags=["copilot"]) + + +def copilot_renderer(): + return template_renderer( + [ + "lnbits/extensions/copilot/templates", + ] + ) + + +from .views_api import * # noqa +from .views import * # noqa +from .tasks import wait_for_paid_invoices +from .lnurl import * # noqa + + +def copilot_start(): + loop = asyncio.get_event_loop() + loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/copilot/config.json b/lnbits/extensions/copilot/config.json new file mode 100644 index 00000000..a4ecb3b5 --- /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 00000000..ce4a2804 --- /dev/null +++ b/lnbits/extensions/copilot/crud.py @@ -0,0 +1,91 @@ +from typing import List, Optional, Union + +from . import db +from .models import Copilots, CreateCopilotData +from lnbits.helpers import urlsafe_short_hash + +###############COPILOTS########################## + + +async def create_copilot( + data: CreateCopilotData, inkey: Optional[str] = "" +) -> 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + data.copilot_id, + data.user, + int(data.lnurl_toggle), + data.wallet, + data.title, + data.animation1, + data.animation2, + data.animation3, + data.animation1threshold, + data.animation2threshold, + data.animation3threshold, + data.animation1webhook, + data.animation2webhook, + data.animation3webhook, + data.lnurl_title, + int(data.show_message), + int(data.show_ack), + data.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 00000000..518b07f3 --- /dev/null +++ b/lnbits/extensions/copilot/lnurl.py @@ -0,0 +1,92 @@ +import json +import hashlib +import math +from fastapi import Request +import hashlib +from http import HTTPStatus + +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse, JSONResponse # type: ignore +import base64 +from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore +from lnurl.types import LnurlPayMetadata +from lnbits.core.services import create_invoice +from .models import Copilots, CreateCopilotData +from . import copilot_ext +from .crud import get_copilot +from typing import Optional +from fastapi.params import Depends +from fastapi.param_functions import Query + + +@copilot_ext.get("/lnurl/{cp_id}", response_class=HTMLResponse) +async def lnurl_response(req: Request, cp_id: str = Query(None)): + cp = await get_copilot(cp_id) + if not cp: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Copilot not found", + ) + + resp = LnurlPayResponse( + callback=req.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 params + + +@copilot_ext.get("/lnurl/cb/{cp_id}", response_class=HTMLResponse) +async def lnurl_callback( + cp_id: str = Query(None), amount: str = Query(None), comment: str = Query(None) +): + cp = await get_copilot(cp_id) + if not cp: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Copilot not found", + ) + + amount_received = int(amount) + + if amount_received < 10000: + return LnurlErrorResponse( + reason=f"Amount {round(amount_received / 1000)} is smaller than minimum 10 sats." + ).dict() + elif amount_received / 1000 > 10000000: + return LnurlErrorResponse( + reason=f"Amount {round(amount_received / 1000)} is greater than maximum 50000." + ).dict() + comment = "" + if comment: + if len(comment or "") > 300: + return 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 resp.dict() diff --git a/lnbits/extensions/copilot/migrations.py b/lnbits/extensions/copilot/migrations.py new file mode 100644 index 00000000..c1fbfc0d --- /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 00000000..230825a0 --- /dev/null +++ b/lnbits/extensions/copilot/models.py @@ -0,0 +1,68 @@ +import json +from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult +from starlette.requests import Request +from fastapi.param_functions import Query +from typing import Optional, Dict +from lnbits.lnurl import encode as lnurl_encode # type: ignore +from lnurl.types import LnurlPayMetadata # type: ignore +from sqlite3 import Row +from pydantic import BaseModel + + +class CreateCopilotData(BaseModel): + id: str = Query(None) + user: str = Query(None) + title: str = Query(None) + lnurl_toggle: int = Query(None) + wallet: str = Query(None) + animation1: str = Query(None) + animation2: str = Query(None) + animation3: str = Query(None) + animation1threshold: int = Query(None) + animation2threshold: int = Query(None) + animation3threshold: int = Query(None) + animation1webhook: str = Query(None) + animation2webhook: str = Query(None) + animation3webhook: str = Query(None) + lnurl_title: str = Query(None) + show_message: int = Query(None) + show_ack: int = Query(None) + show_price: int = Query(None) + amount_made: int = Query(None) + timestamp: int = Query(None) + fullscreen_cam: int = Query(None) + iframe_url: str = Query(None) + success_url: str = Query(None) + + +class Copilots(BaseModel): + 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)) + + def lnurl(self, req: Request) -> str: + url = req.url_for("copilot.lnurl_response", link_id=self.id) + 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 00000000..ef8c2ecd 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 00000000..a3fec971 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 00000000..3e70d779 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 00000000..ad2c9715 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 00000000..e410677d 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 00000000..c36c7e19 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 00000000..6f19597d 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 00000000..ea678222 --- /dev/null +++ b/lnbits/extensions/copilot/tasks.py @@ -0,0 +1,87 @@ +import asyncio +import json +import httpx + +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 +from http import HTTPStatus +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse, JSONResponse # type: ignore + + +async def wait_for_paid_invoices(): + invoice_queue = asyncio.Queue() + register_invoice_listener(invoice_queue) + + while True: + payment = await invoice_queue.get() + 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: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Copilot does not exist", + ) + 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 00000000..d6289be9 --- /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 00000000..33bffda3 --- /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 00000000..12d7058a --- /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 00000000..904ab104 --- /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 00000000..7809c99d --- /dev/null +++ b/lnbits/extensions/copilot/views.py @@ -0,0 +1,76 @@ +from http import HTTPStatus +import httpx +from collections import defaultdict +from lnbits.decorators import check_user_exists +import asyncio +from .crud import get_copilot + +from functools import wraps + +from lnbits.decorators import check_user_exists + +from . import copilot_ext, copilot_renderer +from fastapi import FastAPI, Request, WebSocket +from fastapi.params import Depends +from fastapi.templating import Jinja2Templates +from fastapi.param_functions import Query +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse, JSONResponse # type: ignore +from lnbits.core.models import User +import base64 + + +templates = Jinja2Templates(directory="templates") + + +@copilot_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return copilot_renderer().TemplateResponse( + "copilot/index.html", {"request": request, "user": user.dict()} + ) + + +@copilot_ext.get("/cp/", response_class=HTMLResponse) +async def compose(request: Request): + return copilot_renderer().TemplateResponse( + "copilot/compose.html", {"request": request} + ) + + +@copilot_ext.get("/pn/", response_class=HTMLResponse) +async def panel(request: Request): + return copilot_renderer().TemplateResponse( + "copilot/panel.html", {"request": request} + ) + + +##################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/{id}/") +async def websocket_endpoint(websocket: WebSocket, id: str = Query(None)): + copilot = await get_copilot(id) + if not copilot: + return "", HTTPStatus.FORBIDDEN + await websocket.accept() + invoice_queue = asyncio.Queue() + connected_websockets[id].add(invoice_queue) + try: + while True: + data = await websocket.receive_text() + await websocket.send_text(f"Message text was: {data}") + finally: + connected_websockets[id].remove(invoice_queue) + + +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 00000000..b256b102 --- /dev/null +++ b/lnbits/extensions/copilot/views_api.py @@ -0,0 +1,115 @@ +from fastapi import Request +import hashlib +from http import HTTPStatus +from starlette.exceptions import HTTPException + +from starlette.responses import HTMLResponse, JSONResponse # type: ignore +import base64 +from lnbits.core.crud import get_user +from lnbits.core.services import create_invoice, check_invoice_status +import json +from typing import Optional +from fastapi.params import Depends +from fastapi.param_functions import Query +from .models import Copilots, CreateCopilotData +from lnbits.decorators import ( + WalletAdminKeyChecker, + WalletInvoiceKeyChecker, + api_validate_post_request, + check_user_exists, + WalletTypeInfo, + get_key_type, + api_validate_post_request, +) +from .views import updater +import httpx +from . import copilot_ext +from .crud import ( + create_copilot, + update_copilot, + get_copilot, + get_copilots, + delete_copilot, +) + +#######################COPILOT########################## + + +@copilot_ext.post("/api/v1/copilot", response_class=HTMLResponse) +@copilot_ext.put("/api/v1/copilot/{juke_id}", response_class=HTMLResponse) +async def api_copilot_create_or_update( + data: CreateCopilotData, + copilot_id: str = Query(None), + wallet: WalletTypeInfo = Depends(get_key_type), +): + if not copilot_id: + copilot = await create_copilot(data, user=wallet.wallet.user) + return copilot, HTTPStatus.CREATED + else: + copilot = await update_copilot(data, copilot_id=copilot_id) + return copilot + + +@copilot_ext.get("/api/v1/copilot", response_class=HTMLResponse) +async def api_copilots_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)): + try: + return [{copilot} for copilot in await get_copilots(wallet.wallet.user)] + except: + return "" + + +@copilot_ext.get("/api/v1/copilot/{copilot_id}", response_class=HTMLResponse) +async def api_copilot_retrieve( + copilot_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) +): + copilot = await get_copilot(copilot_id) + if not copilot: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Copilot not found", + ) + if not copilot.lnurl_toggle: + return copilot.dict() + return {**copilot.dict(), **{"lnurl": copilot.lnurl}} + + +@copilot_ext.delete("/api/v1/copilot/{copilot_id}", response_class=HTMLResponse) +async def api_copilot_delete( + copilot_id: str = Query(None), + wallet: WalletTypeInfo = Depends(get_key_type), +): + copilot = await get_copilot(copilot_id) + + if not copilot: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Copilot does not exist", + ) + + await delete_copilot(copilot_id) + + return "", HTTPStatus.NO_CONTENT + + +@copilot_ext.get( + "/api/v1/copilot/ws/{copilot_id}/{comment}/{data}", response_class=HTMLResponse +) +async def api_copilot_ws_relay( + copilot_id: str = Query(None), + comment: str = Query(None), + data: str = Query(None), +): + copilot = await get_copilot(copilot_id) + if not copilot: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Copilot does not exist", + ) + try: + await updater(copilot_id, data, comment) + except: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Not your copilot", + ) + return "" diff --git a/lnbits/extensions/jukebox/__init__.py b/lnbits/extensions/jukebox/__init__.py index f38b0ec7..93e1157a 100644 --- a/lnbits/extensions/jukebox/__init__.py +++ b/lnbits/extensions/jukebox/__init__.py @@ -1,9 +1,7 @@ import asyncio - from fastapi import APIRouter, FastAPI from fastapi.staticfiles import StaticFiles from starlette.routing import Mount - from lnbits.db import Database from lnbits.helpers import template_renderer from lnbits.tasks import catch_everything_and_restart diff --git a/lnbits/extensions/jukebox/views.py b/lnbits/extensions/jukebox/views.py index b0bd0640..230a61e3 100644 --- a/lnbits/extensions/jukebox/views.py +++ b/lnbits/extensions/jukebox/views.py @@ -1,5 +1,6 @@ import json import time + from datetime import datetime from http import HTTPStatus from lnbits.decorators import check_user_exists, WalletTypeInfo, get_key_type