Merge pull request #1167 from lnbits/switchtounisocket

Replaces extension specific websockets with the new generic websocket
This commit is contained in:
Arc
2022-12-01 19:12:48 +00:00
committed by GitHub
10 changed files with 29 additions and 164 deletions

View File

@@ -9,53 +9,10 @@ nav_order: 2
Websockets
=================
`websockets` are a great way to add a two way instant data channel between server and client. This example was taken from the `copilot` extension, we create a websocket endpoint which can be restricted by `id`, then can feed it data to broadcast to any client on the socket using the `updater(extension_id, data)` function (`extension` has been used in place of an extension name, wreplace to your own extension):
`websockets` are a great way to add a two way instant data channel between server and client.
LNbits has a useful in built websocket tool. With a websocket client connect to (obv change `somespecificid`) `wss://legend.lnbits.com/api/v1/ws/somespecificid` (you can use an online websocket tester). Now make a get to `https://legend.lnbits.com/api/v1/ws/somespecificid/somedata`. You can send data to that websocket by using `from lnbits.core.services import websocketUpdater` and the function `websocketUpdater("somespecificid", "somdata")`.
```sh
from fastapi import Request, WebSocket, WebSocketDisconnect
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket, extension_id: str):
await websocket.accept()
websocket.id = extension_id
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def send_personal_message(self, message: str, extension_id: str):
for connection in self.active_connections:
if connection.id == extension_id:
await connection.send_text(message)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@extension_ext.websocket("/ws/{extension_id}", name="extension.websocket_by_id")
async def websocket_endpoint(websocket: WebSocket, extension_id: str):
await manager.connect(websocket, extension_id)
try:
while True:
data = await websocket.receive_text()
except WebSocketDisconnect:
manager.disconnect(websocket)
async def updater(extension_id, data):
extension = await get_extension(extension_id)
if not extension:
return
await manager.send_personal_message(f"{data}", extension_id)
```
Example vue-js function for listening to the websocket:
@@ -67,16 +24,16 @@ initWs: async function () {
document.domain +
':' +
location.port +
'/extension/ws/' +
self.extension.id
'/api/v1/ws/' +
self.item.id
} else {
localUrl =
'ws://' +
document.domain +
':' +
location.port +
'/extension/ws/' +
self.extension.id
'/api/v1/ws/' +
self.item.id
}
this.ws = new WebSocket(localUrl)
this.ws.addEventListener('message', async ({data}) => {

View File

@@ -166,7 +166,7 @@ def lnencode(addr, privkey):
if addr.amount:
amount = Decimal(str(addr.amount))
# We can only send down to millisatoshi.
if amount * 10 ** 12 % 10:
if amount * 10**12 % 10:
raise ValueError(
"Cannot encode {}: too many decimal places".format(addr.amount)
)
@@ -271,7 +271,7 @@ class LnAddr(object):
def shorten_amount(amount):
"""Given an amount in bitcoin, shorten it"""
# Convert to pico initially
amount = int(amount * 10 ** 12)
amount = int(amount * 10**12)
units = ["p", "n", "u", "m", ""]
for unit in units:
if amount % 1000 == 0:
@@ -290,7 +290,7 @@ def _unshorten_amount(amount: str) -> int:
# * `u` (micro): multiply by 0.000001
# * `n` (nano): multiply by 0.000000001
# * `p` (pico): multiply by 0.000000000001
units = {"p": 10 ** 12, "n": 10 ** 9, "u": 10 ** 6, "m": 10 ** 3}
units = {"p": 10**12, "n": 10**9, "u": 10**6, "m": 10**3}
unit = str(amount)[-1]
# BOLT #11:

View File

@@ -329,12 +329,12 @@ async def perform_lnurlauth(
sign_len = 6 + r_len + s_len
signature = BytesIO()
signature.write(0x30 .to_bytes(1, "big", signed=False))
signature.write(0x30.to_bytes(1, "big", signed=False))
signature.write((sign_len - 2).to_bytes(1, "big", signed=False))
signature.write(0x02 .to_bytes(1, "big", signed=False))
signature.write(0x02.to_bytes(1, "big", signed=False))
signature.write(r_len.to_bytes(1, "big", signed=False))
signature.write(r)
signature.write(0x02 .to_bytes(1, "big", signed=False))
signature.write(0x02.to_bytes(1, "big", signed=False))
signature.write(s_len.to_bytes(1, "big", signed=False))
signature.write(s)

View File

@@ -7,11 +7,11 @@ from starlette.exceptions import HTTPException
from lnbits.core import db as core_db
from lnbits.core.models import Payment
from lnbits.core.services import websocketUpdater
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
from .crud import get_copilot
from .views import updater
async def wait_for_paid_invoices():
@@ -65,9 +65,11 @@ async def on_invoice_paid(payment: Payment) -> None:
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"))
await websocketUpdater(
copilot.id, str(data) + "-" + str(payment.extra.get("comment"))
)
await updater(copilot.id, data, "none")
await websocketUpdater(copilot.id, str(data) + "-none")
async def mark_webhook_sent(payment: Payment, status: int) -> None:

View File

@@ -238,7 +238,7 @@
document.domain +
':' +
location.port +
'/copilot/ws/' +
'/api/v1/ws/' +
self.copilot.id
} else {
localUrl =
@@ -246,7 +246,7 @@
document.domain +
':' +
location.port +
'/copilot/ws/' +
'/api/v1/ws/' +
self.copilot.id
}
this.connection = new WebSocket(localUrl)

View File

@@ -35,48 +35,3 @@ async def panel(request: Request):
return copilot_renderer().TemplateResponse(
"copilot/panel.html", {"request": request}
)
##################WEBSOCKET ROUTES########################
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket, copilot_id: str):
await websocket.accept()
websocket.id = copilot_id # type: ignore
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def send_personal_message(self, message: str, copilot_id: str):
for connection in self.active_connections:
if connection.id == copilot_id: # type: ignore
await connection.send_text(message)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@copilot_ext.websocket("/ws/{copilot_id}", name="copilot.websocket_by_id")
async def websocket_endpoint(websocket: WebSocket, copilot_id: str):
await manager.connect(websocket, copilot_id)
try:
while True:
data = await websocket.receive_text()
except WebSocketDisconnect:
manager.disconnect(websocket)
async def updater(copilot_id, data, comment):
copilot = await get_copilot(copilot_id)
if not copilot:
return
await manager.send_personal_message(f"{data + '-' + comment}", copilot_id)

View File

@@ -5,6 +5,7 @@ from fastapi.param_functions import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.core.services import websocketUpdater
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from . import copilot_ext
@@ -16,7 +17,6 @@ from .crud import (
update_copilot,
)
from .models import CreateCopilotData
from .views import updater
#######################COPILOT##########################
@@ -92,7 +92,7 @@ async def api_copilot_ws_relay(
status_code=HTTPStatus.NOT_FOUND, detail="Copilot does not exist"
)
try:
await updater(copilot_id, data, comment)
await websocketUpdater(copilot_id, str(data) + "-" + str(comment))
except:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your copilot")
return ""

View File

@@ -8,12 +8,11 @@ from fastapi import HTTPException
from lnbits import bolt11
from lnbits.core.models import Payment
from lnbits.core.services import pay_invoice
from lnbits.core.services import pay_invoice, websocketUpdater
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
from .crud import get_lnurldevice, get_lnurldevicepayment, update_lnurldevicepayment
from .views import updater
async def wait_for_paid_invoices():
@@ -36,9 +35,8 @@ async def on_invoice_paid(payment: Payment) -> None:
lnurldevicepayment = await update_lnurldevicepayment(
lnurldevicepayment_id=payment.extra.get("id"), payhash="used"
)
return await updater(
return await websocketUpdater(
lnurldevicepayment.deviceid,
lnurldevicepayment.pin,
lnurldevicepayment.payload,
str(lnurldevicepayment.pin) + "-" + str(lnurldevicepayment.payload),
)
return

View File

@@ -157,9 +157,9 @@
unelevated
color="primary"
size="md"
@click="copyText(wslocation + '/lnurldevice/ws/' + settingsDialog.data.id, 'Link copied to clipboard!')"
>{% raw %}{{wslocation}}/lnurldevice/ws/{{settingsDialog.data.id}}{%
endraw %}<q-tooltip> Click to copy URL </q-tooltip>
@click="copyText(wslocation + '/api/v1/ws/' + settingsDialog.data.id, 'Link copied to clipboard!')"
>{% raw %}{{wslocation}}/api/v1/ws/{{settingsDialog.data.id}}{% endraw
%}<q-tooltip> Click to copy URL </q-tooltip>
</q-btn>
<q-btn
v-else
@@ -657,7 +657,7 @@
lnurlValueFetch: function (lnurl, switchId) {
this.lnurlValue = lnurl
this.websocketConnector(
'wss://' + window.location.host + '/lnurldevice/ws/' + switchId
'wss://' + window.location.host + '/api/v1/ws/' + switchId
)
},
addSwitch: function () {

View File

@@ -2,7 +2,7 @@ from http import HTTPStatus
from io import BytesIO
import pyqrcode
from fastapi import Request, WebSocket, WebSocketDisconnect
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
@@ -63,50 +63,3 @@ async def img(request: Request, lnurldevice_id):
status_code=HTTPStatus.NOT_FOUND, detail="LNURLDevice does not exist."
)
return lnurldevice.lnurl(request)
##################WEBSOCKET ROUTES########################
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket, lnurldevice_id: str):
await websocket.accept()
websocket.id = lnurldevice_id
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def send_personal_message(self, message: str, lnurldevice_id: str):
for connection in self.active_connections:
if connection.id == lnurldevice_id:
await connection.send_text(message)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@lnurldevice_ext.websocket("/ws/{lnurldevice_id}", name="lnurldevice.lnurldevice_by_id")
async def websocket_endpoint(websocket: WebSocket, lnurldevice_id: str):
await manager.connect(websocket, lnurldevice_id)
try:
while True:
data = await websocket.receive_text()
except WebSocketDisconnect:
manager.disconnect(websocket)
async def updater(lnurldevice_id, lnurldevice_pin, lnurldevice_amount):
lnurldevice = await get_lnurldevice(lnurldevice_id)
if not lnurldevice:
return
return await manager.send_personal_message(
f"{lnurldevice_pin}-{lnurldevice_amount}", lnurldevice_id
)