Merge pull request #1167 from lnbits/switchtounisocket
Replaces extension specific websockets with the new generic websocket
This commit is contained in:
commit
223c980710
10 changed files with 29 additions and 164 deletions
|
|
@ -9,53 +9,10 @@ nav_order: 2
|
||||||
Websockets
|
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:
|
Example vue-js function for listening to the websocket:
|
||||||
|
|
||||||
|
|
@ -67,16 +24,16 @@ initWs: async function () {
|
||||||
document.domain +
|
document.domain +
|
||||||
':' +
|
':' +
|
||||||
location.port +
|
location.port +
|
||||||
'/extension/ws/' +
|
'/api/v1/ws/' +
|
||||||
self.extension.id
|
self.item.id
|
||||||
} else {
|
} else {
|
||||||
localUrl =
|
localUrl =
|
||||||
'ws://' +
|
'ws://' +
|
||||||
document.domain +
|
document.domain +
|
||||||
':' +
|
':' +
|
||||||
location.port +
|
location.port +
|
||||||
'/extension/ws/' +
|
'/api/v1/ws/' +
|
||||||
self.extension.id
|
self.item.id
|
||||||
}
|
}
|
||||||
this.ws = new WebSocket(localUrl)
|
this.ws = new WebSocket(localUrl)
|
||||||
this.ws.addEventListener('message', async ({data}) => {
|
this.ws.addEventListener('message', async ({data}) => {
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
from lnbits.core import db as core_db
|
from lnbits.core import db as core_db
|
||||||
from lnbits.core.models import Payment
|
from lnbits.core.models import Payment
|
||||||
|
from lnbits.core.services import websocketUpdater
|
||||||
from lnbits.helpers import get_current_extension_name
|
from lnbits.helpers import get_current_extension_name
|
||||||
from lnbits.tasks import register_invoice_listener
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
||||||
from .crud import get_copilot
|
from .crud import get_copilot
|
||||||
from .views import updater
|
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_paid_invoices():
|
async def wait_for_paid_invoices():
|
||||||
|
|
@ -65,9 +65,11 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||||
except (httpx.ConnectError, httpx.RequestError):
|
except (httpx.ConnectError, httpx.RequestError):
|
||||||
await mark_webhook_sent(payment, -1)
|
await mark_webhook_sent(payment, -1)
|
||||||
if payment.extra.get("comment"):
|
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:
|
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@
|
||||||
document.domain +
|
document.domain +
|
||||||
':' +
|
':' +
|
||||||
location.port +
|
location.port +
|
||||||
'/copilot/ws/' +
|
'/api/v1/ws/' +
|
||||||
self.copilot.id
|
self.copilot.id
|
||||||
} else {
|
} else {
|
||||||
localUrl =
|
localUrl =
|
||||||
|
|
@ -246,7 +246,7 @@
|
||||||
document.domain +
|
document.domain +
|
||||||
':' +
|
':' +
|
||||||
location.port +
|
location.port +
|
||||||
'/copilot/ws/' +
|
'/api/v1/ws/' +
|
||||||
self.copilot.id
|
self.copilot.id
|
||||||
}
|
}
|
||||||
this.connection = new WebSocket(localUrl)
|
this.connection = new WebSocket(localUrl)
|
||||||
|
|
|
||||||
|
|
@ -35,48 +35,3 @@ async def panel(request: Request):
|
||||||
return copilot_renderer().TemplateResponse(
|
return copilot_renderer().TemplateResponse(
|
||||||
"copilot/panel.html", {"request": request}
|
"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)
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from fastapi.param_functions import Query
|
||||||
from fastapi.params import Depends
|
from fastapi.params import Depends
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
|
from lnbits.core.services import websocketUpdater
|
||||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
|
|
||||||
from . import copilot_ext
|
from . import copilot_ext
|
||||||
|
|
@ -16,7 +17,6 @@ from .crud import (
|
||||||
update_copilot,
|
update_copilot,
|
||||||
)
|
)
|
||||||
from .models import CreateCopilotData
|
from .models import CreateCopilotData
|
||||||
from .views import updater
|
|
||||||
|
|
||||||
#######################COPILOT##########################
|
#######################COPILOT##########################
|
||||||
|
|
||||||
|
|
@ -92,7 +92,7 @@ async def api_copilot_ws_relay(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Copilot does not exist"
|
status_code=HTTPStatus.NOT_FOUND, detail="Copilot does not exist"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await updater(copilot_id, data, comment)
|
await websocketUpdater(copilot_id, str(data) + "-" + str(comment))
|
||||||
except:
|
except:
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your copilot")
|
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your copilot")
|
||||||
return ""
|
return ""
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,11 @@ from fastapi import HTTPException
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.core.models import Payment
|
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.helpers import get_current_extension_name
|
||||||
from lnbits.tasks import register_invoice_listener
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
||||||
from .crud import get_lnurldevice, get_lnurldevicepayment, update_lnurldevicepayment
|
from .crud import get_lnurldevice, get_lnurldevicepayment, update_lnurldevicepayment
|
||||||
from .views import updater
|
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_paid_invoices():
|
async def wait_for_paid_invoices():
|
||||||
|
|
@ -36,9 +35,8 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||||
lnurldevicepayment = await update_lnurldevicepayment(
|
lnurldevicepayment = await update_lnurldevicepayment(
|
||||||
lnurldevicepayment_id=payment.extra.get("id"), payhash="used"
|
lnurldevicepayment_id=payment.extra.get("id"), payhash="used"
|
||||||
)
|
)
|
||||||
return await updater(
|
return await websocketUpdater(
|
||||||
lnurldevicepayment.deviceid,
|
lnurldevicepayment.deviceid,
|
||||||
lnurldevicepayment.pin,
|
str(lnurldevicepayment.pin) + "-" + str(lnurldevicepayment.payload),
|
||||||
lnurldevicepayment.payload,
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -157,9 +157,9 @@
|
||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
size="md"
|
size="md"
|
||||||
@click="copyText(wslocation + '/lnurldevice/ws/' + settingsDialog.data.id, 'Link copied to clipboard!')"
|
@click="copyText(wslocation + '/api/v1/ws/' + settingsDialog.data.id, 'Link copied to clipboard!')"
|
||||||
>{% raw %}{{wslocation}}/lnurldevice/ws/{{settingsDialog.data.id}}{%
|
>{% raw %}{{wslocation}}/api/v1/ws/{{settingsDialog.data.id}}{% endraw
|
||||||
endraw %}<q-tooltip> Click to copy URL </q-tooltip>
|
%}<q-tooltip> Click to copy URL </q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-else
|
v-else
|
||||||
|
|
@ -657,7 +657,7 @@
|
||||||
lnurlValueFetch: function (lnurl, switchId) {
|
lnurlValueFetch: function (lnurl, switchId) {
|
||||||
this.lnurlValue = lnurl
|
this.lnurlValue = lnurl
|
||||||
this.websocketConnector(
|
this.websocketConnector(
|
||||||
'wss://' + window.location.host + '/lnurldevice/ws/' + switchId
|
'wss://' + window.location.host + '/api/v1/ws/' + switchId
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
addSwitch: function () {
|
addSwitch: function () {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from http import HTTPStatus
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import pyqrcode
|
import pyqrcode
|
||||||
from fastapi import Request, WebSocket, WebSocketDisconnect
|
from fastapi import Request
|
||||||
from fastapi.param_functions import Query
|
from fastapi.param_functions import Query
|
||||||
from fastapi.params import Depends
|
from fastapi.params import Depends
|
||||||
from fastapi.templating import Jinja2Templates
|
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."
|
status_code=HTTPStatus.NOT_FOUND, detail="LNURLDevice does not exist."
|
||||||
)
|
)
|
||||||
return lnurldevice.lnurl(request)
|
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
|
|
||||||
)
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue