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 %} +
+ - -
+
-->
@@ -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:]} -