From b9371805e2eb0038b439a76589d26beca6c507ef Mon Sep 17 00:00:00 2001
From: Tiago vasconcelos
Date: Wed, 28 Sep 2022 17:02:15 +0100
Subject: [PATCH] websockets working localstorage for review
---
lnbits/extensions/diagonalley/notifier.py | 83 ++++++++++
.../templates/diagonalley/chat.html | 145 ++++++++++++++----
.../templates/diagonalley/index.html | 2 +-
lnbits/extensions/diagonalley/views.py | 85 +++++-----
lnbits/extensions/diagonalley/views_api.py | 5 +-
5 files changed, 254 insertions(+), 66 deletions(-)
create mode 100644 lnbits/extensions/diagonalley/notifier.py
diff --git a/lnbits/extensions/diagonalley/notifier.py b/lnbits/extensions/diagonalley/notifier.py
new file mode 100644
index 000000000..58a9f2bb7
--- /dev/null
+++ b/lnbits/extensions/diagonalley/notifier.py
@@ -0,0 +1,83 @@
+## adapted from https://github.com/Sentymental/chat-fastapi-websocket
+"""
+Create a class Notifier that will handle messages
+and delivery to the specific person
+"""
+
+from collections import defaultdict
+
+from fastapi import WebSocket
+from loguru import logger
+
+
+class Notifier:
+ """
+ Manages chatrooms, sessions and members.
+
+ Methods:
+ - get_notification_generator(self): async generator with notification messages
+ - get_members(self, room_name: str): get members in room
+ - push(message: str, room_name: str): push message
+ - connect(websocket: WebSocket, room_name: str): connect to room
+ - remove(websocket: WebSocket, room_name: str): remove
+ - _notify(message: str, room_name: str): notifier
+ """
+
+ def __init__(self):
+ # Create sessions as a dict:
+ self.sessions: dict = defaultdict(dict)
+
+ # Create notification generator:
+ self.generator = self.get_notification_generator()
+
+ async def get_notification_generator(self):
+ """Notification Generator"""
+
+ while True:
+ message = yield
+ msg = message["message"]
+ room_name = message["room_name"]
+ await self._notify(msg, room_name)
+
+ def get_members(self, room_name: str):
+ """Get all members in a room"""
+
+ try:
+ logger.info(f"Looking for members in room: {room_name}")
+ return self.sessions[room_name]
+
+ except Exception:
+ logger.exception(f"There is no member in room: {room_name}")
+ return None
+
+ async def push(self, message: str, room_name: str = None):
+ """Push a message"""
+
+ message_body = {"message": message, "room_name": room_name}
+ await self.generator.asend(message_body)
+
+ async def connect(self, websocket: WebSocket, room_name: str):
+ """Connect to room"""
+
+ await websocket.accept()
+ if self.sessions[room_name] == {} or len(self.sessions[room_name]) == 0:
+ self.sessions[room_name] = []
+
+ self.sessions[room_name].append(websocket)
+ print(f"Connections ...: {self.sessions[room_name]}")
+
+ def remove(self, websocket: WebSocket, room_name: str):
+ """Remove websocket from room"""
+
+ self.sessions[room_name].remove(websocket)
+ print(f"Connection removed...\nOpen connections...: {self.sessions[room_name]}")
+
+ async def _notify(self, message: str, room_name: str):
+ """Notifier"""
+
+ remaining_sessions = []
+ while len(self.sessions[room_name]) > 0:
+ websocket = self.sessions[room_name].pop()
+ await websocket.send_text(message)
+ remaining_sessions.append(websocket)
+ self.sessions[room_name] = remaining_sessions
diff --git a/lnbits/extensions/diagonalley/templates/diagonalley/chat.html b/lnbits/extensions/diagonalley/templates/diagonalley/chat.html
index bb3e607fd..1713c9e24 100644
--- a/lnbits/extensions/diagonalley/templates/diagonalley/chat.html
+++ b/lnbits/extensions/diagonalley/templates/diagonalley/chat.html
@@ -45,13 +45,15 @@
{% raw %}
{{ stall.name }}
-
+
Public Key: {{ sliceKey(stall.publickey) }}
- {{ stall.publickey }}
+ Click to copy
{% endraw %}
+
+
-
-
+
-->
{ changeOrder() }"
>
@@ -79,16 +81,26 @@
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Dolore,
quasi.
-
-
-
-
-
-
+
+
+
+ {% raw %}
+
+
+ {{ user.keys[type] }}
+
+
{{ type == 'publickey' ? 'Public Key' : 'Private Key' }}
+ {% endraw %}
+
+
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Dolore,
@@ -123,7 +135,9 @@
showMessages: false,
messages: {},
stall: null,
+ selectedOrder: null,
orders: [],
+ user: null,
// Mock data
model: null,
mockMerch: ['Google', 'Facebook', 'Twitter', 'Apple', 'Oracle'],
@@ -136,29 +150,108 @@
this.newMessage = ''
this.$refs.newMessage.focus()
},
- sendMessage() {
+ sendMessage(e) {
let message = {
key: Date.now(),
text: this.newMessage,
from: 'me'
}
- this.$set(this.messages, message.key, message)
+ ws.send(JSON.stringify(message))
this.clearMessage()
+ e.preventDefault()
},
sliceKey(key) {
if (!key) return ''
return `${key.slice(0, 4)}...${key.slice(-4)}`
+ },
+ async generateKeys() {
+ await LNbits.api
+ .request('GET', '/diagonalley/api/v1/keys', null)
+ .then(response => {
+ if (response.data) {
+ let data = {
+ keys: {
+ privatekey: response.data.privkey,
+ publickey: response.data.pubkey
+ }
+ }
+ this.user = data
+ console.log('Keys done', this.user)
+ return
+ }
+ })
+ .catch(function (error) {
+ LNbits.utils.notifyApiError(error)
+ })
+ },
+ changeOrder() {
+ console.log(this.selectedOrder)
+ },
+ startChat(room_name) {
+ if (location.protocol == 'https:') {
+ ws_scheme = 'wss://'
+ } else {
+ ws_scheme = 'ws://'
+ }
+ ws = new WebSocket(
+ ws_scheme + location.host + '/diagonalley/ws/' + room_name
+ )
+
+ function checkWebSocket(event) {
+ if (ws.readyState === WebSocket.CLOSED) {
+ console.log('WebSocket CLOSED: Reopening')
+ ws = new WebSocket(
+ ws_scheme + location.host + '/diagonalley/ws/' + room_name
+ )
+ }
+ }
+
+ ws.onmessage = event => {
+ let event_data = JSON.parse(event.data)
+
+ this.$set(this.messages, event_data.key, event_data)
+ this.$q.localStorage.set()
+ }
+
+ this.ws = ws
}
},
- created() {
- let stall = JSON.parse('{{ stall | tojson }}')
- let orders = JSON.parse('{{ orders | tojson }}')
+ async created() {
+ this.stall = JSON.parse('{{ stall | tojson }}')
- this.stall = stall
- this.orders = orders
- console.log(stall)
- console.log(orders)
+ let order_details = JSON.parse('{{ order | tojson }}')
+ let order_id = order_details[0].order_id
+
+ let data = this.$q.localStorage.getItem(`lnbits.diagonalley.data`)
+
+ try {
+ if (data) {
+ this.user = data
+ //add chat key (merchant pubkey) if not set
+ if (!this.user.chats[`${this.stall.publickey}`]) {
+ this.$set(this.user.chats, this.stall.publickey, [])
+ }
+ //this.$q.localStorage.set(`lnbits.diagonalley.data`, this.user)
+ } else {
+ // generate keys
+ await this.generateKeys()
+ // populate user data
+ this.user.chats = {
+ [`${this.stall.publickey}`]: []
+ }
+ this.user.orders = []
+ }
+
+ this.order_details = order_details
+ this.user.orders = [...new Set([...this.user.orders, order_id])]
+ this.selectedOrder = order_id
+
+ this.$q.localStorage.set(`lnbits.diagonalley.data`, this.user)
+ this.startChat(order_id)
+ } catch (e) {
+ console.error(e)
+ }
}
})
diff --git a/lnbits/extensions/diagonalley/templates/diagonalley/index.html b/lnbits/extensions/diagonalley/templates/diagonalley/index.html
index cd03e6f35..5ad6f6a5e 100644
--- a/lnbits/extensions/diagonalley/templates/diagonalley/index.html
+++ b/lnbits/extensions/diagonalley/templates/diagonalley/index.html
@@ -644,7 +644,7 @@
icon="storefront"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
- :href="'/diagonalley/' + props.row.id"
+ :href="'/diagonalley/stalls/' + props.row.id"
target="_blank"
>
Link to pass to stall relay
diff --git a/lnbits/extensions/diagonalley/views.py b/lnbits/extensions/diagonalley/views.py
index d6e701a0f..a378658ab 100644
--- a/lnbits/extensions/diagonalley/views.py
+++ b/lnbits/extensions/diagonalley/views.py
@@ -1,7 +1,8 @@
+import json
from http import HTTPStatus
from typing import List
-from fastapi import Query, Request, WebSocket, WebSocketDisconnect
+from fastapi import BackgroundTasks, Query, Request, WebSocket, WebSocketDisconnect
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from loguru import logger
@@ -11,6 +12,7 @@ from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists # type: ignore
from lnbits.extensions.diagonalley import diagonalley_ext, diagonalley_renderer
+from lnbits.extensions.diagonalley.notifier import Notifier
from .crud import (
get_diagonalley_market,
@@ -32,24 +34,8 @@ async def index(request: Request, user: User = Depends(check_user_exists)):
"diagonalley/index.html", {"request": request, "user": user.dict()}
)
-@diagonalley_ext.get("/chat", response_class=HTMLResponse)
-async def chat_page(request: Request, merch: str = Query(...), order: str = Query(...)):
- stall = await get_diagonalley_stall(merch)
- orders = await get_diagonalley_order_details(order)
- logger.debug(f"ORDER: {orders}")
-
- return diagonalley_renderer().TemplateResponse(
- "diagonalley/chat.html",
- {
- "request": request,
- "stall": {"id": stall.id, "name": stall.name, "publickey": stall.publickey, "wallet": stall.wallet },
- "orders": [details.dict() for details in orders]
- },
- )
-
-
-@diagonalley_ext.get("/{stall_id}", response_class=HTMLResponse)
+@diagonalley_ext.get("/stalls/{stall_id}", response_class=HTMLResponse)
async def display(request: Request, stall_id):
stall = await get_diagonalley_stall(stall_id)
products = await get_diagonalley_products(stall_id)
@@ -103,30 +89,55 @@ async def display(request: Request, market_id):
},
)
+
+@diagonalley_ext.get("/chat", response_class=HTMLResponse)
+async def chat_page(request: Request, merch: str = Query(...), order: str = Query(...)):
+ stall = await get_diagonalley_stall(merch)
+ _order = await get_diagonalley_order_details(order)
+
+ return diagonalley_renderer().TemplateResponse(
+ "diagonalley/chat.html",
+ {
+ "request": request,
+ "stall": {
+ "id": stall.id,
+ "name": stall.name,
+ "publickey": stall.publickey,
+ "wallet": stall.wallet,
+ },
+ "order": [details.dict() for details in _order],
+ },
+ )
+
+
##################WEBSOCKET ROUTES########################
-class ConnectionManager:
- def __init__(self):
- self.active_connections: List[WebSocket] = []
+# Initialize Notifier:
+notifier = Notifier()
- async def connect(self, websocket: WebSocket, order_id: str):
- await websocket.accept()
- websocket.id = order_id
- self.active_connections.append(websocket)
+@diagonalley_ext.websocket("/ws/{room_name}")
+async def websocket_endpoint(
+ websocket: WebSocket, room_name: str, background_tasks: BackgroundTasks
+):
+ await notifier.connect(websocket, room_name)
+ try:
+ while True:
+ data = await websocket.receive_text()
+ d = json.loads(data)
+ d["room_name"] = room_name
- def disconnect(self, websocket: WebSocket):
- self.active_connections.remove(websocket)
+ room_members = (
+ notifier.get_members(room_name)
+ if notifier.get_members(room_name) is not None
+ else []
+ )
- async def send_personal_message(self, message: str, order_id: str):
- for connection in self.active_connections:
- if connection.id == order_id:
- await connection.send_text(message)
+ if websocket not in room_members:
+ print("Sender not in room member: Reconnecting...")
+ await notifier.connect(websocket, room_name)
- async def broadcast(self, message: str):
- for connection in self.active_connections:
- await connection.send_text(message)
-
-
-manager = ConnectionManager()
+ await notifier._notify(f"{data}", room_name)
+ except WebSocketDisconnect:
+ notifier.remove(websocket, room_name)
diff --git a/lnbits/extensions/diagonalley/views_api.py b/lnbits/extensions/diagonalley/views_api.py
index c3615c908..aa8d338dc 100644
--- a/lnbits/extensions/diagonalley/views_api.py
+++ b/lnbits/extensions/diagonalley/views_api.py
@@ -469,14 +469,15 @@ async def api_diagonalley_stall_create(
return market.dict()
+
## KEYS
+
@diagonalley_ext.get("/api/v1/keys")
-async def api_diagonalley_generate_keys(wallet: WalletTypeInfo = Depends(require_admin_key)):
+async def api_diagonalley_generate_keys():
private_key = PrivateKey()
public_key = private_key.pubkey.serialize().hex()
while not public_key.startswith("02"):
private_key = PrivateKey()
public_key = private_key.pubkey.serialize().hex()
return {"privkey": private_key.serialize(), "pubkey": public_key[2:]}
-