Browse Source

resolve conflict

update/libraries
dskvr 2 years ago
parent
commit
620cbdaf34
  1. 5
      nginx/conf.d/default.conf
  2. 12
      package.json
  3. 4
      relays.yaml
  4. 33
      scripts/geo.js
  5. 8
      src/App.vue
  6. 29
      src/components/FooterComponent.vue
  7. 12
      src/components/LeafletComponent.vue
  8. 109
      src/components/LeafletSingleComponent.vue
  9. 49
      src/components/NavComponent.vue
  10. 114
      src/components/PreferencesComponent.vue
  11. 105
      src/components/RefreshComponent.vue
  12. 274
      src/components/RelayGroupedListComponent.vue
  13. 131
      src/components/RelayListComponent.vue
  14. 60
      src/components/RelaySingleComponent.vue
  15. 7
      src/components/RelayTableComponent.vue
  16. 14
      src/main.js
  17. 324
      src/pages/ByStatus.vue
  18. 139
      src/pages/HomePage.vue
  19. 429
      src/pages/SingleRelay.vue
  20. 34
      src/router/index.js
  21. 235
      src/shared.js
  22. 2
      src/styles/main.scss
  23. 24
      src/utils/index.js
  24. 3164
      yarn-error.log

5
nginx/conf.d/default.conf

@ -4,5 +4,10 @@ server {
location / {
index index.html;
# try_files $uri $uri/ /index.html;
}
location ~ /?(.*)$ {
index index.html;
}
}

12
package.json

