mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-25 15:10:41 +01:00
websockets working localstorage for review
This commit is contained in:
parent
62e35ec006
commit
b9371805e2
5 changed files with 254 additions and 66 deletions
83
lnbits/extensions/diagonalley/notifier.py
Normal file
83
lnbits/extensions/diagonalley/notifier.py
Normal 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
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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:]}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue