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>
{% raw %}
<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) }}
<q-tooltip>{{ stall.publickey }}</q-tooltip>
<q-tooltip>Click to copy</q-tooltip>
</p>
{% endraw %}
</q-card-section>
<q-card-section v-if="user">
<q-form @submit="" class="q-gutter-md">
<q-select
<!-- <q-select
filled
dense
emit-value
@ -60,16 +62,16 @@
label="Merchant"
hint="Select a merchant you've opened an order to"
></q-select>
<br />
<br /> -->
<q-select
filled
dense
emit-value
v-model="model"
:options="mockOrder"
v-model="selectedOrder"
:options="user.orders"
label="Order"
hint="Select an order from this merchant"
:disabled="!this.model"
@input="val => { changeOrder() }"
></q-select>
</q-form>
</q-card-section>
@ -79,16 +81,26 @@
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Dolore,
quasi.
</p>
<div class="text-center q-mb-lg">
<a :href="'lightning:'">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="cb4c0164fe03fcdadcbfb4f76611c71620790944c24f21a1cd119395cdedfe1b"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
<div v-if="user?.keys" class="row">
<div
class="col-6"
v-for="type in ['publickey', 'privatekey']"
v-bind:key="type"
>
<div class="text-center q-mb-lg">
{% raw %}
<q-responsive :ratio="1" class="q-mx-xl">
<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>
<p>
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)
}
}
})
</script>

View file

@ -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"
></q-btn>
<q-tooltip> Link to pass to stall relay </q-tooltip>

View file

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

View file

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