@ -1,18 +1,15 @@
{
"name": "nostr-watch",
<<<<<<< Updated upstream
"version": "0.0.11",
=======
"version": "0.0.14",
>>>>>>> Stashed changes
"private": true,
"scripts": {
"prebuild": "node ./scripts/geo.js",
"serve": "yarn prebuild && vue-cli-service serve --host localhost",
"serve": "vue-cli-service serve --host localhost",
"build": "yarn prebuild && vue-cli-service build",
"watch": "yarn prebuild && vue-cli-service build --watch",
"lint": "vue-cli-service lint",
"docker": "yarn docker:build && yarn docker:tag && yarn docker:push",
"docker:up": "docker-compose up",
"docker:build": "docker build . -t nostr-relay-status",
"docker:tag": "docker tag nostr-relay-status:latest registry.digitalocean.com/sandwich-farm/nostr-relay-status",
"docker:push": "docker push registry.digitalocean.com/sandwich-farm/nostr-relay-status"
@ -34,7 +31,7 @@
"node-emoji": "1.11.0",
"node-polyfill-webpack-plugin": "2.0.1",
"nostr": "0.2.5",
"nostr-relay-inspector": "0.0.7",
"nostr-relay-inspector": "0.0.9",
"nostr-tools": "0.24.1",
"onion-regex": "2.0.8",
"requests": "0.3.0",
@ -46,8 +43,11 @@
"vue-final-modal": "3",
"vue-grid-responsive": "1.3.0",
"vue-nav-tabs": "0.5.7",
"vue-router": "4.1.6",
"vue-simple-maps": "1.1.3",
"vue3-popper": "1.5.0",
"vue3-storage": "0.1.11",
"vue3-tabs-component": "1.1.2",
"yaml-loader": "^0.6.0",
"yaml2json": "1.0.2"
},

4
relays.yaml

@ -1,5 +1,5 @@
relays:
- 'wss://rsslay.fiatjaf.com'
# - 'wss://rsslay.fiatjaf.com'
- 'wss://freedom-relay.herokuapp.com/ws'
- 'wss://nostr-relay.freeberty.net'
- 'wss://nostr-relay.wlvs.space'
@ -23,7 +23,7 @@ relays:
- 'wss://relay.grunch.dev'
- 'wss://relay.cynsar.foundation'
- 'wss://nostr-pub.wellorder.net'
- 'wss://relayer.fiatjaf.com'
# - 'wss://relayer.fiatjaf.com'
- 'wss://relay.oldcity-bitcoiners.info'
- 'wss://relay.bitid.nz'
- 'wss://relay.nostr.xyz'

33
scripts/geo.js

@ -8,31 +8,48 @@ let object,
result,
file = fs.readFileSync('./relays.yaml', 'utf8')
const getIp = async function(relay){
let ip
const getDns = async function(relay){
let dns
await fetch(`https://1.1.1.1/dns-query?name=${relay.replace('wss://', '')}`, { headers: { 'accept': 'application/dns-json' } })
.then(response => response.json())
.then((data) => { ip = data.Answer ? data.Answer[data.Answer.length-1].data : false })
.then((data) => { dns = data.Answer ? data.Answer : false })
.catch(err => console.log('./scripts/geo.js', err))
return dns
}
const getIp = async function(dns){
let ip;
if(dns)
ip = dns[dns.length-1].data
return ip
}
const getGeo = async function(ip) {
let geo
await fetch(`http://ip-api.com/json/${ip}`, { headers: { 'accept': 'application/dns-json' } })
.then(response => response.json())
.then((data) => { geo = data })
.catch(err => console.log('./scripts/geo.js', err))
return geo
.then(response => response.json())
.then((data) => { geo = data })
.catch(err => console.log('./scripts/geo.js', err))
return geo;
}
const query = async function(){
const relays = YAML.parse(file).relays,
result = {}
for (const relay of relays) {
ip = await getIp(relay)
let dns, ip, geo
dns = await getDns(relay)
ip = await getIp(dns)
// console.log(dns, ip)
geo = await getGeo(ip)
// console.log(geo, ip, dns)
if(dns)
geo.dns = dns[dns.length-1]
if(geo.status == 'success')
result[relay] = geo
}

8
src/App.vue

@ -1,15 +1,12 @@
<template>
<RelayTableComponent />
<router-view></router-view>
</template>
<script>
import RelayTableComponent from './components/RelayTableComponent.vue'
export default {
name: 'App',
components: {
RelayTableComponent,
}
components: {}
}
</script>
@ -20,6 +17,5 @@ export default {
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

29
src/components/FooterComponent.vue

@ -0,0 +1,29 @@
<template>
<section id="footer">
<span>Updated {{ refreshData.sinceLast }} ago</span>
<span><button @click="invalidate(true)">Update Now</button></span>
<span><input type="checkbox" id="checkbox" v-model="preferences.refresh" /><label for="">Refresh Automatically</label></span>
<span v-if="preferences.refresh"> Next refresh in: {{ refreshData.untilNext }}</span>
<span v-if="preferences.refresh">
Refresh Every
<input type="radio" id="1w" :value="1000*60*60*24*7" v-model="cacheExpiration" />
<label for="1w">1 Week</label>
<input type="radio" id="1d" :value="1000*60*60*24" v-model="cacheExpiration" />
<label for="1d">1 day</label>
<input type="radio" id="30m" :value="1000*60*30" v-model="cacheExpiration" />
<label for="30m">30 minutes</label>
<input type="radio" id="10m" :value="1000*60*10" v-model="cacheExpiration" />
<label for="10m">10 minutes</label>
<input type="radio" id="1m" :value="1000*60" v-model="cacheExpiration" />
<label for="1m">1 Minute</label>
</span>
</section>
</template>
<script>
</script>

12
src/components/LeafletComponent.vue

@ -51,29 +51,23 @@ export default {
},
methods: {
getLatLng(geo){
console.log('meow', [geo.lat, geo.lon])
return [geo.lat, geo.lon]
},
getCircleColor(relay){
if(this.result[relay]?.aggregate == 'public') {
console.log('woof', relay, this.result[relay]?.aggregate)
return '#00AA00'
}
else if(this.result[relay]?.aggregate == 'restricted') {
console.log('woof', relay, this.result[relay]?.aggregate)
return '#FFA500'
}
else if(this.result[relay]?.aggregate == 'offline') {
console.log('woof', relay, this.result[relay]?.aggregate)
return '#FF0000'
}
return 'transparent'
}
},
async mounted() {
console.log('GEO', Object.entries(this.geo))
},
async mounted() {},
props: {
geo: {
type: Object,
@ -98,8 +92,10 @@ export default {
<style>
.leaflet-container {
margin-top:37px;
margin:0;
padding:0;
height:250px !important;
width:100%;
}
.leaflet-control-zoom {
display: none !important;

109
src/components/LeafletSingleComponent.vue

@ -0,0 +1,109 @@
<template>
<l-map
ref="map"
v-model:zoom="zoom"
:center="[47.41322, -1.219482]"
:minZoom="zoom"
:maxZoom="zoom"
:zoomControl="false"
>
<l-tile-layer
url="http://{s}.tile.osm.org/{z}/{x}/{y}.png"
layer-type="base"
name="OpenStreetMap"
/>
<!-- <l-marker v-for="([relay, result]) in Object.entries(geo)" :lat-lng="getLatLng(result)" :key="relay">
<l-popup>
{{ relay }}
</l-popup>
</l-marker> -->
<l-circle-marker
:lat-lng="getLatLng()"
:radius="3"
:weight="6"
:color="getCircleColor(relay)"
:fillOpacity="1"
:class="relay"
>
<l-popup>
{{ relay }}
</l-popup>
</l-circle-marker>
</l-map>
</template>
<script>
import "leaflet/dist/leaflet.css"
import { LMap, LTileLayer, LCircleMarker, LPopup } from "@vue-leaflet/vue-leaflet";
export default {
components: {
LMap,
LTileLayer,
LCircleMarker,
LPopup
},
methods: {
getLatLng(){
console.log("geo", this.relay, this.geo[this.relay].lat, this.geo[this.relay].lon)
// if (!geo[this.relay]) console.log("no geo?", geo, this.relay, geo[this.relay])
return [this.geo[this.relay].lat, this.geo[this.relay].lon]
},
getCircleColor(relay){
if(this.result[relay]?.aggregate == 'public') {
return '#00AA00'
}
else if(this.result[relay]?.aggregate == 'restricted') {
return '#FFA500'
}
else if(this.result[relay]?.aggregate == 'offline') {
return '#FF0000'
}
return 'black'
}
},
async mounted() {
console.log('GEO', this.geo[this.relay])
},
props: {
geo: {
type: Object,
default(){
return {}
}
},
result: {
type: Object,
default(){
return {}
}
},
relay: {
type: String,
default(){
return ""
}
},
},
data() {
return {
zoom: 2
};
},
};
</script>
<style>
.leaflet-container {
margin:0;
height:250px !important;
width:100%;
}
.leaflet-control-zoom {
display: none !important;
}
</style>

49
src/components/NavComponent.vue

@ -1,8 +1,53 @@
<template>
<nav class="menu">
<ul>
<li><a href="#">add relay</a></li>
<li><a href="#">github</a></li>
<router-link :to="`/`" active-class="active">Home</router-link>
<router-link :to="`/status`" active-class="active">Grouped</router-link>
<a href="https://github.com/dskvr/nostr-watch/edit/main/relays.yaml" target="_blank">Add Relay</a>
<span>
<PreferencesComponent />
</span>
</ul>
</nav>
</template>
<style scoped>
nav.menu {
position:relative;
z-index:10;
}
nav span,
nav.menu a {
display: inline-block;
}
nav.menu a {
text-decoration: none;
margin: 0 22px 0 0;
padding:5px 10px;
color:#000;
border-bottom: 1px dotted #999;
}
nav.menu a.active {
background:#000;
color: #fff;
border: none;
}
nav.menu a:hover {
background: #f0f0f0;
}
</style>
<script>
import { defineComponent } from 'vue'
import PreferencesComponent from '../components/PreferencesComponent.vue'
export default defineComponent({
title: "nostr.watch registry & network status",
name: 'NavComponent',
components: {
PreferencesComponent,
}
});
</script>

114
src/components/PreferencesComponent.vue

@ -0,0 +1,114 @@
<template>
<section id="preferences-toggle">
<button @click="toggle"></button>
<section id="preferences" :class="{active:isActive}">
<span><input type="checkbox" id="checkbox" v-model="preferences.refresh" /><label for="">Refresh Automatically</label></span>
<span v-if="preferences.refresh">
Refresh Every
<ul>
<li>
<input type="radio" id="1w" :value="1000*60*60*24*7" v-model="preferences.cacheExpiration" />
<label for="1w">1 Week</label>
</li>
<li>
<input type="radio" id="1d" :value="1000*60*60*24" v-model="preferences.cacheExpiration" />
<label for="1d">1 day</label>
</li>
<li>
<input type="radio" id="30m" :value="1000*60*30" v-model="preferences.cacheExpiration" />
<label for="30m">30 minutes</label>
</li>
<li>
<input type="radio" id="10m" :value="1000*60*10" v-model="preferences.cacheExpiration" />
<label for="10m">10 minutes</label>
</li>
<li>
<input type="radio" id="1m" :value="1000*60" v-model="preferences.cacheExpiration" />
<label for="1m">1 Minute</label>
</li>
</ul>
</span>
</section>
</section>
</template>
<style scoped>
section#preferences {
display:none;
}
section#preferences.active {
display:block;
}
section#preferences-toggle {
position:relative;
text-align:left;
}
section#preferences {
position:absolute;
top:40px;
left:0px;
padding: 5px 10px;
background:#f5f5f5;
width:200px;
}
section#preferences > span {
display:block;
margin: 5px 0 0;
}
ul, li {
list-style:none;
padding:0;
margin:0;
}
button {
cursor: pointer;
padding:0;
margin:0;
background: transparent;
border: none;
}
</style>
<script>
import { defineComponent } from 'vue'
import sharedMethods from '../shared'
import { useStorage } from "vue3-storage";
const localMethods = {
toggle() {
this.isActive = !this.isActive;
},
}
export default defineComponent({
name: 'PreferencesComponent',
components: {},
mounted(){
this.storage = useStorage()
this.preferences = this.getState('preferences') || this.preferences
},
updated(){
this.saveState('preferences')
},
computed: {},
methods: Object.assign(localMethods, sharedMethods),
props: {},
data() {
return {
storage: null,
refresh: true,
preferences: {
refresh: true,
cacheExpiration: 30*60*1000
},
isActive: false,
}
},
})
</script>

105
src/components/RefreshComponent.vue

@ -0,0 +1,105 @@
<template>
<section id="refresh">
<span>Updated {{ refreshData?.sinceLast }} ago <button @click="invalidate(true)">Update Now</button></span>
<span v-if="preferences.refresh"> Next refresh in: {{ refreshData?.untilNext }}</span>
</section>
</template>
<style scoped>
</style>
<script>
import { defineComponent, reactive } from 'vue'
import sharedMethods from '../shared'
import { useStorage } from "vue3-storage";
const localMethods = {
timeUntilRefresh(){
return this.timeSince(Date.now()-(this.lastUpdate+this.preferences.cacheExpiration-Date.now()))
},
timeSinceRefresh(){
return this.timeSince(this.lastUpdate)
},
nextRefresh: function(){
return this.timeSince(Date.now()-(this.lastUpdate+this.preferences.cacheExpiration-Date.now()))
},
}
export default defineComponent({
name: 'RefreshComponent',
components: {},
mounted(){
this.storage = useStorage()
this.lastUpdate = this.getState('lastUpdate')|| this.lastUpdate
this.preferences = this.getState('preferences') || this.preferences
this.refreshData = reactive({
untilNext: this.timeUntilRefresh(),
sinceLast: this.timeSinceRefresh()
})
setInterval(() => {
this.preferences = this.getState('preferences') || this.preferences
this.refreshData.untilNext = this.timeUntilRefresh()
this.refreshData.sinceLast = this.timeSinceRefresh()
if(this.isExpired())
this.invalidate()
}, 1000)
},
updated(){
this.saveState('preferences')
if(this.isDone()) {
this.saveState('lastUpdate')
console.log('isDone()', this.getState('lastUpdate') )
}
this.refreshData.untilNext = this.timeUntilRefresh()
this.refreshData.sinceLast = this.timeSinceRefresh()
},
computed: {},
methods: Object.assign(localMethods, sharedMethods),
props: {
relaysProp:{
type: Object,
default(){
return {}
}
},
messagesProp:{
type: Object,
default(){
return {}
}
},
resultProp: {
type: Object,
default(){
return {}
}
},
},
data() {
return {
relays: this.relaysProp,
result: this.resultProp,
messages: this.messagesProp,
storage: null,
lastUpdate: null,
refresh: true,
refreshData: this.refreshDataProp,
interval: null,
preferences: {
refresh: true,
cacheExpiration: 30*60*1000
}
}
},
})
</script>

274
src/components/RelayGroupedListComponent.vue

@ -0,0 +1,274 @@
<template>
<tr :class="getHeadingClass()">
<vue-final-modal v-model="showModal" classes="modal-container" content-class="modal-content">
<div class="modal__content">
<span>
{{ queryJson(section) }}
</span>
</div>
</vue-final-modal>
<td colspan="11">
<h2><span class="indicator badge">{{ query(section).length }}</span>{{ section }} <a @click="showModal=true" class="section-json" v-if="showJson">{...}</a></h2>
</td>
</tr>
<tr :class="getHeadingClass()" v-if="query(section).length > 0">
<th class="table-column status-indicator"></th>
<th class="table-column relay"></th>
<th class="table-column verified">
<span class="verified-shape-wrapper">
<span class="shape verified"></span>
</span>
</th>
<th class="table-column location" v-tooltip:top.tooltip="Ping">
🌎
</th>
<th class="table-column latency" v-tooltip:top.tooltip="'Relay Latency on Read'">
</th>
<th class="table-column connect" v-tooltip:top.tooltip="'Relay connection status'">
🔌
</th>
<th class="table-column read" v-tooltip:top.tooltip="'Relay read status'">
👁🗨
</th>
<th class="table-column write" v-tooltip:top.tooltip="'Relay write status'">
</th>
<th class="table-column info" v-tooltip:top.tooltip="'Additional information detected regarding the relay during processing'">
ℹ️
</th>
<!-- <th class="table-column nip nip-20" v-tooltip:top.tooltip="'Does the relay support NIP-20'">
<span>NIP-11</span>
</th> -->
</tr>
<tr v-for="relay in query(section)" :key="{relay}" :class="getResultClass(relay)" class="relay">
<RelaySingleComponent
:relay="relay"
:result="result[relay]"
:geo="geo[relay]"
:showColumns="showColumns"
:connection="connections[relay]"
/>
</tr>
</template>
<script>
import { defineComponent} from 'vue'
import RelaySingleComponent from './RelaySingleComponent.vue'
import { VueFinalModal } from 'vue-final-modal'
export default defineComponent({
name: 'RelayListComponent',
components: {
RelaySingleComponent,
VueFinalModal
},
props: {
showJson: {
type: Boolean,
default(){
return true
}
},
section: {
type: String,
required: true,
default: ""
},
relays:{
type: Object,
default(){
return {}
}
},
result: {
type: Object,
default(){
return {}
}
},
geo: {
type: Object,
default(){
return {}
}
},
messages: {
type: Object,
default(){
return {}
}
},
alerts: {
type: Object,
default(){
return {}
}
},
connections: {
type: Object,
default(){
return {}
}
},
showColumns: {
type: Object,
default() {
return {
connectionStatuses: false,
nips: false,
geo: false,
additionalInfo: false
}
}
},
grouping: {
type: Boolean,
default(){
return true
}
}
},
data() {
return {
showModal: false
}
},
mounted(){
console.log('')
},
computed: {},
methods: {
getHeadingClass(){
return {
online: this.section != "offline",
public: this.section == "public",
offline: this.section == "offline",
restricted: this.section == "restricted"
}
},
getResultClass (relay) {
return {
loaded: this.result?.[relay]?.state == 'complete',
online: this.section != "offline",
offline: this.section == "offline",
public: this.section == "public"
}
},
query (aggregate) {
let unsorted,
sorted,
filterFn
filterFn = (relay) => this.grouping ? this.result?.[relay]?.aggregate == aggregate : true
unsorted = this.relays.filter(filterFn);
console.log('unsorted', unsorted)
console.log('isDone', this.isDone())
if(!this.isDone()) {
return unsorted
}
if (unsorted.length) {
sorted = unsorted.sort((relay1, relay2) => {
return this.result?.[relay1]?.latency.final - this.result?.[relay2]?.latency.final
})
console.log('sorted', sorted)
return sorted
}
return []
},
queryJson(aggregate){
const relays = this.query(aggregate)
const result = {}
result.relays = relays.map( relay => relay )
return JSON.stringify(result,null,'\t')
},
relaysTotal () {
return this.relays.length
},
relaysConnected () {
return Object.keys(this.result).length
},
relaysCompleted () {
let value = Object.entries(this.result).map((value) => { return value.state == 'complete' }).length
console.log('relaysCompleted', value)
return value
},
isDone(){
console.log('isDone()', this.relaysTotal(), '-', this.relaysCompleted(), '=', this.relaysTotal()-this.relaysCompleted() )
return this.relaysTotal()-this.relaysCompleted() == 0
},
}
})
</script>
<style lang='css' scoped>
.nip span {
text-transform: uppercase;
letter-spacing:-1px;
font-size:12px;
}
.section-json {
font-size:13px;
color: #555;
cursor:pointer;
}
::v-deep(.modal-container) {
display: flex;
justify-content: center;
align-items: center;
}
::v-deep(.modal-content) {
position: relative;
display: flex;
flex-direction: column;
max-height: 90%;
max-width:400px;
margin: 0 1rem;
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 0.25rem;
background: #fff;
}
.modal__title {
margin: 0 2rem 0 0;
font-size: 1.5rem;
font-weight: 700;
}
.modal__content {
flex-grow: 1;
overflow-y: auto;
}
.modal__action {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
padding: 1rem 0 0;
}
.modal__close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
}
.nip-11 a { cursor: pointer }
</style>
<style scoped>
.dark-mode div ::v-deep(.modal-content) {
border-color: #2d3748;
background-color: #1a202c;
}
</style>

131
src/components/RelayListComponent.vue

@ -1,17 +1,17 @@
<template>
<tr :class="getHeadingClass()">
<tr>
<vue-final-modal v-model="showModal" classes="modal-container" content-class="modal-content">
<div class="modal__content">
<span>
{{ queryJson(section) }}
{{ queryJson() }}
</span>
</div>
</vue-final-modal>
<td colspan="11">
<h2><span class="indicator badge">{{ query(section).length }}</span>{{ section }} <a @click="showModal=true" class="section-json" v-if="showJson">{...}</a></h2>
<h2><span class="indicator badge">{{ this.relays.length }}</span>Relays <a @click="showModal=true" class="section-json" v-if="showJson">{...}</a></h2>
</td>
</tr>
<tr :class="getHeadingClass()" v-if="query(section).length > 0">
<tr v-if="this.relays.length > 0">
<th class="table-column status-indicator"></th>
<th class="table-column relay"></th>
@ -39,16 +39,15 @@
<th class="table-column info" v-tooltip:top.tooltip="'Additional information detected regarding the relay during processing'">
ℹ️
</th>
<th class="table-column nip nip-20" v-tooltip:top.tooltip="'Does the relay support NIP-20'">
<!-- <th class="table-column nip nip-20" v-tooltip:top.tooltip="'Does the relay support NIP-20'">
<span>NIP-11</span>
</th>
</th> -->
</tr>
<tr v-for="relay in query(section)" :key="{relay}" :class="getResultClass(relay)" class="relay">
<tr v-for="relay in sortByLatency()" :key="{relay}" class="relay" :class="getResultClass(relay)">
<RelaySingleComponent
:relay="relay"
:result="result[relay]"
:geo="geo[relay]"
:showColumns="showColumns"
:connection="connections[relay]"
/>
</tr>
@ -74,11 +73,6 @@ export default defineComponent({
return true
}
},
section: {
type: String,
required: true,
default: "public"
},
relays:{
type: Object,
default(){
@ -115,17 +109,6 @@ export default defineComponent({
return {}
}
},
showColumns: {
type: Object,
default() {
return {
connectionStatuses: false,
nips: false,
geo: false,
additionalInfo: false
}
}
}
},
data() {
return {
@ -137,61 +120,91 @@ export default defineComponent({
},
computed: {},
methods: {
getHeadingClass(){
return {
online: this.section != "offline",
public: this.section == "public",
offline: this.section == "offline",
restricted: this.section == "restricted"
}
},
// getHeadingClass(){
// return {
// online: this.section != "offline",
// public: this.section == "public",
// offline: this.section == "offline",
// restricted: this.section == "restricted"
// }
// },
getResultClass (relay) {
return {
loaded: this.result?.[relay]?.state == 'complete',
online: this.section != "offline",
offline: this.section == "offline",
public: this.section == "public"
loaded: this.result?.[relay]?.state == 'complete'
}
},
query (aggregate) {
let unordered,
filterFn
sort_by_latency(ascending) {
const self = this
return function (a, b) {
// equal items sort equally
if (self.result?.[a]?.latency.final === self.result?.[b]?.latency.final) {
return 0;
}
filterFn = (relay) => this.result?.[relay]?.aggregate == aggregate
// nulls sort after anything else
if (self.result?.[a]?.latency.final === null) {
return 1;
}
if (self.result?.[b]?.latency.final === null) {
return -1;
}
unordered = this.relays.filter(filterFn);
// otherwise, if we're ascending, lowest sorts first
if (ascending) {
return self.result?.[a]?.latency.final - self.result?.[b]?.latency.final;
}
if(!this.isDone()) {
return unordered
}
// if descending, highest sorts first
return self.result?.[b]?.latency.final-self.result?.[a]?.latency.final;
};
},
sortByLatency () {
let unsorted
if (unordered.length) {
return unordered.sort((relay1, relay2) => {
return this.result?.[relay1]?.latency.final - this.result?.[relay2]?.latency.final
})
}
unsorted = this.relays;
console.log('unsorted', unsorted)
// console.log('isDone', this.isDone())
// if(!this.isDone())
// return unsorted
if (unsorted.length)
return unsorted.sort(this.sort_by_latency(true))
return []
},
queryJson(aggregate){
const relays = this.query(aggregate)
const result = {}
result.relays = relays.map( relay => relay )
queryJson(){
const result = { relays: this.relays }
return JSON.stringify(result,null,'\t')
},
relaysTotal () {
return this.relays.length-1
return this.relays.length //TODO: Figure out WHY?
},
relaysConnected () {
return Object.keys(this.result).length
return Object.entries(this.result).length
},
relaysCompleted () {
let value = Object.entries(this.result).length
return value
relaysComplete () {
if(!Object.keys(this.results).length) return 0
return this.relays.filter(relay => this.results?.[relay]?.state == 'complete').length
},
sha1 (message) {
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex')
return hash
},
isDone(){
return this.relaysTotal()-this.relaysCompleted() == 0
console.log('is done', this.relaysTotal(), '-', this.relaysComplete(), '<=', 0, this.relaysTotal()-this.relaysComplete() <= 0)
return this.relaysTotal()-this.relaysComplete() <= 0
},
loadingComplete(){
return this.isDone() ? 'loaded' : ''
}
}
})
</script>

60
src/components/RelaySingleComponent.vue

@ -1,23 +1,25 @@
<template>
<td class="status-indicator" :key="generateKey(relay, 'aggregate')">
<span :class="result.aggregate" class="aggregate indicator">
<span :class="result?.aggregate" class="aggregate indicator">
<span></span>
<span></span>
</span>
</td>
<td class="relay left-align relay-url">
<span @click="copy(relay)" v-tooltip:top.tooltip="'Click to copy'">{{ relay }}</span>
<router-link :to="`/relay/${relayClean(relay)}`" active-class="active">{{ relay }}</router-link>
</td>
<td class="verified">
<span v-tooltip:top.tooltip="identityList()"> <span class="verified-shape-wrapper" v-if="Object.entries(result.identities).length"><span class="shape verified"></span></span></span>
<span v-if="result?.identities">
<span v-tooltip:top.tooltip="identityList()"> <span class="verified-shape-wrapper" v-if="Object.entries(result?.identities).length"><span class="shape verified"></span></span></span>
</span>
</td>
<td class="location">{{ getFlag() }}</td>
<td class="latency">
<span>{{ result.latency.final }}<span v-if="result.check.latency">ms</span></span>
<span>{{ result?.latency.final }}<span v-if="result?.check.latency">ms</span></span>
</td>
<td class="connect" :key="generateKey(relay, 'check.connect')">
@ -33,44 +35,44 @@
</td>
<td class="info">
<ul v-if="result.observations && result.observations.length">
<li class="observation" v-for="(alert) in result.observations" :key="generateKey(relay, alert.description)">
<ul v-if="result?.observations && result?.observations.length">
<li class="observation" v-for="(alert) in result?.observations" :key="generateKey(relay, alert.description)">
<span v-tooltip:top.tooltip="alert.description" :class="alert.type" v-if="alert.type == 'notice'"></span>
<span v-tooltip:top.tooltip="alert.description" :class="alert.type" v-if="alert.type == 'caution'"></span>
</li>
</ul>
</td>
<td class="nip nip-11">
<a v-if="result.info" @click="showModal=true"> </a>
</td>
<!-- <td class="nip nip-11">
<a v-if="result?.info" @click="showModal=true"> </a>
</td> -->
<vue-final-modal v-model="showModal" classes="modal-container" content-class="modal-content">
<div class="modal__title">
<span>{{ result.info?.name }}</span>
<span>{{ result?.info?.name }}</span>
</div>
<div class="modal__content">
<div v-if="result.info?.description">
{{ result.info?.description }} <br/>
<strong v-if="result.info?.pubkey">Public Key:</strong> {{ result.info?.pubkey }} <br/>
<strong v-if="result.info?.contact">Contact:</strong> <SafeMail :email="result.info?.contact" v-if="result.info?.contact" />
<div v-if="result?.info?.description">
{{ result?.info?.description }} <br/>
<strong v-if="result?.info?.pubkey">Public Key:</strong> {{ result?.info?.pubkey }} <br/>
<strong v-if="result?.info?.contact">Contact:</strong> <SafeMail :email="result?.info?.contact" v-if="result?.info?.contact" />
</div>
<!-- <div>
<div>
<h4>Status</h4>
<ul>
<li><strong>Connected</strong> <span :class="getResultClass(relay, 'connect')" class="connect indicator"></span></li>
<li><strong>Read</strong> <span :class="getResultClass(relay, 'read')" class="read indicator"></span></li>
<li><strong>Write</strong> <span :class="getResultClass(relay, 'write')" class="write indicator"></span></li>
</ul>
</div> -->
</div>
<h4>Relay Info</h4>
<ul>
<li><strong>Software:</strong> {{ result.info?.software }} </li>
<li><strong>Version</strong>: {{ result.info?.version }} </li>
<li><strong>Software:</strong> {{ result?.info?.software }} </li>
<li><strong>Version</strong>: {{ result?.info?.version }} </li>
</ul>
<h4>NIP Support</h4>
<ul>
<li v-for="(nip) in result.info?.supported_nips" :key="`${relay}_${nip}`">
<li v-for="(nip) in result?.info?.supported_nips" :key="`${relay}_${nip}`">
<a :href="nipLink(nip)" target="_blank">{{ nipFormatted(nip) }}</a>
</li>
</ul>
@ -167,14 +169,16 @@ export default defineComponent({
identityList () {
let string = '',
extraString = '',
users = Object.entries(this.result.identities),
users = Object.entries(this.result?.identities),
count = 0
console.log(this.result.uri, 'admin', this.result.identities.serverAdmin)
// if(!this.result?.identities) return
console.log(this.result?.uri, 'admin', this.result?.identities.serverAdmin, this.result.info)
if(this.result.identities) {
if(this.result.identities.serverAdmin) {
string = `Relay has registered an administrator pubkey: ${this.result.identities.serverAdmin}. `
if(this.result?.identities) {
if(this.result?.identities.serverAdmin) {
string = `Relay has registered an administrator pubkey: ${this.result?.identities.serverAdmin}. `
extraString = "Additionally, "
}
@ -192,6 +196,9 @@ export default defineComponent({
}
return string
},
relayClean(relay) {
return relay.replace('wss://', '')
},
nipSignature(key){
return key.toString().length == 1 ? `0${key}` : key
},
@ -273,4 +280,9 @@ td.verified span {
border-color: #2d3748;
background-color: #1a202c;
}
.restricted.aggregate.indicator {
position:relative;
left:-7px;
}
</style>

7
src/components/RelayTableComponent.vue

@ -213,6 +213,11 @@ export default defineComponent({
return Object.entries(this.result).length
},
relaysComplete () {
if(!Object.keys(this.results).length) return 0
return this.relays.filter(relay => this.results?.[relay]?.state == 'complete').length
},
sha1 (message) {
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex')
// //console.log(message, ':', hash)
@ -220,7 +225,7 @@ export default defineComponent({
},
isDone(){
return this.relaysTotal()-this.relaysConnected() == 0
return this.relaysTotal()-this.relaysComplete() == 0
},
loadingComplete(){

14
src/main.js

@ -1,14 +1,20 @@
import { createApp } from 'vue'
import App from './App.vue'
import Vue3Storage from "vue3-storage";
import router from './router'
import "./styles/main.scss"
import directives from "./directives/"
import titleMixin from './mixins/titleMixin'
import {Tabs, Tab} from 'vue3-tabs-component';
const app = createApp(App)
app.mixin(titleMixin)
.use(router)
.use(Vue3Storage, { namespace: "nostrwatch_" })
.component('tabs', Tabs)
.component('tab', Tab)
.mixin(titleMixin)
directives(app);
app.mount('#app')
app.mount('#app')

324
src/pages/ByStatus.vue

@ -0,0 +1,324 @@
<template>
<!-- <NavComponent /> -->
<LeafletComponent
:geo="geo"
:result="result"
/>
<div id="wrapper" :class="loadingComplete()">
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<h1>nostr.watch<sup>{{version}}</sup></h1>
</column>
</row>
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<NavComponent />
</column>
</row>
<div>
<table>
<RelayGroupedListComponent
section="public"
:relays="relays"
:result="result"
:geo="geo"
:messages="messages"
:alerts="alerts"
:connections="connections"
/>
<RelayGroupedListComponent
section="restricted"
:relays="relays"
:result="result"
:geo="geo"
:messages="messages"
:alerts="alerts"
:connections="connections"
/>
<RelayGroupedListComponent
section="offline"
:relays="relays"
:result="result"
:geo="geo"
:messages="messages"
:alerts="alerts"
:connections="connections"
/>
<RelayGroupedListComponent
section="processing"
:relays="relays"
:result="result"
:messages="messages"
:alerts="alerts"
:connections="connections"
:showJson="false"
/>
</table>
</div>
<section id="footer">
<RefreshComponent
:relaysProp="relays"
v-bind:resultProp="result"
:messagesProp="messages"
/>
<span class="credit"><a href="http://sandwich.farm">Another 🥪 by sandwich.farm</a>, built with <a href="https://github.com/jb55/nostr-js">nostr-js</a> and <a href="https://github.com/dskvr/nostr-relay-inspector">nostr-relay-inspector</a>, inspired by <a href="https://github.com/fiatjaf/nostr-relay-registry">nostr-relay-registry</a></span>
</section>
</div>
</template>
<script>
import { defineComponent} from 'vue'
import { useStorage } from "vue3-storage";
// import { CallbackResult } from "vue3-storage/dist/lib/types";
import crypto from "crypto"
import { Row, Column } from 'vue-grid-responsive';
import { Inspector, InspectorObservation, InspectorResult } from 'nostr-relay-inspector'
// import RelayListComponent from '../components/RelayListComponent.vue'
import RelayGroupedListComponent from '../components/RelayGroupedListComponent.vue'
import LeafletComponent from '../components/LeafletComponent.vue'
import NavComponent from '../components/NavComponent.vue'
import RefreshComponent from '../components/RefreshComponent.vue'
import { version } from '../../package.json'
import { relays } from '../../relays.yaml'
import { geo } from '../../geo.yaml'
import { messages as RELAY_MESSAGES, codes as RELAY_CODES } from '../../codes.yaml'
export default defineComponent({
title: "nostr.watch registry & network status",
name: 'RelayTableComponent',
components: {
Row,
Column,
// RelayListComponent,
RelayGroupedListComponent,
LeafletComponent,
NavComponent,
RefreshComponent
},
data() {
return {
relays,
result: {},
messages: {},
connections: {},
nips: {},
alerts: {},
timeouts: {},
intervals: {},
lastPing: Date.now(),
nextPing: Date.now() + (30*60*1000),
count: 0,
storage: null,
geo,
version: version,
hasStorage: false,
lastUpdate: null,
cacheExpiration: (30*60*1000),
}
},
updated() {
Object.keys(this.timeouts).forEach(timeout => clearTimeout(this.timeouts[timeout]))
Object.keys(this.intervals).forEach(interval => clearInterval(this.intervals[interval]))
},
async mounted() {
this.relays.forEach(relay => {
this.result[relay] = structuredClone(InspectorResult)
})
this.storage = useStorage()
this.storage.setStorageSync('relays', relays)
this.lastUpdate = this.storage.getStorageSync('lastUpdate')
this.relays.forEach(async relay => {
this.result[relay] = this.storage.getStorageSync(relay)
})
console.log('meow', this.result)
if(Object.keys(this.result).length)
this.hasStorage = true
if(this.isExpired())
this.relays.forEach(async relay => await this.check(relay) )
console.log('zzz', this.result)
// this.relays.forEach( relay => {
// // this.result[relay].state = 'complete'
// // this.setAggregateResult(relay)
// // this.adjustResult(relay)
// console.log('boom', relay, this.result[relay])
// })
// }
// console.log(`zing ${Date.now()} - ${this.lastUpdate} = ${Date.now()-this.lastUpdate} > ${60*1000}`)
return true
},
// head: {
// // creates a title tag in header.
// base () {
// return {
// href: "/"
// }
// }
// },
computed: {},
methods: {
isExpired(){
return typeof this.lastUpdate === 'undefined' || Date.now() - this.lastUpdate > this.cacheExpiration
},
saveState(relay){
this.storage
.setStorage({
key: relay,
data: this.result[relay]
})
.then(successCallback => {
console.log(successCallback.errMsg);
})
.catch(failCallback => {
console.log(failCallback.errMsg);
})
this.storage
.setStorage({
key: "lastUpdate",
data: Date.now()
})
.then(successCallback => {
console.log(successCallback.errMsg);
this.lastUpdate = Date.now()
})
.catch(failCallback => {
console.log(failCallback.errMsg);
})
},
resetState(){
this.relays.forEach(relay=>{
this.storage.removeStorage(relay)
})
},
async check(relay){
return new Promise( (resolve, reject) => {
const opts = {
checkLatency: true,
setIP: false,
setGeo: false,
debug: true,
}
let inspect = new Inspector(relay, opts)
.on('run', (result) => {
result
// result.aggregate = 'processing'
})
.on('open', (e, result) => {
this.result[relay] = result
})
.on('complete', (instance) => {
this.result[relay] = Object.assign(this.result[relay], instance.result)
this.messages[relay] = instance.inbox
// this.setFlag(relay)
this.setAggregateResult(relay)
// this.adjustResult(relay)
this.saveState(relay)
resolve(this.result[relay])
})
.on('notice', (notice) => {
const hash = this.sha1(notice)
let message_obj = RELAY_MESSAGES[hash]
let code_obj = RELAY_CODES[message_obj.code]
let response_obj = {...message_obj, ...code_obj}
this.result[relay].observations.push( new InspectorObservation('notice', response_obj.code, response_obj.description, response_obj.relates_to) )
})
.on('close', () => {})
.on('error', () => {
reject(this.result[relay])
})
.run()
this.connections[relay] = inspect
})
},
recheck(relay){
const inspect = this.connections[relay]
inspect.checkLatency()
},
// adjustResult (relay) {
// this.result[relay].observations.forEach( observation => {
// if (observation.code == "BLOCKS_WRITE_STATUS_CHECK") {
// this.result[relay].check.write = false
// this.result[relay].aggregate = 'public'
// }
// })
// },
setAggregateResult (relay) {
let aggregateTally = 0
aggregateTally += this.result?.[relay]?.check.connect ? 1 : 0
aggregateTally += this.result?.[relay]?.check.read ? 1 : 0
aggregateTally += this.result?.[relay]?.check.write ? 1 : 0
if (aggregateTally == 3) {
this.result[relay].aggregate = 'public'
}
else if (aggregateTally == 0) {
this.result[relay].aggregate = 'offline'
}
else {
this.result[relay].aggregate = 'restricted'
}
},
relaysTotal () {
return this.relays.length //TODO: Figure out WHY?
},
relaysConnected () {
return Object.entries(this.result).filter(result => result.state == 'complete').length
},
sha1 (message) {
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex')
return hash
},
isDone(){
return this.relaysTotal()-this.relaysConnected() <= 0
},
loadingComplete(){
return this.isDone() ? 'loaded' : ''
},
},
})
</script>

139
src/pages/HomePage.vue

@ -0,0 +1,139 @@
<template>
<!-- <NavComponent /> -->
<LeafletComponent
:geo="geo"
:result="result"
/>
<div id="wrapper" :class="loadingComplete()">
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<h1>nostr.watch<sup>{{version}}</sup></h1>
</column>
</row>
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<NavComponent />
</column>
</row>
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="list">
<table>
<RelayListComponent
:relays="relays"
:result="result"
:geo="geo"
:messages="messages"
:alerts="alerts"
:connections="connections"
/>
</table>
</column>
</row>
<row container :gutter="12" v-if="(relaysTotal()-relaysConnected()>0)">
<column :xs="12" :md="12" :lg="12" class="processing-card loading">
<span>Processing {{ relaysConnected() }}/{{ relaysTotal() }}</span>
</column>
</row>
<section id="footer">
<RefreshComponent
:relaysProp="relays"
v-bind:resultProp="result"
:messagesProp="messages"
/>
<span class="credit"><a href="http://sandwich.farm">Another 🥪 by sandwich.farm</a>, built with <a href="https://github.com/jb55/nostr-js">nostr-js</a> and <a href="https://github.com/dskvr/nostr-relay-inspector">nostr-relay-inspector</a>, inspired by <a href="https://github.com/fiatjaf/nostr-relay-registry">nostr-relay-registry</a></span>
</section>
</div>
</template>
<script>
import { defineComponent } from 'vue'
import { useStorage } from "vue3-storage";
// import { CallbackResult } from "vue3-storage/dist/lib/types";
import { Row, Column } from 'vue-grid-responsive';
// import { Inspector, InspectorObservation } from '../../lib/nostr-relay-inspector'
import sharedMethods from '../shared'
import RelayListComponent from '../components/RelayListComponent.vue'
// import RelayGroupedListComponent from '../components/RelayGroupedListComponent.vue'
import LeafletComponent from '../components/LeafletComponent.vue'
import NavComponent from '../components/NavComponent.vue'
import RefreshComponent from '../components/RefreshComponent.vue'
import { version } from '../../package.json'
import { relays } from '../../relays.yaml'
import { geo } from '../../geo.yaml'
export default defineComponent({
title: "nostr.watch registry & network status",
name: 'RelayTableComponent',
components: {
Row,
Column,
RelayListComponent,
// RelayGroupedListComponent,
LeafletComponent,
NavComponent,
RefreshComponent,
},
data() {
return {
relays: relays,
result: {},
messages: {},
connections: {},
alerts: {},
timeouts: {},
intervals: {},
count: 0,
storage: null,
geo,
version: version,
hasStorage: false,
// cacheExpiration: 10*60*1000, //10 minutes
}
},
updated(){},
async mounted() {
this.storage = useStorage()
this.lastUpdate = this.getState('lastUpdate')|| this.lastUpdate
this.preferences = this.getState('preferences') || this.preferences
this.relays.forEach(async relay => {
this.result[relay] = this.getState(relay)
this.messages[relay] = this.getState(`${relay}_inbox`)
})
this.invalidate()
console.log('last update',-1*(Date.now()-(this.lastUpdate+this.preferences.cacheExpiration)))
},
computed: {
},
methods: sharedMethods
})
</script>
<style scoped>
.list {
position:relative;
z-index:1;
}
</style>

429
src/pages/SingleRelay.vue

@ -0,0 +1,429 @@
<template>
<LeafletSingleComponent
:geo="geo"
:relay="relay"
:result="result"
/>
<!-- <NavComponent /> -->
<div id="wrapper">
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<h1>{{ relayUrl() }}</h1>
</column>
</row>
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<NavComponent />
</column>
</row>
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<div style="display: none">{{result}}</div> <!-- ? -->
<br >
<span class="badges">
<span><img :src="badgeCheck('connect')" /></span>
<span><img :src="badgeCheck('read')" /></span>
<span><img :src="badgeCheck('write')" /></span>
</span>
<br />
<span v-if="result.info?.supported_nips" class="badges">
<span v-for="(nip) in result.info.supported_nips" :key="`${relay}_${nip}`">
<a :href="nipLink(nip)" target="_blank"><img :src="badgeLink(nip)" /></a>
</span>
</span>
<!--table>
<tr>
<th colspan="3"><h4>Status</h4></th>
</tr>
<tr v-if="result.checkClass">
<td :class="result.checkClass.connect" class="connect indicator">Connected</td>
<td :class="result.checkClass.read" class="read indicator">Read</td>
<td :class="result.checkClass.write" class="write indicator">Write</td>
</tr>
</table-->
<table v-if="result.info">
<tr>
<th colspan="2"><h4>Info</h4></th>
</tr>
<tr v-for="(value, key) in Object.entries(result.info).filter(value => value[0] != 'id' && value[0] != 'supported_nips')" :key="`${value}_${key}`">
<td>{{ value[0] }}</td>
<td v-if="value[0]!='contact' && value[0]!='pubkey' && value[0]!='software' && value[0]!='version'">{{ value[1] }} </td>
<td v-if="value[0]=='contact'"><SafeMail :email="value[1]" /></td>
<td v-if="value[0]=='pubkey' || value[0]=='version'"><code>{{ value[1] }}</code></td>
<td v-if="value[0]=='software'"><a href="{{ value[1] }}">{{ value[1] }}</a></td>
</tr>
</table>
<h4>Identities</h4>
<table v-if="result.identities">
<tr v-for="(value, key) in Object.entries(result?.identities)" :key="`${value}_${key}`">
<td>{{ value[0] }}</td>
<td><code>{{ value[1] }}</code></td>
</tr>
</table>
<div style="display: none">{{result}}</div> <!-- ? -->
</column>
</row>
<row container :gutter="12">
<column :xs="12" :md="6" :lg="6" class="title-card">
<h4>GEO {{geo?.countryCode ? getFlag() : ''}}</h4>
<table v-if="geo[relay]">
<tr v-for="(value, key) in Object.entries(geo[relay])" :key="`${value}_${key}`">
<td>{{ value[0] }}</td>
<td>{{ value[1] }} </td>
</tr>
</table>
</column>
<column :xs="12" :md="6" :lg="6" class="title-card">
<h4>DNS</h4>
<table v-if="geo[relay]">
<tr v-for="(value, key) in Object.entries(geo[relay].dns)" :key="`${value}_${key}`">
<td>{{ value[0] }}</td>
<td>{{ value[1] }} </td>
</tr>
</table>
<div style="display: none">{{result}}</div> <!-- ? -->
</column>
</row>
<span class="credit"><a href="http://sandwich.farm">Another 🥪 by sandwich.farm</a>, built with <a href="https://github.com/jb55/nostr-js">nostr-js</a> and <a href="https://github.com/dskvr/nostr-relay-inspector">nostr-relay-inspector</a>, inspired by <a href="https://github.com/fiatjaf/nostr-relay-registry">nostr-relay-registry</a></span>
</div>
</template>
<script>
import { defineComponent} from 'vue'
import { useStorage } from "vue3-storage";
import LeafletSingleComponent from '../components/LeafletSingleComponent.vue'
import NavComponent from '../components/NavComponent.vue'
import { Row, Column } from 'vue-grid-responsive';
import SafeMail from "@2alheure/vue-safe-mail";
import emoji from 'node-emoji';
import { countryCodeEmoji } from 'country-code-emoji';
import { Inspector, InspectorObservation } from 'nostr-relay-inspector'
// import { Inspector, InspectorObservation } from '../../lib/nostr-relay-inspector'
// import { Inspector, InspectorObservation } from '../../lib/nostr-relay-inspector'
import { version } from '../../package.json'
import { relays } from '../../relays.yaml'
import { geo } from '../../geo.yaml'
import { messages as RELAY_MESSAGES, codes as RELAY_CODES } from '../../codes.yaml'
import crypto from "crypto"
export default defineComponent({
title: "nostr.watch registry & network status",
name: 'SingleRelay',
components: {
Row,
Column,
LeafletSingleComponent,
NavComponent,
SafeMail,
},
data() {
return {
relays,
result: {},
messages: {},
nips: {},
alerts: {},
timeouts: {},
intervals: {},
lastPing: Date.now(),
nextPing: Date.now() + (60*1000),
count: 0,
geo,
relay: "",
version: version,
storage: null,
lastUpdate: null,
cacheExpiration: 10*60*1000 //10 minutes
}
},
async mounted() {
this.relay = this.relayUrl()
this.storage = useStorage()
this.lastUpdate = this.storage.getStorageSync('lastUpdate')
this.result = this.storage.getStorageSync(this.relay)
if(this.isExpired())
this.check(this.relay)
// console.log('zing ', (Date.now() - this.lastUpdate) /1000)
},
computed: {
},
updated() {
Object.keys(this.timeouts).forEach(timeout => clearTimeout(this.timeouts[timeout]))
Object.keys(this.intervals).forEach(interval => clearInterval(this.intervals[interval]))
},
methods: {
isExpired(){
return typeof this.lastUpdate === 'undefined' || Date.now() - this.lastUpdate > this.preferences.cacheExpiration
},
saveState(relay){
this.storage
.setStorage({
key: relay,
data: this.result
})
.then(successCallback => {
console.log(successCallback.errMsg);
})
.catch(failCallback => {
console.log(failCallback.errMsg);
})
this.storage
.setStorage({
key: "lastUpdate",
data: Date.now()
})
.then(successCallback => {
console.log(successCallback.errMsg);
this.lastUpdate = Date.now()
})
.catch(failCallback => {
console.log(failCallback.errMsg);
})
},
relayUrl() {
// We will see what `params` is shortly
return `wss://${this.$route.params.relayUrl}`
},
async check(relay){
//const self = this
/* return new Promise(function(resolve, reject) { */
/* let nip = new Array(99).fill(false);
nip[5] = true
nip[11] = true */
const opts = {
checkLatency: true,
checkNips: true,
/* checkNip: nip, */
/* debug: true */
}
let inspect = new Inspector(relay, opts)
.on('run', (result) => {
result.aggregate = 'processing'
})
.on('open', (e, result) => {
this.result = result
this.result.checkClass = {read: null, write: null, connect: null}
this.setResultClass('connect')
/* console.log('result on open', this.result) */
})
.on('complete', (instance) => {
/* console.log('on_complete', instance.result.aggregate) */
this.result = instance.result
this.messages[this.relay] = instance.inbox
/* this.setFlag(relay) */
this.setAggregateResult()
/* this.adjustResult(relay) */
this.setResultClass('read')
this.setResultClass('write')
this.saveState(relay)
/* console.log(this.result)
console.log(this.result.info.supported_nips) */
/* resolve(this.result) */
})
.on('notice', (notice) => {
const hash = this.sha1(notice)
let message_obj = RELAY_MESSAGES[hash]
let code_obj = RELAY_CODES[message_obj.code]
let response_obj = {...message_obj, ...code_obj}
this.result.observations.push( new InspectorObservation('notice', response_obj.code, response_obj.description, response_obj.relates_to) )
/* console.log(this.result.observations) */
})
.on('close', (msg) => {
console.warn("CAUTION", msg)
/* console.log('supported_nips', inspect.result.info) */
})
.on('error', (err) => {
console.error("ERROR", err)
/* reject(err) */
})
.run()
return inspect;
/* }) */
},
setResultClass (key) {
let result = this.result?.check?.[key] === true
? 'success'
: this.result?.check?.[key] === false
? 'failure'
: 'pending'
/* console.log('result class', result) */
this.result.checkClass[key] = result
},
getLoadingClass () {
return this.result?.state == 'complete' ? "relay loaded" : "relay"
},
generateKey (url, key) {
return `${url}_${key}`
},
getFlag () {
return this.geo?.countryCode ? countryCodeEmoji(this.geo.countryCode) : emoji.get('shrug');
},
setCheck (bool) {
return bool ? '✅ ' : ''
},
badgeLink(nip){
return `https://img.shields.io/static/v1?style=for-the-badge&label=NIP&message=${this.nipSignature(nip)}&color=black`
},
badgeCheck(which){
return `https://img.shields.io/static/v1?style=for-the-badge&label=&message=${which}&color=${this.result?.check?.[which] ? 'green' : 'red'}`
},
setCross (bool) {
return !bool ? '❌' : ''
},
setCaution (bool) {
return !bool ? '⚠️' : ''
},
identityList () {
let string = '',
extraString = '',
users = Object.entries(this.result.identities),
count = 0
// console.log(this.result.uri, 'admin', this.result.identities.serverAdmin)
if(this.result.identities) {
if(this.result.identities.serverAdmin) {
string = `Relay has registered an administrator pubkey: ${this.result.identities.serverAdmin}. `
extraString = "Additionally, "
}
const total = users.filter(([key]) => key!='serverAdmin').length,
isOne = total==1
if(total) {
string = `${string}${extraString}Relay domain contains NIP-05 verification data for:`
users.forEach( ([key]) => {
if(key == "serverAdmin") return
count++
string = `${string} ${(count==total && !isOne) ? 'and' : ''} @${key}${(count!=total && !isOne) ? ', ' : ''}`
})
}
}
return string
},
nipSignature(key){
return key.toString().length == 1 ? `0${key}` : key
},
nipFormatted(key){
return `NIP-${this.nipSignature(key)}`
},
nipLink(key){
return `https://github.com/nostr-protocol/nips/blob/master/${this.nipSignature(key)}.md`
},
async copy(text) {
// console.log('copy', text)
try {
await navigator.clipboard.writeText(text);
} catch($e) {
//console.log('Cannot copy');
}
},
// adjustResult (relay) {
// this.result.observations.forEach( observation => {
// if (observation.code == "BLOCKS_WRITE_STATUS_CHECK") {
// this.result.check.write = false
// this.result.aggregate = 'public'
// }
// })
// },
setAggregateResult () {
if(!this.result) return
let aggregateTally = 0
aggregateTally += this.result?.check.connect ? 1 : 0
aggregateTally += this.result?.check.read ? 1 : 0
aggregateTally += this.result?.check.write ? 1 : 0
if (aggregateTally == 3) {
this.result.aggregate = 'public'
}
else if (aggregateTally == 0) {
this.result.aggregate = 'offline'
}
else {
this.result.aggregate = 'restricted'
}
},
sha1 (message) {
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex')
// //console.log(message, ':', hash)
return hash
},
},
})
</script>
<style scoped>
ul, ul li { padding:0; margin:0; list-style:none; }
td { padding:5px 10px; }
th h4 { text-align:center; padding:5px 10px; margin:0 0 6px; background:#f0f0f0; }
table {width:90%; max-width:90%; margin:0 auto 20px ; border: 2px solid #f5f5f5; padding:20px}
tr td:first-child { text-align:right }
tr td:last-child { text-align:left }
.indicator { display: table-cell; width:33% ; font-weight:bold; text-align: center !important; color: white; text-transform: uppercase; font-size:0.8em}
body, .grid-column { padding:0; margin:0; }
.badges { display:block; margin: 10px 0 0}
.badges > span {margin-right:5px}
#wrapper {max-width:800px}
h1 {margin: 25px 0 15px; padding:0 0 10px; border-bottom:3px solid #e9e9e9}
</style>

34
src/router/index.js

@ -0,0 +1,34 @@
// /router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomePage from '../pages/HomePage.vue'
import ByStatus from '../pages/ByStatus.vue'
import SingleRelay from '../pages/SingleRelay.vue'
const routes = [
{
path: '/relay/:relayUrl(.*)',
// name: 'nostr.watch - :relayUrl',
component: SingleRelay
},
{
path: '/status',
// name: 'nostr.watch',
component: ByStatus
},
// Added our new route file named profile.vue
{
path: '/',
// name: 'nostr.watch',
component: HomePage
},
]
// Create Vue Router Object
const router = createRouter({
history: createWebHistory(),
// base: process.env.BASE_URL,
routes: routes
})
export default router

235
src/shared.js

@ -0,0 +1,235 @@
import { Inspector, InspectorObservation } from 'nostr-relay-inspector'
import { messages as RELAY_MESSAGES, codes as RELAY_CODES } from '../codes.yaml'
import crypto from "crypto"
export default {
invalidate: function(force){
if(!this.isExpired() && !force)
return
this.relays.forEach(async relay => {
await this.check(relay)
this.relays[relay] = this.getState(relay)
this.messages[relay] = this.getState(`${relay}_inbox`)
})
// if(this.preferences.refresh)
// this.timeouts.invalidate = setTimeout(()=> this.invalidate(), 1000)
},
isExpired: function(){
return typeof this.lastUpdate === 'undefined' || Date.now() - this.lastUpdate > this.preferences.cacheExpiration
},
getState: function(key){
return this.storage.getStorageSync(key)
},
check: async function(relay){
return new Promise( (resolve, reject) => {
// if(!this.isExpired())
// return reject(relay)
const opts = {
checkLatency: true,
setIP: false,
setGeo: false,
getInfo: true,
debug: true,
// data: { result: this.result[relay] }
}
let inspect = new Inspector(relay, opts)
// .on('run', (result) => {
// result.aggregate = 'processing'
// })
// .on('open', (e, result) => {
// this.result[relay] = result
// })
.on('complete', (instance) => {
// console.log('getinfo()', instance.result.info)
this.result[relay] = instance.result
// this.setFlag(relay)
// this.adjustResult(relay)
this.result[relay].aggregate = this.getAggregate(relay)
this.saveState('relay', relay)
this.saveState('messages', relay, instance.inbox)
this.saveState('lastUpdate')
resolve(this.result[relay])
})
.on('notice', (notice) => {
const hash = this.sha1(notice)
let message_obj = RELAY_MESSAGES[hash]
let code_obj = RELAY_CODES[message_obj.code]
let response_obj = {...message_obj, ...code_obj}
this.result[relay].observations.push( new InspectorObservation('notice', response_obj.code, response_obj.description, response_obj.relates_to) )
})
.on('close', () => {})
.on('error', () => {
reject(this.result[relay])
})
.run()
inspect
})
},
saveState: function(type, key, data){
const now = Date.now()
let store, success, error, instance
switch(type){
case 'relay':
console.log('savestate', 'relay', data || this.result[data])
if(data)
data.aggregate = this.getAggregate(key)
store = {
key: key,
data: data || this.result[key],
// expire: Date.now()+1000*60*60*24*180,
}
success = () => {
if(data)
this.result[key] = data
}
break;
case 'messages':
console.log('savestate', 'messages', this.messages[data])
store = {
key: `${key}_inbox`,
data: data || this.messages[key],
// expire: Date.now()+1000*60*60*24*180,
}
success = () => {
if(data)
this.messages[key] = data
}
break;
case 'lastUpdate':
console.log('savestate', 'lastUpdate', now)
store = {
key: "lastUpdate",
data: now
}
success = () => {
// console.log('lastupdate success', successCallback.msg)
this.lastUpdate = now
}
break;
case 'preferences':
console.log('savestate', 'preferences', this.preferences)
store = {
key: "preferences",
data: this.preferences
}
break;
}
if(store)
instance = this.storage.setStorage(store)
if(success && store)
instance.then(success)
if(error && store)
instance.catch(error)
},
resetState: function(){
this.relays.forEach(relay=>{
this.storage.removeStorage(relay)
})
},
recheck: function(relay){
const inspect = this.connections[relay]
inspect.checkLatency()
},
adjustResult: function(relay) {
this.result[relay].observations.forEach( observation => {
if (observation.code == "BLOCKS_WRITE_STATUS_CHECK") {
this.result[relay].check.write = false
this.result[relay].aggregate = 'public'
}
})
},
getAggregate: function(relay) {
console.log('getAggregate()', this.result?.[relay]?.check.connect, this.result?.[relay]?.check.read, this.result?.[relay]?.check.write)
let aggregateTally = 0
aggregateTally += this.result?.[relay]?.check.connect ? 1 : 0
aggregateTally += this.result?.[relay]?.check.read ? 1 : 0
aggregateTally += this.result?.[relay]?.check.write ? 1 : 0
if (aggregateTally == 3) {
return 'public'
}
else if (aggregateTally == 0) {
return 'offline'
}
else {
return 'restricted'
}
},
relaysTotal: function() {
return this.relays.length //TODO: Figure out WHY?
},
relaysConnected: function() {
return Object.entries(this.result).length
},
relaysComplete: function() {
return this.relays.filter(relay => this.results?.[relay]?.state == 'complete').length
},
sha1: function(message) {
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex')
return hash
},
isDone: function(){
console.log('is done', this.relaysTotal(), '-', this.relaysConnected(), '<=', 0, this.relaysTotal()-this.relaysConnected() <= 0)
return this.relaysTotal()-this.relaysComplete() <= 0
},
loadingComplete: function(){
return this.isDone() ? 'loaded' : ''
},
timeSince: function(date) {
var seconds = Math.floor((new Date() - date) / 1000);
var interval = seconds / 31536000;
if (interval > 1) {
return Math.floor(interval) + " years";
}
interval = seconds / 2592000;
if (interval > 1) {
return Math.floor(interval) + " months";
}
interval = seconds / 86400;
if (interval > 1) {
return Math.floor(interval) + " days";
}
interval = seconds / 3600;
if (interval > 1) {
return Math.floor(interval) + " hours";
}
interval = seconds / 60;
if (interval > 1) {
return Math.floor(interval) + " minutes";
}
return Math.floor(seconds) + " seconds";
},
}

2
src/styles/main.scss

@ -327,4 +327,4 @@ tr.offline .location {
display: none;
}
}
}

24
src/utils/index.js

@ -0,0 +1,24 @@
export const timeSince = function(date) {
var seconds = Math.floor((new Date() - date) / 1000);
var interval = seconds / 31536000;
if (interval > 1) {
return Math.floor(interval) + " years";
}
interval = seconds / 2592000;
if (interval > 1) {
return Math.floor(interval) + " months";
}
interval = seconds / 86400;
if (interval > 1) {
return Math.floor(interval) + " days";
}
interval = seconds / 3600;
if (interval > 1) {
return Math.floor(interval) + " hours";
}
interval = seconds / 60;
if (interval > 1) {
return Math.floor(interval) + " minutes";
}
return Math.floor(seconds) + " seconds";
}

3164
yarn-error.log

File diff suppressed because it is too large
Loading…
Cancel
Save