rework peers to come from database announcements
This commit is contained in:
@@ -66,54 +66,58 @@
|
||||
|
||||
<!-- peers -->
|
||||
<div class="flex-1 flex flex-col border rounded-xl bg-white shadow overflow-hidden">
|
||||
<div class="flex border-b border-gray-300 text-gray-700 p-2">
|
||||
<div class="mr-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>Peers Discovered ({{ peersCount }})</div>
|
||||
</div>
|
||||
<div v-if="peersCount > 0" class="p-1 border-b border-gray-300">
|
||||
<input v-model="peersSearchTerm" type="text" placeholder="Search peers..." class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||
</div>
|
||||
<div v-if="searchedPeers.length > 0" class="overflow-y-scroll">
|
||||
<div @click="onPeerClick(peer)" v-for="peer of searchedPeers" class="flex cursor-pointer p-2 border-l-2 border-transparent" :class="[ peer.destination_hash === selectedPeer?.destination_hash ? 'bg-gray-100 border-blue-500' : 'bg-white hover:bg-gray-50 hover:border-gray-200' ]">
|
||||
<div class="my-auto mr-2">
|
||||
<img class="w-9 h-9 rounded-full" src="assets/images/user.png"/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-900">{{ peer.app_data || "Anonymous Peer" }}</div>
|
||||
<div class="text-gray-500 text-sm">{{ formatTimeAgo(peer.last_announce_timestamp) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- tabs -->
|
||||
<div class="border-b border-gray-200">
|
||||
<div class="-mb-px flex">
|
||||
<!-- Current: "border-blue-500 text-blue-600", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" -->
|
||||
<div @click="tab = 'peers'" class="w-full border-b-2 py-3 px-1 text-center text-sm font-medium cursor-pointer" :class="[ tab === 'peers' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700']">Peers</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="mx-auto my-auto text-center leading-5">
|
||||
|
||||
<!-- no peers at all -->
|
||||
<div v-if="peersCount === 0" class="flex flex-col">
|
||||
<div class="mx-auto mb-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="font-semibold">No Peers Discovered</div>
|
||||
<div>Waiting for someone to announce!</div>
|
||||
<template v-if="tab === 'peers'">
|
||||
<div v-if="peersCount > 0" class="p-1 border-b border-gray-300">
|
||||
<input v-model="peersSearchTerm" type="text" :placeholder="`Search ${peersCount} peers...`" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||
</div>
|
||||
|
||||
<!-- is searching, but no results -->
|
||||
<div v-if="peersSearchTerm !== '' && peersCount > 0" class="flex flex-col">
|
||||
<div class="mx-auto mb-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
||||
</svg>
|
||||
<div v-if="searchedPeers.length > 0" class="overflow-y-scroll">
|
||||
<div @click="onPeerClick(peer)" v-for="peer of searchedPeers" class="flex cursor-pointer p-2 border-l-2 border-transparent" :class="[ peer.destination_hash === selectedPeer?.destination_hash ? 'bg-gray-100 border-blue-500' : 'bg-white hover:bg-gray-50 hover:border-gray-200' ]">
|
||||
<div class="my-auto mr-2">
|
||||
<img class="w-9 h-9 rounded-full" src="assets/images/user.png"/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-900">{{ peer.name }}</div>
|
||||
<div class="text-gray-500 text-sm">{{ formatTimeAgo(peer.updated_at) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-semibold">No Search Results</div>
|
||||
<div>Your search didn't match any Peers!</div>
|
||||
</div>
|
||||
<div v-else class="mx-auto my-auto text-center leading-5">
|
||||
|
||||
<!-- no peers at all -->
|
||||
<div v-if="peersCount === 0" class="flex flex-col">
|
||||
<div class="mx-auto mb-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="font-semibold">No Peers Discovered</div>
|
||||
<div>Waiting for someone to announce!</div>
|
||||
</div>
|
||||
|
||||
<!-- is searching, but no results -->
|
||||
<div v-if="peersSearchTerm !== '' && peersCount > 0" class="flex flex-col">
|
||||
<div class="mx-auto mb-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="font-semibold">No Search Results</div>
|
||||
<div>Your search didn't match any Peers!</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- my identity -->
|
||||
@@ -159,7 +163,7 @@
|
||||
|
||||
<!-- peer info -->
|
||||
<div>
|
||||
<div class="font-semibold">{{ selectedPeer.app_data || "Unknown" }}</div>
|
||||
<div class="font-semibold">{{ selectedPeer.name }}</div>
|
||||
<div class="text-sm">@<{{ selectedPeer.destination_hash }}></div>
|
||||
</div>
|
||||
|
||||
@@ -197,7 +201,7 @@
|
||||
<div v-for="chatItem of selectedPeerChatItems" class="flex flex-col max-w-xl" :class="{ 'ml-auto pl-4 md:pl-16 items-end': chatItem.is_outbound, 'mr-auto pr-4 md:pr-16 items-start': !chatItem.is_outbound }">
|
||||
|
||||
<!-- sender name -->
|
||||
<div v-if="!chatItem.is_outbound" class="text-xs text-gray-500 ml-2">{{ selectedPeer.app_data || "Unknown" }}</div>
|
||||
<div v-if="!chatItem.is_outbound" class="text-xs text-gray-500 ml-2">{{ selectedPeer.name }}</div>
|
||||
|
||||
<!-- message content -->
|
||||
<div @click="onChatItemClick(chatItem)" class="border border-gray-300 rounded-xl shadow overflow-hidden" :class="[ chatItem.lxmf_message.state === 'failed' ? 'bg-red-500 text-white' : chatItem.is_outbound ? 'bg-[#3b82f6] text-white' : 'bg-[#efefef]' ]">
|
||||
@@ -406,6 +410,9 @@
|
||||
displayName: "Anonymous Peer",
|
||||
config: null,
|
||||
|
||||
lxmfDeliveryAnnounces: [],
|
||||
|
||||
tab: "peers",
|
||||
peers: {},
|
||||
peersSearchTerm: "",
|
||||
selectedPeer: null,
|
||||
@@ -420,6 +427,7 @@
|
||||
},
|
||||
mounted: function() {
|
||||
this.connectWebsocket();
|
||||
this.getLxmfDeliveryAnnounces();
|
||||
},
|
||||
methods: {
|
||||
connectWebsocket: function() {
|
||||
@@ -450,20 +458,9 @@
|
||||
break;
|
||||
}
|
||||
case 'announce': {
|
||||
this.peers[json.announce.destination_hash] = {
|
||||
destination_hash: json.announce.destination_hash,
|
||||
app_data: json.announce.app_data,
|
||||
last_announce_timestamp: json.announce.last_announce_timestamp,
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'known_peers': {
|
||||
for(const known_peer of json.known_peers){
|
||||
this.peers[known_peer.destination_hash] = {
|
||||
destination_hash: known_peer.destination_hash,
|
||||
app_data: known_peer.app_data,
|
||||
last_announce_timestamp: known_peer.last_announce_timestamp,
|
||||
}
|
||||
const aspect = json.announce.aspect;
|
||||
if(aspect === "lxmf.delivery"){
|
||||
this.updatePeerFromAnnounce(json.announce);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -784,6 +781,41 @@
|
||||
}
|
||||
|
||||
},
|
||||
async getLxmfDeliveryAnnounces() {
|
||||
try {
|
||||
|
||||
// fetch announces for "lxmf.delivery" aspect
|
||||
const response = await window.axios.get(`/api/v1/announces`, {
|
||||
params: {
|
||||
aspect: "lxmf.delivery",
|
||||
},
|
||||
});
|
||||
|
||||
// update ui
|
||||
const lxmfDeliveryAnnounces = response.data.announces;
|
||||
for(const lxmfDeliveryAnnounce of lxmfDeliveryAnnounces){
|
||||
this.updatePeerFromAnnounce(lxmfDeliveryAnnounce);
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
// do nothing if failed to load announces
|
||||
}
|
||||
},
|
||||
getPeerNameFromAppData: function(appData) {
|
||||
try {
|
||||
// app data should be peer name, and our server provides it base64 encoded
|
||||
return atob(appData);
|
||||
} catch(e){
|
||||
return "Anonymous Peer";
|
||||
}
|
||||
},
|
||||
updatePeerFromAnnounce: function(announce) {
|
||||
this.peers[announce.destination_hash] = {
|
||||
...announce,
|
||||
// helper property for easily grabbing peer name from app data
|
||||
name: this.getPeerNameFromAppData(announce.app_data),
|
||||
};
|
||||
},
|
||||
async loadLxmfMessages(destinationHash) {
|
||||
const seq = ++this.lxmfMessagesRequestSequence;
|
||||
try {
|
||||
@@ -891,9 +923,10 @@
|
||||
seconds: seconds,
|
||||
};
|
||||
},
|
||||
formatTimeAgo: function(seconds) {
|
||||
formatTimeAgo: function(datetimeString) {
|
||||
|
||||
const secondsAgo = Math.round((Date.now() / 1000) - seconds);
|
||||
const millisecondsAgo = Date.now() - new Date(datetimeString).getTime();
|
||||
const secondsAgo = Math.round(millisecondsAgo / 1000);
|
||||
const parsedSeconds = this.parseSeconds(secondsAgo);
|
||||
|
||||
if(parsedSeconds.days > 0){
|
||||
@@ -1029,15 +1062,17 @@
|
||||
peersOrderedByLatestAnnounce() {
|
||||
const peers = Object.values(this.peers);
|
||||
return peers.sort(function(peerA, peerB) {
|
||||
// order by last_announce_timestamp desc
|
||||
return peerB.last_announce_timestamp - peerA.last_announce_timestamp;
|
||||
// order by updated_at desc
|
||||
const peerAUpdatedAt = new Date(peerA.updated_at).getTime();
|
||||
const peerBUpdatedAt = new Date(peerB.updated_at).getTime();
|
||||
return peerBUpdatedAt - peerAUpdatedAt;
|
||||
});
|
||||
},
|
||||
searchedPeers() {
|
||||
return this.peersOrderedByLatestAnnounce.filter((peer) => {
|
||||
const search = this.peersSearchTerm.toLowerCase();
|
||||
const matchesAppData = (peer.app_data || "").toLowerCase().includes(search);
|
||||
const matchesDestinationHash = (peer.destination_hash || "").toLowerCase().includes(search);
|
||||
const matchesAppData = peer.name.toLowerCase().includes(search);
|
||||
const matchesDestinationHash = peer.destination_hash.toLowerCase().includes(search);
|
||||
return matchesAppData || matchesDestinationHash;
|
||||
});
|
||||
},
|
||||
|
||||
79
web.py
79
web.py
@@ -4,7 +4,6 @@ import argparse
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Callable, List
|
||||
|
||||
@@ -121,9 +120,6 @@ class ReticulumWebChat:
|
||||
# send config to all clients
|
||||
await self.send_config_to_websocket_clients()
|
||||
|
||||
# send known peers to all clients
|
||||
await self.send_known_peers_to_websocket_clients()
|
||||
|
||||
# handle websocket messages until disconnected
|
||||
async for msg in websocket_response:
|
||||
msg: WSMessage = msg
|
||||
@@ -164,22 +160,13 @@ class ReticulumWebChat:
|
||||
# process announces
|
||||
announces = []
|
||||
for announce in query_results:
|
||||
announces.append({
|
||||
"id": announce.id,
|
||||
"destination_hash": announce.destination_hash,
|
||||
"aspect": announce.aspect,
|
||||
"identity_hash": announce.identity_hash,
|
||||
"identity_public_key": announce.identity_public_key,
|
||||
"app_data": announce.app_data,
|
||||
"created_at": announce.created_at,
|
||||
"updated_at": announce.updated_at,
|
||||
})
|
||||
announces.append(self.convert_db_announce_to_dict(announce))
|
||||
|
||||
return web.json_response({
|
||||
"announces": announces,
|
||||
})
|
||||
|
||||
# serve lxmf messages
|
||||
# delete lxmf message
|
||||
@routes.delete("/api/v1/lxmf-messages/{id}")
|
||||
async def index(request):
|
||||
|
||||
@@ -199,7 +186,7 @@ class ReticulumWebChat:
|
||||
"message": "ok",
|
||||
})
|
||||
|
||||
# serve lxmf messages
|
||||
# serve lxmf messages for conversation
|
||||
@routes.get("/api/v1/lxmf-messages/conversation/{destination_hash}")
|
||||
async def index(request):
|
||||
|
||||
@@ -459,26 +446,6 @@ class ReticulumWebChat:
|
||||
},
|
||||
}))
|
||||
|
||||
# broadcasts known peers to all websocket clients
|
||||
async def send_known_peers_to_websocket_clients(self):
|
||||
|
||||
# process known peers
|
||||
known_peers = []
|
||||
for destination_hash in RNS.Identity.known_destinations:
|
||||
known_destination = RNS.Identity.known_destinations[destination_hash]
|
||||
last_announce_timestamp = known_destination[0]
|
||||
known_peers.append({
|
||||
"destination_hash": destination_hash.hex(),
|
||||
"app_data": self.convert_app_data_to_string(RNS.Identity.recall_app_data(destination_hash)),
|
||||
"last_announce_timestamp": last_announce_timestamp,
|
||||
})
|
||||
|
||||
# send known peers to websocket clients
|
||||
await self.websocket_broadcast(json.dumps({
|
||||
"type": "known_peers",
|
||||
"known_peers": known_peers,
|
||||
}))
|
||||
|
||||
# convert app data to string, or return none unable to do so
|
||||
def convert_app_data_to_string(self, app_data):
|
||||
|
||||
@@ -564,6 +531,19 @@ class ReticulumWebChat:
|
||||
|
||||
return lxmf_message_state
|
||||
|
||||
# convert database announce to a dictionary
|
||||
def convert_db_announce_to_dict(self, announce: database.Announce):
|
||||
return {
|
||||
"id": announce.id,
|
||||
"destination_hash": announce.destination_hash,
|
||||
"aspect": announce.aspect,
|
||||
"identity_hash": announce.identity_hash,
|
||||
"identity_public_key": announce.identity_public_key,
|
||||
"app_data": announce.app_data,
|
||||
"created_at": announce.created_at,
|
||||
"updated_at": announce.updated_at,
|
||||
}
|
||||
|
||||
# handle an lxmf delivery from reticulum
|
||||
# NOTE: cant be async, as Reticulum doesn't await it
|
||||
def on_lxmf_delivery(self, lxmf_message):
|
||||
@@ -742,19 +722,15 @@ class ReticulumWebChat:
|
||||
# upsert announce to database
|
||||
self.db_upsert_announce(announced_identity, destination_hash, "lxmf.delivery", app_data)
|
||||
|
||||
# parse app data
|
||||
parsed_app_data = None
|
||||
if app_data is not None:
|
||||
parsed_app_data = app_data.decode("utf-8")
|
||||
# find announce from database
|
||||
announce = database.Announce.get_or_none(database.Announce.destination_hash == destination_hash.hex())
|
||||
if announce is None:
|
||||
return
|
||||
|
||||
# send received lxmf announce to all websocket clients
|
||||
# send database announce to all websocket clients
|
||||
asyncio.run(self.websocket_broadcast(json.dumps({
|
||||
"type": "announce",
|
||||
"announce": {
|
||||
"destination_hash": destination_hash.hex(),
|
||||
"app_data": parsed_app_data,
|
||||
"last_announce_timestamp": time.time(),
|
||||
},
|
||||
"announce": self.convert_db_announce_to_dict(announce),
|
||||
})))
|
||||
|
||||
# handle an announce received from reticulum, for a nomadnet node
|
||||
@@ -767,6 +743,17 @@ class ReticulumWebChat:
|
||||
# upsert announce to database
|
||||
self.db_upsert_announce(announced_identity, destination_hash, "nomadnetwork.node", app_data)
|
||||
|
||||
# find announce from database
|
||||
announce = database.Announce.get_or_none(database.Announce.destination_hash == destination_hash.hex())
|
||||
if announce is None:
|
||||
return
|
||||
|
||||
# send database announce to all websocket clients
|
||||
asyncio.run(self.websocket_broadcast(json.dumps({
|
||||
"type": "announce",
|
||||
"announce": self.convert_db_announce_to_dict(announce),
|
||||
})))
|
||||
|
||||
|
||||
# class to manage config stored in database
|
||||
class Config:
|
||||
|
||||
Reference in New Issue
Block a user