3
lnbits/extensions/copilot/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# StreamerCopilot
|
||||||
|
|
||||||
|
Tool to help streamers accept sats for tips
|
37
lnbits/extensions/copilot/__init__.py
Normal file
|
@ -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))
|
8
lnbits/extensions/copilot/config.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "Streamer Copilot",
|
||||||
|
"short_description": "Video tips/animations/webhooks",
|
||||||
|
"icon": "face",
|
||||||
|
"contributors": [
|
||||||
|
"arcbtc"
|
||||||
|
]
|
||||||
|
}
|
91
lnbits/extensions/copilot/crud.py
Normal file
|
@ -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,))
|
92
lnbits/extensions/copilot/lnurl.py
Normal file
|
@ -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()
|
76
lnbits/extensions/copilot/migrations.py
Normal file
|
@ -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;")
|
68
lnbits/extensions/copilot/models.py
Normal file
|
@ -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)
|
BIN
lnbits/extensions/copilot/static/bitcoin.gif
Normal file
After Width: | Height: | Size: 308 KiB |
BIN
lnbits/extensions/copilot/static/confetti.gif
Normal file
After Width: | Height: | Size: 333 KiB |
BIN
lnbits/extensions/copilot/static/face.gif
Normal file
After Width: | Height: | Size: 536 KiB |
BIN
lnbits/extensions/copilot/static/lnurl.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
lnbits/extensions/copilot/static/martijn.gif
Normal file
After Width: | Height: | Size: 504 KiB |
BIN
lnbits/extensions/copilot/static/rick.gif
Normal file
After Width: | Height: | Size: 2.3 MiB |
BIN
lnbits/extensions/copilot/static/rocket.gif
Normal file
After Width: | Height: | Size: 577 KiB |
87
lnbits/extensions/copilot/tasks.py
Normal file
|
@ -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),
|
||||||
|
)
|
172
lnbits/extensions/copilot/templates/copilot/_api_docs.html
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<p>
|
||||||
|
StreamerCopilot: get tips via static QR (lnurl-pay) and show an
|
||||||
|
animation<br />
|
||||||
|
<small>
|
||||||
|
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
<q-expansion-item
|
||||||
|
group="extras"
|
||||||
|
icon="swap_vertical_circle"
|
||||||
|
label="API info"
|
||||||
|
:content-inset-level="0.5"
|
||||||
|
>
|
||||||
|
<q-expansion-item group="api" dense expand-separator label="Create copilot">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-blue">POST</span> /copilot/api/v1/copilot</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Body (application/json)
|
||||||
|
</h5>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Returns 200 OK (application/json)
|
||||||
|
</h5>
|
||||||
|
<code>[<copilot_object>, ...]</code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>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 }}"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-expansion-item group="api" dense expand-separator label="Update copilot">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-blue">PUT</span>
|
||||||
|
/copilot/api/v1/copilot/<copilot_id></code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Body (application/json)
|
||||||
|
</h5>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Returns 200 OK (application/json)
|
||||||
|
</h5>
|
||||||
|
<code>[<copilot_object>, ...]</code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>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 }}"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item group="api" dense expand-separator label="Get copilot">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-blue">GET</span>
|
||||||
|
/copilot/api/v1/copilot/<copilot_id></code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Body (application/json)
|
||||||
|
</h5>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Returns 200 OK (application/json)
|
||||||
|
</h5>
|
||||||
|
<code>[<copilot_object>, ...]</code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>curl -X GET {{ request.url_root }}api/v1/copilot/<copilot_id>
|
||||||
|
-H "X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-expansion-item group="api" dense expand-separator label="Get copilots">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-blue">GET</span> /copilot/api/v1/copilots</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Body (application/json)
|
||||||
|
</h5>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Returns 200 OK (application/json)
|
||||||
|
</h5>
|
||||||
|
<code>[<copilot_object>, ...]</code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>curl -X GET {{ request.url_root }}api/v1/copilots -H "X-Api-Key: {{
|
||||||
|
g.user.wallets[0].inkey }}"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-expansion-item
|
||||||
|
group="api"
|
||||||
|
dense
|
||||||
|
expand-separator
|
||||||
|
label="Delete a pay link"
|
||||||
|
class="q-pb-md"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-pink">DELETE</span>
|
||||||
|
/copilot/api/v1/copilot/<copilot_id></code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
|
||||||
|
<code></code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>curl -X DELETE {{ request.url_root
|
||||||
|
}}api/v1/copilot/<copilot_id> -H "X-Api-Key: {{
|
||||||
|
g.user.wallets[0].adminkey }}"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-expansion-item
|
||||||
|
group="api"
|
||||||
|
dense
|
||||||
|
expand-separator
|
||||||
|
label="Trigger an animation"
|
||||||
|
class="q-pb-md"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-blue">GET</span>
|
||||||
|
/api/v1/copilot/ws/<copilot_id>/<comment>/<data></code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Returns 200</h5>
|
||||||
|
<code></code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>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 }}"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
</q-expansion-item>
|
||||||
|
</q-card>
|
289
lnbits/extensions/copilot/templates/copilot/compose.html
Normal file
|
@ -0,0 +1,289 @@
|
||||||
|
{% extends "public.html" %} {% block page %}<q-page>
|
||||||
|
<video
|
||||||
|
autoplay="true"
|
||||||
|
id="videoScreen"
|
||||||
|
style="width: 100%"
|
||||||
|
class="fixed-bottom-right"
|
||||||
|
></video>
|
||||||
|
<video
|
||||||
|
autoplay="true"
|
||||||
|
id="videoCamera"
|
||||||
|
style="width: 100%"
|
||||||
|
class="fixed-bottom-right"
|
||||||
|
></video>
|
||||||
|
<img src="" style="width: 100%" id="animations" class="fixed-bottom-left" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="copilot.lnurl_toggle == 1"
|
||||||
|
class="rounded-borders column fixed-right"
|
||||||
|
style="
|
||||||
|
width: 250px;
|
||||||
|
background-color: white;
|
||||||
|
height: 300px;
|
||||||
|
margin-top: 10%;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="col">
|
||||||
|
<qrcode
|
||||||
|
:value="copilot.lnurl"
|
||||||
|
:options="{width:250}"
|
||||||
|
class="rounded-borders"
|
||||||
|
></qrcode>
|
||||||
|
<center class="absolute-bottom" style="color: black; font-size: 20px">
|
||||||
|
{% raw %}{{ copilot.lnurl_title }}{% endraw %}
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2
|
||||||
|
v-if="copilot.show_price != 0"
|
||||||
|
class="text-bold fixed-bottom-left"
|
||||||
|
style="
|
||||||
|
margin: 60px 60px;
|
||||||
|
font-size: 110px;
|
||||||
|
text-shadow: 4px 8px 4px black;
|
||||||
|
color: white;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{% raw %}{{ price }}{% endraw %}
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
v-if="copilot.show_ack != 0"
|
||||||
|
class="fixed-top"
|
||||||
|
style="
|
||||||
|
font-size: 22px;
|
||||||
|
text-shadow: 2px 4px 1px black;
|
||||||
|
color: white;
|
||||||
|
padding-left: 40%;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Powered by LNbits/StreamerCopilot
|
||||||
|
</p>
|
||||||
|
</q-page>
|
||||||
|
{% endblock %} {% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||||
|
<style>
|
||||||
|
body.body--dark .q-drawer,
|
||||||
|
body.body--dark .q-footer,
|
||||||
|
body.body--dark .q-header,
|
||||||
|
.q-drawer,
|
||||||
|
.q-footer,
|
||||||
|
.q-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.q-page {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
price: '',
|
||||||
|
counter: 1,
|
||||||
|
colours: ['teal', 'purple', 'indigo', 'pink', 'green'],
|
||||||
|
copilot: {},
|
||||||
|
animQueue: [],
|
||||||
|
queue: false,
|
||||||
|
lnurl: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showNotif: function (userMessage) {
|
||||||
|
var colour = this.colours[
|
||||||
|
Math.floor(Math.random() * this.colours.length)
|
||||||
|
]
|
||||||
|
this.$q.notify({
|
||||||
|
color: colour,
|
||||||
|
icon: 'chat_bubble_outline',
|
||||||
|
html: true,
|
||||||
|
message: '<h4 style="color: white;">' + userMessage + '</h4>',
|
||||||
|
position: 'top-left',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
},
|
||||||
|
openURL: function (url) {
|
||||||
|
return Quasar.utils.openURL(url)
|
||||||
|
},
|
||||||
|
initCamera() {
|
||||||
|
var video = document.querySelector('#videoCamera')
|
||||||
|
|
||||||
|
if (navigator.mediaDevices.getUserMedia) {
|
||||||
|
navigator.mediaDevices
|
||||||
|
.getUserMedia({video: true})
|
||||||
|
.then(function (stream) {
|
||||||
|
video.srcObject = stream
|
||||||
|
})
|
||||||
|
.catch(function (err0r) {
|
||||||
|
console.log('Something went wrong!')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initScreenShare() {
|
||||||
|
var video = document.querySelector('#videoScreen')
|
||||||
|
navigator.mediaDevices
|
||||||
|
.getDisplayMedia({video: true})
|
||||||
|
.then(function (stream) {
|
||||||
|
video.srcObject = stream
|
||||||
|
})
|
||||||
|
.catch(function (err0r) {
|
||||||
|
console.log('Something went wrong!')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
pushAnim(content) {
|
||||||
|
document.getElementById('animations').style.width = content[0]
|
||||||
|
document.getElementById('animations').src = content[1]
|
||||||
|
if (content[2] != 'none') {
|
||||||
|
self.showNotif(content[2])
|
||||||
|
}
|
||||||
|
setTimeout(function () {
|
||||||
|
document.getElementById('animations').src = ''
|
||||||
|
}, 5000)
|
||||||
|
},
|
||||||
|
launch() {
|
||||||
|
self = this
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/copilot/api/v1/copilot/ws/' +
|
||||||
|
self.copilot.id +
|
||||||
|
'/launching/rocket'
|
||||||
|
)
|
||||||
|
.then(function (response1) {
|
||||||
|
self.$q.notify({
|
||||||
|
color: 'green',
|
||||||
|
message: 'Sent!'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initCamera()
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
self = this
|
||||||
|
self.copilot = JSON.parse(localStorage.getItem('copilot'))
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/copilot/api/v1/copilot/' + self.copilot.id,
|
||||||
|
localStorage.getItem('inkey')
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.copilot = response.data
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.connectionBitStamp = new WebSocket('wss://ws.bitstamp.net')
|
||||||
|
|
||||||
|
const obj = JSON.stringify({
|
||||||
|
event: 'bts:subscribe',
|
||||||
|
data: {channel: 'live_trades_' + self.copilot.show_price}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.connectionBitStamp.onmessage = function (e) {
|
||||||
|
if (self.copilot.show_price) {
|
||||||
|
if (self.copilot.show_price == 'btcusd') {
|
||||||
|
self.price = String(
|
||||||
|
new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'USD'
|
||||||
|
}).format(JSON.parse(e.data).data.price)
|
||||||
|
)
|
||||||
|
} else if (self.copilot.show_price == 'btceur') {
|
||||||
|
self.price = String(
|
||||||
|
new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'EUR'
|
||||||
|
}).format(JSON.parse(e.data).data.price)
|
||||||
|
)
|
||||||
|
} else if (self.copilot.show_price == 'btcgbp') {
|
||||||
|
self.price = String(
|
||||||
|
new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'GBP'
|
||||||
|
}).format(JSON.parse(e.data).data.price)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.connectionBitStamp.onopen = () => this.connectionBitStamp.send(obj)
|
||||||
|
|
||||||
|
const fetch = data =>
|
||||||
|
new Promise(resolve => setTimeout(resolve, 5000, this.pushAnim(data)))
|
||||||
|
|
||||||
|
const addTask = (() => {
|
||||||
|
let pending = Promise.resolve()
|
||||||
|
const run = async data => {
|
||||||
|
try {
|
||||||
|
await pending
|
||||||
|
} finally {
|
||||||
|
return fetch(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data => (pending = run(data))
|
||||||
|
})()
|
||||||
|
|
||||||
|
if (location.protocol !== 'http:') {
|
||||||
|
localUrl =
|
||||||
|
'wss://' +
|
||||||
|
document.domain +
|
||||||
|
':' +
|
||||||
|
location.port +
|
||||||
|
'/copilot/ws/' +
|
||||||
|
self.copilot.id +
|
||||||
|
'/'
|
||||||
|
} else {
|
||||||
|
localUrl =
|
||||||
|
'ws://' +
|
||||||
|
document.domain +
|
||||||
|
':' +
|
||||||
|
location.port +
|
||||||
|
'/copilot/ws/' +
|
||||||
|
self.copilot.id +
|
||||||
|
'/'
|
||||||
|
}
|
||||||
|
this.connection = new WebSocket(localUrl)
|
||||||
|
this.connection.onmessage = function (e) {
|
||||||
|
res = e.data.split('-')
|
||||||
|
if (res[0] == 'rocket') {
|
||||||
|
addTask(['40%', '/copilot/static/rocket.gif', res[1]])
|
||||||
|
}
|
||||||
|
if (res[0] == 'face') {
|
||||||
|
addTask(['35%', '/copilot/static/face.gif', res[1]])
|
||||||
|
}
|
||||||
|
if (res[0] == 'bitcoin') {
|
||||||
|
addTask(['30%', '/copilot/static/bitcoin.gif', res[1]])
|
||||||
|
}
|
||||||
|
if (res[0] == 'confetti') {
|
||||||
|
addTask(['100%', '/copilot/static/confetti.gif', res[1]])
|
||||||
|
}
|
||||||
|
if (res[0] == 'martijn') {
|
||||||
|
addTask(['40%', '/copilot/static/martijn.gif', res[1]])
|
||||||
|
}
|
||||||
|
if (res[0] == 'rick') {
|
||||||
|
addTask(['40%', '/copilot/static/rick.gif', res[1]])
|
||||||
|
}
|
||||||
|
if (res[0] == 'true') {
|
||||||
|
document.getElementById('videoCamera').style.width = '20%'
|
||||||
|
self.initScreenShare()
|
||||||
|
}
|
||||||
|
if (res[0] == 'false') {
|
||||||
|
document.getElementById('videoCamera').style.width = '100%'
|
||||||
|
document.getElementById('videoScreen').src = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.connection.onopen = () => this.launch
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
658
lnbits/extensions/copilot/templates/copilot/index.html
Normal file
|
@ -0,0 +1,658 @@
|
||||||
|
{% 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-7 q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
{% raw %}
|
||||||
|
<q-btn unelevated color="primary" @click="formDialogCopilot.show = true"
|
||||||
|
>New copilot instance
|
||||||
|
</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">Copilots</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-input
|
||||||
|
borderless
|
||||||
|
dense
|
||||||
|
debounce="300"
|
||||||
|
v-model="filter"
|
||||||
|
placeholder="Search"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="search"></q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
<q-btn flat color="grey" @click="exportcopilotCSV"
|
||||||
|
>Export to CSV</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:data="CopilotLinks"
|
||||||
|
row-key="id"
|
||||||
|
:columns="CopilotsTable.columns"
|
||||||
|
:pagination.sync="CopilotsTable.pagination"
|
||||||
|
:filter="filter"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th style="width: 5%"></q-th>
|
||||||
|
<q-th style="width: 5%"></q-th>
|
||||||
|
<q-th style="width: 5%"></q-th>
|
||||||
|
<q-th style="width: 5%"></q-th>
|
||||||
|
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
auto-width
|
||||||
|
>
|
||||||
|
<div v-if="col.name == 'id'"></div>
|
||||||
|
<div v-else>{{ col.label }}</div>
|
||||||
|
</q-th>
|
||||||
|
<!-- <q-th auto-width></q-th> -->
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
icon="apps"
|
||||||
|
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||||
|
@click="openCopilotPanel(props.row.id)"
|
||||||
|
>
|
||||||
|
<q-tooltip> Panel </q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
icon="face"
|
||||||
|
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||||
|
@click="openCopilotCompose(props.row.id)"
|
||||||
|
>
|
||||||
|
<q-tooltip> Compose window </q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="deleteCopilotLink(props.row.id)"
|
||||||
|
icon="cancel"
|
||||||
|
color="pink"
|
||||||
|
>
|
||||||
|
<q-tooltip> Delete copilot </q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="openUpdateCopilotLink(props.row.id)"
|
||||||
|
icon="edit"
|
||||||
|
color="light-blue"
|
||||||
|
>
|
||||||
|
<q-tooltip> Edit copilot </q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
<q-td
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
auto-width
|
||||||
|
>
|
||||||
|
<div v-if="col.name == 'id'"></div>
|
||||||
|
<div v-else>{{ col.value }}</div>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
{% endraw %}
|
||||||
|
</q-table>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<h6 class="text-subtitle1 q-my-none">
|
||||||
|
{{SITE_TITLE}} StreamCopilot Extension
|
||||||
|
</h6>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-list> {% include "copilot/_api_docs.html" %} </q-list>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<q-dialog
|
||||||
|
v-model="formDialogCopilot.show"
|
||||||
|
position="top"
|
||||||
|
@hide="closeFormDialog"
|
||||||
|
>
|
||||||
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
|
<q-form @submit="sendFormDataCopilot" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialogCopilot.data.title"
|
||||||
|
type="text"
|
||||||
|
label="Title"
|
||||||
|
></q-input>
|
||||||
|
<div class="row">
|
||||||
|
<q-checkbox
|
||||||
|
v-model="formDialogCopilot.data.lnurl_toggle"
|
||||||
|
label="Include lnurl payment QR? (requires https)"
|
||||||
|
left-label
|
||||||
|
></q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="formDialogCopilot.data.lnurl_toggle">
|
||||||
|
<q-checkbox
|
||||||
|
v-model="formDialogCopilot.data.show_message"
|
||||||
|
left-label
|
||||||
|
label="Show lnurl-pay messages? (supported by few wallets)"
|
||||||
|
></q-checkbox>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="formDialogCopilot.data.wallet"
|
||||||
|
:options="g.user.walletOptions"
|
||||||
|
label="Wallet *"
|
||||||
|
></q-select>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
group="api"
|
||||||
|
dense
|
||||||
|
expand-separator
|
||||||
|
label="Payment threshold 1"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialogCopilot.data.animation1"
|
||||||
|
:options="options"
|
||||||
|
label="Animation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col q-pl-xs">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialogCopilot.data.animation1threshold"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
label="From *sats (min. 10)"
|
||||||
|
:rules="[ val => val >= 10 || 'Please use minimum 10' ]"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-pl-xs">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialogCopilot.data.animation1webhook"
|
||||||
|
type="text"
|
||||||
|
label="Webhook"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
group="api"
|
||||||
|
dense
|
||||||
|
expand-separator
|
||||||
|
label="Payment threshold 2 (Must be higher than last)"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
v-if="formDialogCopilot.data.animation1threshold > 0"
|
||||||
|
>
|
||||||
|
<div class="col">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialogCopilot.data.animation2"
|
||||||
|
:options="options"
|
||||||
|
label="Animation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col q-pl-xs">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="formDialogCopilot.data.animation2threshold"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
label="From *sats"
|
||||||
|
:min="formDialogCopilot.data.animation1threshold"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-pl-xs">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialogCopilot.data.animation2webhook"
|
||||||
|
type="text"
|
||||||
|
label="Webhook"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
group="api"
|
||||||
|
dense
|
||||||
|
expand-separator
|
||||||
|
label="Payment threshold 3 (Must be higher than last)"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
v-if="formDialogCopilot.data.animation2threshold > formDialogCopilot.data.animation1threshold"
|
||||||
|
>
|
||||||
|
<div class="col">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialogCopilot.data.animation3"
|
||||||
|
:options="options"
|
||||||
|
label="Animation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col q-pl-xs">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="formDialogCopilot.data.animation3threshold"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
label="From *sats"
|
||||||
|
:min="formDialogCopilot.data.animation2threshold"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-pl-xs">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialogCopilot.data.animation3webhook"
|
||||||
|
type="text"
|
||||||
|
label="Webhook"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialogCopilot.data.lnurl_title"
|
||||||
|
type="text"
|
||||||
|
max="1440"
|
||||||
|
label="Lnurl title (message with QR code)"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-gutter-sm">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
style="width: 50%"
|
||||||
|
v-model.trim="formDialogCopilot.data.show_price"
|
||||||
|
:options="currencyOptions"
|
||||||
|
label="Show price"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="q-gutter-sm">
|
||||||
|
<div class="row">
|
||||||
|
<q-checkbox
|
||||||
|
v-model="formDialogCopilot.data.show_ack"
|
||||||
|
left-label
|
||||||
|
label="Show 'powered by LNbits'"
|
||||||
|
></q-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
v-if="formDialogCopilot.data.id"
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="
|
||||||
|
formDialogCopilot.data.title == ''"
|
||||||
|
type="submit"
|
||||||
|
>Update Copilot</q-btn
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="
|
||||||
|
formDialogCopilot.data.title == ''"
|
||||||
|
type="submit"
|
||||||
|
>Create Copilot</q-btn
|
||||||
|
>
|
||||||
|
<q-btn @click="cancelCopilot" flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
||||||
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
|
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||||
|
<style></style>
|
||||||
|
<script>
|
||||||
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
|
var mapCopilot = obj => {
|
||||||
|
obj._data = _.clone(obj)
|
||||||
|
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
|
||||||
|
obj.time = obj.time + 'mins'
|
||||||
|
|
||||||
|
if (obj.time_elapsed) {
|
||||||
|
obj.date = 'Time elapsed'
|
||||||
|
} else {
|
||||||
|
obj.date = Quasar.utils.date.formatDate(
|
||||||
|
new Date((obj.theTime - 3600) * 1000),
|
||||||
|
'HH:mm:ss'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
obj.displayComposeUrl = ['/copilot/cp/', obj.id].join('')
|
||||||
|
obj.displayPanelUrl = ['/copilot/', obj.id].join('')
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
filter: '',
|
||||||
|
CopilotLinks: [],
|
||||||
|
CopilotLinksObj: [],
|
||||||
|
CopilotsTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'theId',
|
||||||
|
align: 'left',
|
||||||
|
label: 'id',
|
||||||
|
field: 'id'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lnurl_toggle',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Show lnurl pay link',
|
||||||
|
field: 'lnurl_toggle'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
align: 'left',
|
||||||
|
label: 'title',
|
||||||
|
field: 'title'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amount_made',
|
||||||
|
align: 'left',
|
||||||
|
label: 'amount made',
|
||||||
|
field: 'amount_made'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
passedCopilot: {},
|
||||||
|
formDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {}
|
||||||
|
},
|
||||||
|
formDialogCopilot: {
|
||||||
|
show: false,
|
||||||
|
data: {
|
||||||
|
lnurl_toggle: false,
|
||||||
|
show_message: false,
|
||||||
|
show_ack: false,
|
||||||
|
show_price: 'None',
|
||||||
|
title: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
qrCodeDialog: {
|
||||||
|
show: false,
|
||||||
|
data: null
|
||||||
|
},
|
||||||
|
options: ['bitcoin', 'confetti', 'rocket', 'face', 'martijn', 'rick'],
|
||||||
|
currencyOptions: ['None', 'btcusd', 'btceur', 'btcgbp']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
cancelCopilot: function (data) {
|
||||||
|
var self = this
|
||||||
|
self.formDialogCopilot.show = false
|
||||||
|
self.clearFormDialogCopilot()
|
||||||
|
},
|
||||||
|
closeFormDialog: function () {
|
||||||
|
this.clearFormDialogCopilot()
|
||||||
|
this.formDialog.data = {
|
||||||
|
is_unique: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sendFormDataCopilot: function () {
|
||||||
|
var self = this
|
||||||
|
if (self.formDialogCopilot.data.id) {
|
||||||
|
this.updateCopilot(
|
||||||
|
self.g.user.wallets[0].adminkey,
|
||||||
|
self.formDialogCopilot.data
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.createCopilot(
|
||||||
|
self.g.user.wallets[0].adminkey,
|
||||||
|
self.formDialogCopilot.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createCopilot: function (wallet, data) {
|
||||||
|
var self = this
|
||||||
|
var updatedData = {}
|
||||||
|
for (const property in data) {
|
||||||
|
if (data[property]) {
|
||||||
|
updatedData[property] = data[property]
|
||||||
|
}
|
||||||
|
if (property == 'animation1threshold' && data[property]) {
|
||||||
|
updatedData[property] = parseInt(data[property])
|
||||||
|
}
|
||||||
|
if (property == 'animation2threshold' && data[property]) {
|
||||||
|
updatedData[property] = parseInt(data[property])
|
||||||
|
}
|
||||||
|
if (property == 'animation3threshold' && data[property]) {
|
||||||
|
updatedData[property] = parseInt(data[property])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LNbits.api
|
||||||
|
.request('POST', '/copilot/api/v1/copilot', wallet, updatedData)
|
||||||
|
.then(function (response) {
|
||||||
|
self.CopilotLinks.push(mapCopilot(response.data))
|
||||||
|
self.formDialogCopilot.show = false
|
||||||
|
self.clearFormDialogCopilot()
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getCopilots: function () {
|
||||||
|
var self = this
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/copilot/api/v1/copilot',
|
||||||
|
this.g.user.wallets[0].inkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
if(response.data){
|
||||||
|
self.CopilotLinks = response.data.map(mapCopilot)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getCopilot: function (copilot_id) {
|
||||||
|
var self = this
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/copilot/api/v1/copilot/' + copilot_id,
|
||||||
|
this.g.user.wallets[0].inkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
localStorage.setItem('copilot', JSON.stringify(response.data))
|
||||||
|
localStorage.setItem('inkey', self.g.user.wallets[0].inkey)
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
openCopilotCompose: function (copilot_id) {
|
||||||
|
this.getCopilot(copilot_id)
|
||||||
|
let params =
|
||||||
|
'scrollbars=no, resizable=no,status=no,location=no,toolbar=no,menubar=no,width=1200,height=644,left=410,top=100'
|
||||||
|
open('../copilot/cp/', '_blank', params)
|
||||||
|
},
|
||||||
|
openCopilotPanel: function (copilot_id) {
|
||||||
|
this.getCopilot(copilot_id)
|
||||||
|
let params =
|
||||||
|
'scrollbars=no, resizable=no,status=no,location=no,toolbar=no,menubar=no,width=300,height=450,left=10,top=400'
|
||||||
|
open('../copilot/pn/', '_blank', params)
|
||||||
|
},
|
||||||
|
deleteCopilotLink: function (copilotId) {
|
||||||
|
var self = this
|
||||||
|
var link = _.findWhere(this.CopilotLinks, {id: copilotId})
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog('Are you sure you want to delete this pay link?')
|
||||||
|
.onOk(function () {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'DELETE',
|
||||||
|
'/copilot/api/v1/copilot/' + copilotId,
|
||||||
|
self.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.CopilotLinks = _.reject(self.CopilotLinks, function (obj) {
|
||||||
|
return obj.id === copilotId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
openUpdateCopilotLink: function (copilotId) {
|
||||||
|
var self = this
|
||||||
|
var copilot = _.findWhere(this.CopilotLinks, {id: copilotId})
|
||||||
|
self.formDialogCopilot.data = _.clone(copilot._data)
|
||||||
|
self.formDialogCopilot.show = true
|
||||||
|
},
|
||||||
|
updateCopilot: function (wallet, data) {
|
||||||
|
var self = this
|
||||||
|
var updatedData = {}
|
||||||
|
for (const property in data) {
|
||||||
|
if (data[property]) {
|
||||||
|
updatedData[property] = data[property]
|
||||||
|
}
|
||||||
|
if (property == 'animation1threshold' && data[property]) {
|
||||||
|
updatedData[property] = parseInt(data[property])
|
||||||
|
}
|
||||||
|
if (property == 'animation2threshold' && data[property]) {
|
||||||
|
updatedData[property] = parseInt(data[property])
|
||||||
|
}
|
||||||
|
if (property == 'animation3threshold' && data[property]) {
|
||||||
|
updatedData[property] = parseInt(data[property])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'PUT',
|
||||||
|
'/copilot/api/v1/copilot/' + updatedData.id,
|
||||||
|
wallet,
|
||||||
|
updatedData
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.CopilotLinks = _.reject(self.CopilotLinks, function (obj) {
|
||||||
|
return obj.id === updatedData.id
|
||||||
|
})
|
||||||
|
self.CopilotLinks.push(mapCopilot(response.data))
|
||||||
|
self.formDialogCopilot.show = false
|
||||||
|
self.clearFormDialogCopilot()
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clearFormDialogCopilot(){
|
||||||
|
this.formDialogCopilot.data = {
|
||||||
|
lnurl_toggle: false,
|
||||||
|
show_message: false,
|
||||||
|
show_ack: false,
|
||||||
|
show_price: 'None',
|
||||||
|
title: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exportcopilotCSV: function () {
|
||||||
|
var self = this
|
||||||
|
LNbits.utils.exportCSV(self.CopilotsTable.columns, this.CopilotLinks)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
var self = this
|
||||||
|
var getCopilots = this.getCopilots
|
||||||
|
getCopilots()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
157
lnbits/extensions/copilot/templates/copilot/panel.html
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
{% extends "public.html" %} {% block page %}
|
||||||
|
<div class="q-pa-sm" style="width: 240px; margin: 10px auto">
|
||||||
|
<q-card class="my-card">
|
||||||
|
<div class="column">
|
||||||
|
<div class="col">
|
||||||
|
<center>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
@click="openCompose"
|
||||||
|
icon="face"
|
||||||
|
style="font-size: 60px"
|
||||||
|
></q-btn>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
<center>
|
||||||
|
<div class="col" style="margin: 15px; font-size: 22px">
|
||||||
|
Title: {% raw %} {{ copilot.title }} {% endraw %}
|
||||||
|
</div>
|
||||||
|
</center>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<div class="col">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-btn
|
||||||
|
class="q-mt-sm q-ml-sm"
|
||||||
|
color="primary"
|
||||||
|
@click="fullscreenToggle"
|
||||||
|
label="Screen share"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-pa-sm">
|
||||||
|
<div class="col">
|
||||||
|
<q-btn
|
||||||
|
style="width: 95%"
|
||||||
|
color="primary"
|
||||||
|
@click="animationBTN('rocket')"
|
||||||
|
label="rocket"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-btn
|
||||||
|
style="width: 95%"
|
||||||
|
color="primary"
|
||||||
|
@click="animationBTN('confetti')"
|
||||||
|
label="confetti"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-btn
|
||||||
|
style="width: 95%"
|
||||||
|
color="primary"
|
||||||
|
@click="animationBTN('face')"
|
||||||
|
label="face"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-pa-sm">
|
||||||
|
<div class="col">
|
||||||
|
<q-btn
|
||||||
|
style="width: 95%"
|
||||||
|
color="primary"
|
||||||
|
@click="animationBTN('rick')"
|
||||||
|
label="rick"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-btn
|
||||||
|
style="width: 95%"
|
||||||
|
color="primary"
|
||||||
|
@click="animationBTN('martijn')"
|
||||||
|
label="martijn"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-btn
|
||||||
|
style="width: 95%"
|
||||||
|
color="primary"
|
||||||
|
@click="animationBTN('bitcoin')"
|
||||||
|
label="bitcoin"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %} {% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
fullscreen_cam: true,
|
||||||
|
textareaModel: '',
|
||||||
|
iframe: '',
|
||||||
|
copilot: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
iframeChange: function (url) {
|
||||||
|
this.connection.send(String(url))
|
||||||
|
},
|
||||||
|
fullscreenToggle: function () {
|
||||||
|
self = this
|
||||||
|
self.animationBTN(String(this.fullscreen_cam))
|
||||||
|
if (this.fullscreen_cam) {
|
||||||
|
this.fullscreen_cam = false
|
||||||
|
} else {
|
||||||
|
this.fullscreen_cam = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openCompose: function () {
|
||||||
|
let params =
|
||||||
|
'scrollbars=no, resizable=no,status=no,location=no,toolbar=no,menubar=no,width=1200,height=644,left=410,top=100'
|
||||||
|
open('../cp/', 'test', params)
|
||||||
|
},
|
||||||
|
animationBTN: function (name) {
|
||||||
|
self = this
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/copilot/api/v1/copilot/ws/' + self.copilot.id + '/none/' + name
|
||||||
|
)
|
||||||
|
.then(function (response1) {
|
||||||
|
self.$q.notify({
|
||||||
|
color: 'green',
|
||||||
|
message: 'Sent!'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
self = this
|
||||||
|
self.copilot = JSON.parse(localStorage.getItem('copilot'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
76
lnbits/extensions/copilot/views.py
Normal file
|
@ -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}")
|
115
lnbits/extensions/copilot/views_api.py
Normal file
|
@ -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 ""
|
|
@ -1,9 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from fastapi import APIRouter, FastAPI
|
from fastapi import APIRouter, FastAPI
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from starlette.routing import Mount
|
from starlette.routing import Mount
|
||||||
|
|
||||||
from lnbits.db import Database
|
from lnbits.db import Database
|
||||||
from lnbits.helpers import template_renderer
|
from lnbits.helpers import template_renderer
|
||||||
from lnbits.tasks import catch_everything_and_restart
|
from lnbits.tasks import catch_everything_and_restart
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from lnbits.decorators import check_user_exists, WalletTypeInfo, get_key_type
|
from lnbits.decorators import check_user_exists, WalletTypeInfo, get_key_type
|
||||||
|
|