websockets working localstorage for review

This commit is contained in:
Tiago vasconcelos 2022-09-28 17:02:15 +01:00
parent 62e35ec006
commit b9371805e2
5 changed files with 254 additions and 66 deletions

View file

@ -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

View file

@ -45,13 +45,15 @@
<q-card-section> <q-card-section>
{% raw %} {% raw %}
<h6 class="text-subtitle1 q-my-none">{{ stall.name }}</h6> <h6 class="text-subtitle1 q-my-none">{{ stall.name }}</h6>
<p @click="copyText(stall.publickey)"> <p @click="copyText(stall.publickey)" style="width: max-content">
Public Key: {{ sliceKey(stall.publickey) }} Public Key: {{ sliceKey(stall.publickey) }}
<q-tooltip>{{ stall.publickey }}</q-tooltip> <q-tooltip>Click to copy</q-tooltip>
</p> </p>
{% endraw %} {% endraw %}
</q-card-section>
<q-card-section v-if="user">
<q-form @submit="" class="q-gutter-md"> <q-form @submit="" class="q-gutter-md">
<q-select <!-- <q-select
filled filled
dense dense
emit-value emit-value
@ -60,16 +62,16 @@
label="Merchant" label="Merchant"
hint="Select a merchant you've opened an order to" hint="Select a merchant you've opened an order to"
></q-select> ></q-select>
<br /> <br /> -->
<q-select <q-select
filled filled
dense dense
emit-value emit-value
v-model="model" v-model="selectedOrder"
:options="mockOrder" :options="user.orders"
label="Order" label="Order"
hint="Select an order from this merchant" hint="Select an order from this merchant"
:disabled="!this.model" @input="val => { changeOrder() }"
></q-select> ></q-select>
</q-form> </q-form>
</q-card-section> </q-card-section>
@ -79,16 +81,26 @@
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Dolore, Lorem ipsum dolor, sit amet consectetur adipisicing elit. Dolore,
quasi. quasi.
</p> </p>
<div class="text-center q-mb-lg"> <div v-if="user?.keys" class="row">
<a :href="'lightning:'"> <div
<q-responsive :ratio="1" class="q-mx-xl"> class="col-6"
<qrcode v-for="type in ['publickey', 'privatekey']"
:value="cb4c0164fe03fcdadcbfb4f76611c71620790944c24f21a1cd119395cdedfe1b" v-bind:key="type"
:options="{width: 340}" >
class="rounded-borders" <div class="text-center q-mb-lg">
></qrcode> {% raw %}
</q-responsive> <q-responsive :ratio="1" class="q-mx-xl">
</a> <qrcode
:value="user.keys[type]"
:options="{width: 250}"
class="rounded-borders"
></qrcode>
<q-tooltip>{{ user.keys[type] }}</q-tooltip>
</q-responsive>
<p>{{ type == 'publickey' ? 'Public Key' : 'Private Key' }}</p>
{% endraw %}
</div>
</div>
</div> </div>
<p> <p>
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Dolore, Lorem ipsum dolor, sit amet consectetur adipisicing elit. Dolore,
@ -123,7 +135,9 @@
showMessages: false, showMessages: false,
messages: {}, messages: {},
stall: null, stall: null,
selectedOrder: null,
orders: [], orders: [],
user: null,
// Mock data // Mock data
model: null, model: null,
mockMerch: ['Google', 'Facebook', 'Twitter', 'Apple', 'Oracle'], mockMerch: ['Google', 'Facebook', 'Twitter', 'Apple', 'Oracle'],
@ -136,29 +150,108 @@
this.newMessage = '' this.newMessage = ''
this.$refs.newMessage.focus() this.$refs.newMessage.focus()
}, },
sendMessage() { sendMessage(e) {
let message = { let message = {
key: Date.now(), key: Date.now(),
text: this.newMessage, text: this.newMessage,
from: 'me' from: 'me'
} }
this.$set(this.messages, message.key, message) ws.send(JSON.stringify(message))
this.clearMessage() this.clearMessage()
e.preventDefault()
}, },
sliceKey(key) { sliceKey(key) {
if (!key) return '' if (!key) return ''
return `${key.slice(0, 4)}...${key.slice(-4)}` 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() { async created() {
let stall = JSON.parse('{{ stall | tojson }}') this.stall = JSON.parse('{{ stall | tojson }}')
let orders = JSON.parse('{{ orders | tojson }}')
this.stall = stall let order_details = JSON.parse('{{ order | tojson }}')
this.orders = orders let order_id = order_details[0].order_id
console.log(stall)
console.log(orders) 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)
}
} }
}) })
</script> </script>

View file

@ -644,7 +644,7 @@
icon="storefront" icon="storefront"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a" type="a"
:href="'/diagonalley/' + props.row.id" :href="'/diagonalley/stalls/' + props.row.id"
target="_blank" target="_blank"
></q-btn> ></q-btn>
<q-tooltip> Link to pass to stall relay </q-tooltip> <q-tooltip> Link to pass to stall relay </q-tooltip>

View file

@ -1,7 +1,8 @@
import json
from http import HTTPStatus from http import HTTPStatus
from typing import List 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.params import Depends
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from loguru import logger from loguru import logger
@ -11,6 +12,7 @@ from starlette.responses import HTMLResponse
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.decorators import check_user_exists # type: ignore from lnbits.decorators import check_user_exists # type: ignore
from lnbits.extensions.diagonalley import diagonalley_ext, diagonalley_renderer from lnbits.extensions.diagonalley import diagonalley_ext, diagonalley_renderer
from lnbits.extensions.diagonalley.notifier import Notifier
from .crud import ( from .crud import (
get_diagonalley_market, 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/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}") @diagonalley_ext.get("/stalls/{stall_id}", response_class=HTMLResponse)
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)
async def display(request: Request, stall_id): async def display(request: Request, stall_id):
stall = await get_diagonalley_stall(stall_id) stall = await get_diagonalley_stall(stall_id)
products = await get_diagonalley_products(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######################## ##################WEBSOCKET ROUTES########################
class ConnectionManager: # Initialize Notifier:
def __init__(self): notifier = Notifier()
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket, order_id: str): @diagonalley_ext.websocket("/ws/{room_name}")
await websocket.accept() async def websocket_endpoint(
websocket.id = order_id websocket: WebSocket, room_name: str, background_tasks: BackgroundTasks
self.active_connections.append(websocket) ):
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): room_members = (
self.active_connections.remove(websocket) 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): if websocket not in room_members:
for connection in self.active_connections: print("Sender not in room member: Reconnecting...")
if connection.id == order_id: await notifier.connect(websocket, room_name)
await connection.send_text(message)
async def broadcast(self, message: str): await notifier._notify(f"{data}", room_name)
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
except WebSocketDisconnect:
notifier.remove(websocket, room_name)

View file

@ -469,14 +469,15 @@ async def api_diagonalley_stall_create(
return market.dict() return market.dict()
## KEYS ## KEYS
@diagonalley_ext.get("/api/v1/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() private_key = PrivateKey()
public_key = private_key.pubkey.serialize().hex() public_key = private_key.pubkey.serialize().hex()
while not public_key.startswith("02"): while not public_key.startswith("02"):
private_key = PrivateKey() private_key = PrivateKey()
public_key = private_key.pubkey.serialize().hex() public_key = private_key.pubkey.serialize().hex()
return {"privkey": private_key.serialize(), "pubkey": public_key[2:]} return {"privkey": private_key.serialize(), "pubkey": public_key[2:]}