dskvr
2 years ago
39 changed files with 2612 additions and 952 deletions
@ -1,5 +1,6 @@ |
|||
module.exports = { |
|||
presets: [ |
|||
'@vue/cli-plugin-babel/preset' |
|||
] |
|||
'@vue/cli-plugin-babel/preset', |
|||
], |
|||
plugins: ["@babel/plugin-syntax-top-level-await"] |
|||
} |
|||
|
File diff suppressed because it is too large
@ -0,0 +1,7 @@ |
|||
const tailwindcss = require('tailwindcss'); |
|||
module.exports = { |
|||
plugins: [ |
|||
'postcss-preset-env', |
|||
tailwindcss |
|||
], |
|||
}; |
@ -0,0 +1,6 @@ |
|||
<template> |
|||
|
|||
</template> |
|||
<script> |
|||
|
|||
</script> |
@ -1,126 +1,169 @@ |
|||
<template> |
|||
<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" |
|||
/> |
|||
<table v-for="section of sections" :key="`section-${section}`"> |
|||
<tr :class="getHeadingClass(section)"> |
|||
<!-- <vue-final-modal v-model="showModal" classes="modal-container" content-class="modal-content"> |
|||
<div class="modal__content"> |
|||
<pre> |
|||
{{ queryJson(section) }} |
|||
</pre> |
|||
</div> |
|||
</vue-final-modal> --> |
|||
<td colspan="11"> |
|||
<h2><span class="indicator badge">{{ sort(getByAggregate(section)).length }}</span>{{ section }} <a @click="showModal=true" class="section-json" v-if="showJson">{...}</a></h2> |
|||
</td> |
|||
</tr> |
|||
<tr :class="getHeadingClass()" v-if="sort(getByAggregate(section)). length > 0"> |
|||
<TableHeaders /> |
|||
</tr> |
|||
<tr v-for="(relay, index) in sort(getByAggregate(section))" :key="{relay}" :class="getResultClass(relay, index, section)" class="relay"> |
|||
<RelaySingleComponent |
|||
:relay="relay" /> |
|||
</tr> |
|||
</table> |
|||
</template> |
|||
|
|||
<script> |
|||
import { defineComponent} from 'vue' |
|||
|
|||
import RelaySingleComponent from './RelaySingleComponent.vue' |
|||
import TableHeaders from './TableHeaders.vue' |
|||
|
|||
import { defineComponent} from 'vue' |
|||
import RelayGroupedListComponent from './RelayGroupedListComponent.vue' |
|||
import RelaysLib from '../shared/relays-lib.js' |
|||
|
|||
import { setupStore } from '../store' |
|||
|
|||
const localMethods = { |
|||
getByAggregate(section){ |
|||
return this.store.relays.getByAggregate(section) |
|||
}, |
|||
getHeadingClass(section){ |
|||
return { |
|||
online: section != "offline", |
|||
public: section == "public", |
|||
offline: section == "offline", |
|||
restricted: section == "restricted" |
|||
} |
|||
}, |
|||
getResultClass (relay, index, section) { |
|||
return { |
|||
loaded: this.store.relays.results?.[relay]?.state == 'complete', |
|||
online: section != "offline", |
|||
offline: section == "offline", |
|||
public: section == "public", |
|||
even: index % 2, |
|||
} |
|||
}, |
|||
// queryJson(aggregate){ |
|||
// // const relays = this.sort(this.store.relays.getByAggregate(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.store.relays.results).length |
|||
}, |
|||
relaysCompleted () { |
|||
let value = Object.entries(this.store.relays.results).map((value) => { return value.state == 'complete' }).length |
|||
return value |
|||
}, |
|||
isDone(){ |
|||
return this.relaysTotal()-this.relaysCompleted() == 0 |
|||
}, |
|||
} |
|||
|
|||
export default defineComponent({ |
|||
title: "nostr.watch registry & network status", |
|||
name: 'GroupByAvailability', |
|||
components: { |
|||
RelayGroupedListComponent, |
|||
RelaySingleComponent, |
|||
TableHeaders |
|||
}, |
|||
|
|||
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 |
|||
} |
|||
props: {}, |
|||
setup(){ |
|||
return { |
|||
store : setupStore() |
|||
} |
|||
}, |
|||
|
|||
data() { |
|||
return {} |
|||
return { |
|||
showModal: false, |
|||
showJson: false, |
|||
sections: ['public', 'restricted', 'offline'], |
|||
groups: {}, |
|||
relays: [], |
|||
section: "" |
|||
} |
|||
}, |
|||
mounted(){ |
|||
this.relays = this.store.relays.getAll |
|||
console.log(this.relays) |
|||
}, |
|||
computed: {}, |
|||
methods: Object.assign(localMethods, RelaysLib) |
|||
}) |
|||
</script> |
|||
<style scoped> |
|||
table { |
|||
border-collapse: collapse !important; |
|||
} |
|||
</style> |
|||
|
|||
<style lang='css' scoped> |
|||
table { |
|||
border-collapse: collapse !important; |
|||
} |
|||
.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) { |
|||
text-align:left; |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: Column; |
|||
max-height: 500px; |
|||
max-width:800px; |
|||
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 } |
|||
|
|||
tr.even { |
|||
background:#f9f9f9 |
|||
} |
|||
</style> |
@ -1,100 +1,168 @@ |
|||
<template> |
|||
<table> |
|||
<RelayListComponent |
|||
:relays="relays" |
|||
:result="result" |
|||
:geo="geo" |
|||
:messages="messages" |
|||
:alerts="alerts" |
|||
:connections="connections" |
|||
/> |
|||
<tr> |
|||
<td colspan="11"> |
|||
<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 v-if="this.relays.length > 0"> |
|||
<TableHeaders /> |
|||
</tr> |
|||
<tr v-for="(relay, index) in sort()" :key="{relay}" class="relay" :class="getResultClass(relay, index)"> |
|||
<RelaySingleComponent |
|||
:relay="relay" |
|||
/> |
|||
</tr> |
|||
</table> |
|||
</template> |
|||
</template> |
|||
|
|||
<script> |
|||
import { defineComponent} from 'vue' |
|||
import RelayListComponent from './RelayListComponent.vue' |
|||
|
|||
export default defineComponent({ |
|||
title: "nostr.watch registry & network status", |
|||
name: 'GroupByNone', |
|||
components: { |
|||
RelayListComponent, |
|||
}, |
|||
import RelaySingleComponent from './RelaySingleComponent.vue' |
|||
import TableHeaders from './TableHeaders.vue' |
|||
|
|||
props: { |
|||
showJson: { |
|||
type: Boolean, |
|||
default(){ |
|||
return true |
|||
import RelaysLib from '../shared/relays-lib.js' |
|||
|
|||
import { setupStore } from '../store' |
|||
|
|||
const localMethods = { |
|||
// getHeadingClass(){ |
|||
// return { |
|||
// online: this.section != "offline", |
|||
// public: this.section == "public", |
|||
// offline: this.section == "offline", |
|||
// restricted: this.section == "restricted" |
|||
// } |
|||
// }, |
|||
getResultClass (relay, index) { |
|||
return { |
|||
loaded: this.store.relays.getResult(relay)?.state == 'complete', |
|||
even: index % 2 |
|||
} |
|||
}, |
|||
relays:{ |
|||
type: Object, |
|||
default(){ |
|||
return {} |
|||
} |
|||
queryJson(){ |
|||
const result = { relays: this.relays } |
|||
return JSON.stringify(result,null,'\t') |
|||
}, |
|||
result: { |
|||
type: Object, |
|||
default(){ |
|||
return {} |
|||
} |
|||
relaysTotal () { |
|||
return this.relays.length //TODO: Figure out WHY? |
|||
}, |
|||
geo: { |
|||
type: Object, |
|||
default(){ |
|||
return {} |
|||
} |
|||
|
|||
relaysConnected () { |
|||
return Object.entries(this.result).length |
|||
}, |
|||
messages: { |
|||
type: Object, |
|||
default(){ |
|||
return {} |
|||
} |
|||
|
|||
relaysComplete () { |
|||
return this.relays.filter(relay => this.results?.[relay]?.state == 'complete').length |
|||
}, |
|||
alerts: { |
|||
type: Object, |
|||
default(){ |
|||
return {} |
|||
} |
|||
|
|||
sha1 (message) { |
|||
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex') |
|||
return hash |
|||
}, |
|||
connections: { |
|||
type: Object, |
|||
default(){ |
|||
return {} |
|||
} |
|||
|
|||
isDone(){ |
|||
return this.relaysTotal()-this.relaysComplete() <= 0 |
|||
}, |
|||
showColumns: { |
|||
type: Object, |
|||
default() { |
|||
return { |
|||
connectionStatuses: false, |
|||
nips: false, |
|||
geo: false, |
|||
additionalInfo: false |
|||
} |
|||
} |
|||
|
|||
loadingComplete(){ |
|||
return this.isDone() ? 'loaded' : '' |
|||
}, |
|||
grouping: { |
|||
} |
|||
|
|||
export default defineComponent({ |
|||
name: 'GroupByNone', |
|||
components: { |
|||
RelaySingleComponent, |
|||
TableHeaders, |
|||
}, |
|||
setup(){ |
|||
return { |
|||
store : setupStore() |
|||
} |
|||
}, |
|||
mounted(){ |
|||
this.relays = this.store.relays.getAll |
|||
}, |
|||
props: { |
|||
showJson: { |
|||
type: Boolean, |
|||
default(){ |
|||
return true |
|||
} |
|||
} |
|||
}, |
|||
mounted(){ |
|||
// console.log(this.relays) |
|||
}, |
|||
}, |
|||
data() { |
|||
return {} |
|||
return { |
|||
showModal: false, |
|||
relays: [] |
|||
} |
|||
}, |
|||
|
|||
computed: {}, |
|||
methods: Object.assign(localMethods, RelaysLib) |
|||
}) |
|||
|
|||
</script> |
|||
<style scoped> |
|||
table { |
|||
border-collapse: collapse !important; |
|||
} |
|||
</style> |
|||
|
|||
<style lang='css' scoped> |
|||
table { |
|||
border-collapse: collapse !important; |
|||
} |
|||
.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:800px; |
|||
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 } |
|||
|
|||
tr.even { |
|||
background:#f9f9f9 |
|||
} |
|||
</style> |
@ -0,0 +1,6 @@ |
|||
<template> |
|||
|
|||
</template> |
|||
<script> |
|||
|
|||
</script> |
@ -1,28 +1,28 @@ |
|||
<template> |
|||
<th class="table-column status-indicator"> |
|||
<th class="table-Column status-indicator"> |
|||
|
|||
</th> |
|||
<th class="table-column relay"> |
|||
<th class="table-Column relay"> |
|||
|
|||
</th> |
|||
<th class="table-column verified"> |
|||
<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 class="table-Column location" v-tooltip:top.tooltip="Ping"> |
|||
🌎 |
|||
</th> |
|||
<th class="table-column latency" v-tooltip:top.tooltip="'Relay Latency on Read'"> |
|||
<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 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 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 class="table-Column write" v-tooltip:top.tooltip="'Relay write status'"> |
|||
✏️ |
|||
</th> |
|||
</template> |
@ -1,274 +0,0 @@ |
|||
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 { |
|||
isExpired: function(){ |
|||
return typeof this.lastUpdate === 'undefined' || Date.now() - this.lastUpdate > this.preferences.cacheExpiration |
|||
}, |
|||
|
|||
getCache: function(key){ |
|||
return this.storage.getStorageSync(key) |
|||
}, |
|||
|
|||
removeCache: function(key){ |
|||
return this.storage.removeStorageSync(key) |
|||
}, |
|||
|
|||
sort(aggregate) { |
|||
let unsorted, |
|||
sorted, |
|||
filterFn |
|||
|
|||
filterFn = (relay) => this.grouping ? this.result?.[relay]?.aggregate == aggregate : true |
|||
|
|||
unsorted = this.relays.filter(filterFn); |
|||
|
|||
// if(!this.isDone()) {
|
|||
// return unsorted
|
|||
// }
|
|||
|
|||
if (unsorted.length) { |
|||
sorted = unsorted |
|||
.sort((relay1, relay2) => { |
|||
return this.result?.[relay1]?.latency.final - this.result?.[relay2]?.latency.final |
|||
}) |
|||
.sort((relay1, relay2) => { |
|||
let a = this.result?.[relay1]?.latency.final , |
|||
b = this.result?.[relay2]?.latency.final |
|||
return (b != null) - (a != null) || a - b; |
|||
}) |
|||
.sort((relay1, relay2) => { |
|||
let x = this.result?.[relay2]?.check?.connect, |
|||
y = this.result?.[relay2]?.check?.connect |
|||
|
|||
return (x === y)? 0 : x? -1 : 1; |
|||
}); |
|||
return sorted |
|||
} |
|||
|
|||
return [] |
|||
}, |
|||
|
|||
check: async function(relay){ |
|||
return new Promise( (resolve, reject) => { |
|||
// if(!this.isExpired())
|
|||
// return reject(relay)
|
|||
|
|||
const opts = { |
|||
checkLatency: true, |
|||
getInfo: true, |
|||
getIdentities: true, |
|||
// debug: true,
|
|||
// data: { result: this.result[relay] }
|
|||
} |
|||
|
|||
let socket = new Inspector(relay, opts) |
|||
|
|||
socket |
|||
.on('complete', (instance) => { |
|||
this.result[relay] = instance.result |
|||
|
|||
this.result[relay].aggregate = this.getAggregate(relay) |
|||
|
|||
this.setCache('relay', relay) |
|||
this.setCache('messages', relay, instance.inbox) |
|||
this.setCache('lastUpdate') |
|||
|
|||
instance.relay.close() |
|||
|
|||
resolve(this.result[relay]) |
|||
}) |
|||
.on('notice', (notice) => { |
|||
const hash = this.sha1(notice) |
|||
let message_obj = RELAY_MESSAGES[hash] |
|||
|
|||
if(!message_obj || !Object.prototype.hasOwnProperty.call(message_obj, 'code')) |
|||
return |
|||
|
|||
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() |
|||
|
|||
|
|||
}) |
|||
}, |
|||
|
|||
setCache: function(type, key, data){ |
|||
|
|||
const now = Date.now() |
|||
|
|||
let store, success, error, instance |
|||
|
|||
switch(type){ |
|||
case 'relay': |
|||
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': |
|||
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': |
|||
store = { |
|||
key: "lastUpdate", |
|||
data: now |
|||
} |
|||
success = () => { |
|||
this.lastUpdate = now |
|||
} |
|||
break; |
|||
case '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) |
|||
}) |
|||
}, |
|||
|
|||
getAggregate: function(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) { |
|||
return 'public' |
|||
} |
|||
else if (aggregateTally == 0) { |
|||
return 'offline' |
|||
} |
|||
else { |
|||
return 'restricted' |
|||
} |
|||
}, |
|||
|
|||
relaysTotal: function() { |
|||
return this.relays.length |
|||
}, |
|||
|
|||
relaysConnected: function() { |
|||
return Object.entries(this.result).length |
|||
}, |
|||
|
|||
relaysComplete: function() { |
|||
return this.relays?.filter(relay => this.result?.[relay]?.state == 'complete').length |
|||
}, |
|||
|
|||
sha1: function(message) { |
|||
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex') |
|||
return hash |
|||
}, |
|||
|
|||
isDone: function(){ |
|||
return this.relaysTotal()-this.relaysComplete() <= 0 |
|||
}, |
|||
|
|||
loadingComplete: function(){ |
|||
return this.isDone() ? 'loaded' : '' |
|||
}, |
|||
|
|||
timeSince: function(date) { |
|||
let seconds = Math.floor((new Date() - date) / 1000); |
|||
let 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"; |
|||
}, |
|||
|
|||
delay(ms) { |
|||
return new Promise(resolve => setTimeout(resolve, ms)); |
|||
}, |
|||
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; |
|||
} |
|||
|
|||
// nulls sort after anything else
|
|||
if (self.result?.[a]?.latency.final === null) { |
|||
return 1; |
|||
} |
|||
if (self.result?.[b]?.latency.final === null) { |
|||
return -1; |
|||
} |
|||
|
|||
// otherwise, if we're ascending, lowest sorts first
|
|||
if (ascending) { |
|||
return self.result?.[a]?.latency.final - self.result?.[b]?.latency.final; |
|||
} |
|||
|
|||
// if descending, highest sorts first
|
|||
return self.result?.[b]?.latency.final-self.result?.[a]?.latency.final; |
|||
}; |
|||
}, |
|||
sortByLatency () { |
|||
let unsorted |
|||
|
|||
unsorted = this.relays; |
|||
|
|||
if (unsorted.length) |
|||
return unsorted.sort(this.sort_by_latency(true)) |
|||
|
|||
return [] |
|||
}, |
|||
} |
@ -1,24 +1,25 @@ |
|||
import { createApp } from 'vue' |
|||
import App from './App.vue' |
|||
import Vue3Storage from "vue3-storage"; |
|||
// import Vue3Storage from "vue3-storage";
|
|||
|
|||
import router from './router' |
|||
import "./styles/main.scss" |
|||
import directives from "./directives/" |
|||
import titleMixin from './mixins/titleMixin' |
|||
// import titleMixin from './mixins/titleMixin'
|
|||
import {Tabs, Tab} from 'vue3-tabs-component'; |
|||
import { createPinia } from 'pinia' |
|||
|
|||
const pinia = createPinia() |
|||
import { plugin as storePlugin } from './store' |
|||
import { createMetaManager } from 'vue-meta' |
|||
|
|||
const app = createApp(App) |
|||
.use(router) |
|||
.use(pinia) |
|||
.use(Vue3Storage, { namespace: "nostrwatch_" }) |
|||
.use(storePlugin) |
|||
.use(createMetaManager()) |
|||
// .use(Vue3Storage, { namespace: "nostrwatch_" })
|
|||
.component('tabs', Tabs) |
|||
.component('tab', Tab) |
|||
.mixin(titleMixin) |
|||
|
|||
directives(app); |
|||
|
|||
await router.isReady() |
|||
|
|||
app.mount('#app') |
@ -0,0 +1,305 @@ |
|||
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: async function(force, single){ |
|||
|
|||
if(!this.isExpired() && !force) |
|||
return |
|||
|
|||
this.store.relays.updateNow() |
|||
|
|||
// console.log('invalidate', 'total relays', this.relays.length)
|
|||
|
|||
if(single) { |
|||
await this.check(single) |
|||
// this.relays[single] = this.getCache(single)
|
|||
// this.messages[single] = this.getCache(`${single}_inbox`)
|
|||
} |
|||
else { |
|||
// console.log('total relays', this.relays.length)
|
|||
// console.log(this.relays.length)
|
|||
for(let index = 0; index < this.relays.length; index++) { |
|||
let relay = this.relays[index] |
|||
// console.log('invalidating', relay)
|
|||
await this.delay(20).then( () => { |
|||
this.check(relay) |
|||
.then(() => { |
|||
// this.store.relays.setResult(relay)
|
|||
// this.store.relays.results[relay] = this.getCache(relay)
|
|||
// this.messages[relay] = this.getCache(`${relay}_inbox`)
|
|||
}).catch( err => console.log(err)) |
|||
}).catch(err => console.log(err)) |
|||
} |
|||
} |
|||
}, |
|||
isExpired: function(){ |
|||
return !this.store.relays.lastUpdate |
|||
|| Date.now() - this.store.relays.lastUpdate > this.store.prefs.duration |
|||
}, |
|||
|
|||
// getCache: function(key){
|
|||
// return this.storage.getStorageSync(key)
|
|||
// },
|
|||
|
|||
// removeCache: function(key){
|
|||
// return this.storage.removeStorageSync(key)
|
|||
// },
|
|||
|
|||
sort(relays) { |
|||
let unsorted, |
|||
sorted |
|||
|
|||
if(!relays && !this.relays) |
|||
return [] |
|||
|
|||
unsorted = relays || this.relays.map(x=>x) |
|||
|
|||
if (unsorted.length) { |
|||
sorted = unsorted |
|||
.sort((relay1, relay2) => { |
|||
return this.store.relays.results?.[relay1]?.latency.final - this.store.relays.results?.[relay2]?.latency.final |
|||
}) |
|||
.sort((relay1, relay2) => { |
|||
let a = this.store.relays.results?.[relay1]?.latency.final , |
|||
b = this.store.relays.results?.[relay2]?.latency.final |
|||
return (b != null) - (a != null) || a - b; |
|||
}) |
|||
.sort((relay1, relay2) => { |
|||
let x = this.store.relays.results?.[relay1]?.check?.connect, |
|||
y = this.store.relays.results?.[relay2]?.check?.connect |
|||
return (x === y)? 0 : x? -1 : 1; |
|||
}) |
|||
// .sort((relay1, relay2) => {
|
|||
// let x = this.store.relays.results?.[relay1]?.check?.read,
|
|||
// y = this.store.relays.results?.[relay2]?.check?.read
|
|||
// return (x === y)? 0 : x? -1 : 1;
|
|||
// })
|
|||
// .sort((relay1, relay2) => {
|
|||
// let x = this.store.relays.results?.[relay1]?.check?.write,
|
|||
// y = this.store.relays.results?.[relay2]?.check?.write
|
|||
// return (x === y)? 0 : x? -1 : 1;
|
|||
// });
|
|||
return sorted |
|||
} |
|||
|
|||
return [] |
|||
}, |
|||
|
|||
cleanUrl: function(relay){ |
|||
return relay.replace('wss://', '') |
|||
}, |
|||
|
|||
check: async function(relay){ |
|||
return new Promise( (resolve, reject) => { |
|||
// if(!this.isExpired())
|
|||
// return reject(relay)
|
|||
|
|||
const opts = { |
|||
checkLatency: true, |
|||
getInfo: true, |
|||
getIdentities: true, |
|||
debug: true, |
|||
// data: { result: this.store.relays.results[relay] }
|
|||
} |
|||
|
|||
let socket = new Inspector(relay, opts) |
|||
|
|||
socket |
|||
.on('complete', (instance) => { |
|||
instance.result.aggregate = this.getAggregate(instance.result) |
|||
this.store.relays.setResult(instance.result) |
|||
// this.setCache('relay', relay)
|
|||
// this.setCache('messages', relay, instance.inbox)
|
|||
this.store.relays.updateNow() |
|||
instance.relay.close() |
|||
|
|||
resolve(instance.result) |
|||
}) |
|||
.on('notice', (notice) => { |
|||
const hash = this.sha1(notice) |
|||
let message_obj = RELAY_MESSAGES[hash] |
|||
|
|||
if(!message_obj || !Object.prototype.hasOwnProperty.call(message_obj, 'code')) |
|||
return |
|||
|
|||
let code_obj = RELAY_CODES[message_obj.code] |
|||
|
|||
let response_obj = {...message_obj, ...code_obj} |
|||
|
|||
this.store.relays.results[relay].observations.push( new InspectorObservation('notice', response_obj.code, response_obj.description, response_obj.relates_to) ) |
|||
}) |
|||
.on('close', () => {}) |
|||
.on('error', () => { |
|||
reject(this.store.relays.results[relay]) |
|||
}) |
|||
.run() |
|||
|
|||
|
|||
}) |
|||
}, |
|||
|
|||
// setCache: function(type, key, data){
|
|||
|
|||
// const now = Date.now()
|
|||
|
|||
// let store, success, error, instance
|
|||
|
|||
// switch(type){
|
|||
// case 'relay':
|
|||
// if(data)
|
|||
// data.aggregate = this.getAggregate(key)
|
|||
// store = {
|
|||
// key: key,
|
|||
// data: data || this.store.relays.results[key],
|
|||
// // expire: Date.now()+1000*60*60*24*180,
|
|||
// }
|
|||
// success = () => {
|
|||
// if(data)
|
|||
// this.store.relays.results[key] = data
|
|||
// }
|
|||
// break;
|
|||
// case 'messages':
|
|||
// 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':
|
|||
// store = {
|
|||
// key: "lastUpdate",
|
|||
// data: now
|
|||
// }
|
|||
// success = () => {
|
|||
// this.lastUpdate = now
|
|||
// }
|
|||
// break;
|
|||
// case '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)
|
|||
// })
|
|||
// },
|
|||
|
|||
getAggregate: function(result) { |
|||
let aggregateTally = 0 |
|||
aggregateTally += result?.check.connect ? 1 : 0 |
|||
aggregateTally += result?.check.read ? 1 : 0 |
|||
aggregateTally += result?.check.write ? 1 : 0 |
|||
|
|||
console.log(result.uri, result?.check.connect, result?.check.read, result?.check.write, aggregateTally) |
|||
|
|||
if (aggregateTally == 3) { |
|||
return 'public' |
|||
} |
|||
else if (aggregateTally == 0) { |
|||
return 'offline' |
|||
} |
|||
else { |
|||
return 'restricted' |
|||
} |
|||
}, |
|||
|
|||
relaysTotal: function() { |
|||
return this.relays.length |
|||
}, |
|||
|
|||
relaysConnected: function() { |
|||
return Object.entries(this.store.relays.results).length |
|||
}, |
|||
|
|||
relaysComplete: function() { |
|||
return this.relays?.filter(relay => this.store.relays.results?.[relay]?.state == 'complete').length |
|||
}, |
|||
|
|||
sha1: function(message) { |
|||
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex') |
|||
return hash |
|||
}, |
|||
|
|||
isDone: function(){ |
|||
return this.relaysTotal()-this.relaysComplete() <= 0 |
|||
}, |
|||
|
|||
loadingComplete: function(){ |
|||
return this.isDone() ? 'loaded' : '' |
|||
}, |
|||
|
|||
timeSince: function(date) { |
|||
let seconds = Math.floor((new Date() - date) / 1000); |
|||
let 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"; |
|||
}, |
|||
|
|||
delay(ms) { |
|||
return new Promise(resolve => setTimeout(resolve, ms)); |
|||
}, |
|||
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; |
|||
} |
|||
|
|||
// nulls sort after anything else
|
|||
if (self.result?.[a]?.latency.final === null) { |
|||
return 1; |
|||
} |
|||
if (self.result?.[b]?.latency.final === null) { |
|||
return -1; |
|||
} |
|||
|
|||
// otherwise, if we're ascending, lowest sorts first
|
|||
if (ascending) { |
|||
return self.result?.[a]?.latency.final - self.result?.[b]?.latency.final; |
|||
} |
|||
|
|||
// if descending, highest sorts first
|
|||
return self.result?.[b]?.latency.final-self.result?.[a]?.latency.final; |
|||
}; |
|||
}, |
|||
} |
@ -0,0 +1,26 @@ |
|||
import { createPinia } from 'pinia' |
|||
import { createPersistedStatePlugin } from 'pinia-plugin-persistedstate-2' |
|||
|
|||
import { useRelaysStore } from './relays.js' |
|||
import { usePrefsStore } from './prefs.js' |
|||
|
|||
export const plugin = (app) => { |
|||
const pinia = createPinia() |
|||
|
|||
const installPersistedStatePlugin = createPersistedStatePlugin() |
|||
pinia.use((context) => installPersistedStatePlugin(context)) |
|||
|
|||
app.use(pinia) |
|||
} |
|||
|
|||
export const setupStore = function(){ |
|||
return { |
|||
relays: useRelaysStore(), |
|||
prefs: usePrefsStore() |
|||
} |
|||
} |
|||
|
|||
export const store = { |
|||
useRelaysStore, |
|||
usePrefsStore |
|||
} |
@ -0,0 +1,18 @@ |
|||
import { defineStore } from 'pinia' |
|||
|
|||
export const usePrefsStore = defineStore('prefs', { |
|||
state: () => ({ |
|||
refresh: true, |
|||
duration: 30*60*1000, |
|||
}), |
|||
getters: { |
|||
doRefresh: (state) => state.refresh, |
|||
expireAfter: (state) => state.duration, |
|||
}, |
|||
actions: { |
|||
enable(){ this.refresh = true }, |
|||
disable(){ this.refresh = false }, |
|||
toggleRefresh(){ this.refresh = !this.refresh }, |
|||
updateExpiration(dur) { this.duration = dur }, |
|||
}, |
|||
}) |
@ -0,0 +1,39 @@ |
|||
import { defineStore } from 'pinia' |
|||
|
|||
export const useRelaysStore = defineStore('relays', { |
|||
state: () => ({ |
|||
urls: new Array(), |
|||
results: new Object(), |
|||
geo: new Object(), |
|||
lastUpdate: null, |
|||
groups: {} |
|||
}), |
|||
getters: { |
|||
getAll: (state) => state.urls.map((x)=>x), //clone it
|
|||
getByAggregate: (state) => (aggregate) => { |
|||
return state.urls |
|||
.filter( (relay) => state.results?.[relay]?.aggregate == aggregate) |
|||
.map((x)=>x) //clone it
|
|||
}, |
|||
|
|||
getResults: (state) => state.results, |
|||
getResult: (state) => (relayUrl) => state.results?.[relayUrl], |
|||
|
|||
getGeo: (state) => (relayUrl) => state.geo[relayUrl], |
|||
|
|||
getLastUpdate: (state) => state.lastUpdate, |
|||
}, |
|||
actions: { |
|||
addRelay(relayUrl){ this.urls.push(relayUrl) }, |
|||
addRelays(relayUrls){ this.urls = Array.from(new Set(this.urls.concat(this.urls, relayUrls))) }, |
|||
setRelays(relayUrls){ this.urls = relayUrls }, |
|||
|
|||
setResult(result){ this.results[result.uri] = result }, |
|||
setResults(results){ this.results = results }, |
|||
clearResults(){ this.results = {} }, |
|||
|
|||
setGeo(geo){ this.geo = geo }, |
|||
|
|||
updateNow(){ this.lastUpdate = Date.now() }, |
|||
}, |
|||
}) |
@ -1,27 +0,0 @@ |
|||
import { defineStore } from 'pinia' |
|||
import { useResultsStore } from './results.js' |
|||
import { userGeoStore } from './geo.js' |
|||
|
|||
export const useRelayStore = defineStore('counter', { |
|||
state: () => ({ |
|||
relays: [] |
|||
}), |
|||
getters: { |
|||
getRelayResult: (state) => { |
|||
return (relay_url) => state.results[relay_url] |
|||
}, |
|||
getRelaysByAvailability: (state) => { |
|||
return (type) => { |
|||
for(const result in results) { |
|||
|
|||
} |
|||
} |
|||
}, |
|||
getRelayByAvailability |
|||
}, |
|||
actions: { |
|||
increment() { |
|||
this.count++ |
|||
}, |
|||
}, |
|||
}) |
@ -0,0 +1,11 @@ |
|||
module.exports = { |
|||
content: ['./dist/*.html'], |
|||
theme: { |
|||
extend: {}, |
|||
}, |
|||
variants: { |
|||
extend: {}, |
|||
}, |
|||
plugins: [], |
|||
} |
|||
|
Loading…
Reference in new issue