Browse Source

pinia install + debug

develop
dskvr 2 years ago
parent
commit
551a1ba1af
  1. 5
      babel.config.js
  2. 1496
      cache/continents.json
  3. 21
      cache/geo.yaml
  4. 28
      package.json
  5. 7
      postcss.config.js
  6. 8
      relays.yaml
  7. 30
      scripts/geo.js
  8. 1
      scripts/relays.js
  9. 9
      src/App.vue
  10. 6
      src/components/AddRelay.vue
  11. 2
      src/components/AuthComponent.vue
  12. 257
      src/components/GroupByAvailability.vue
  13. 214
      src/components/GroupByNone.vue
  14. 36
      src/components/HeaderComponent.vue
  15. 30
      src/components/LeafletComponent.vue
  16. 6
      src/components/LoadData.vue
  17. 12
      src/components/NavComponent.vue
  18. 116
      src/components/PreferencesComponent.vue
  19. 101
      src/components/RefreshComponent.vue
  20. 47
      src/components/RelayGroupedListComponent.vue
  21. 115
      src/components/RelayListComponent.vue
  22. 66
      src/components/RelaySingleComponent.vue
  23. 16
      src/components/TableHeaders.vue
  24. 274
      src/lib/relays-lib.js
  25. 17
      src/main.js
  26. 106
      src/pages/HomePage.vue
  27. 73
      src/pages/SingleRelay.vue
  28. 33
      src/shared/events.js
  29. 305
      src/shared/relays-lib.js
  30. 26
      src/store/index.js
  31. 18
      src/store/prefs.js
  32. 39
      src/store/relays.js
  33. 0
      src/store/user.js
  34. 27
      src/stores/relays.js
  35. 0
      src/stores/results.js
  36. 0
      src/stores/userRelays.js
  37. 3
      src/styles/main.scss
  38. 11
      tailwind.config.js
  39. 3
      vue.config.js

5
babel.config.js

@ -1,5 +1,6 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
'@vue/cli-plugin-babel/preset',
],
plugins: ["@babel/plugin-syntax-top-level-await"]
}

1496
cache/continents.json

File diff suppressed because it is too large

21
cache/geo.yaml

@ -3339,3 +3339,24 @@ geo:
type: 1
TTL: 358
data: 164.90.218.45
wss://no.contry.xyz:
status: success
country: Japan
countryCode: JP
region: "13"
regionName: Tokyo
city: Shinagawa
zip: 105-8711
lat: 35.6096
lon: 139.7283
timezone: Asia/Tokyo
isp: Choopa
org: Vultr Holdings, LLC
as: AS20473 The Constant Company, LLC
query: 198.13.32.125
continent: {}
dns:
name: no.contry.xyz
type: 1
TTL: 300
data: 198.13.32.125

28
package.json

@ -19,50 +19,62 @@
"dependencies": {
"@2alheure/vue-safe-mail": "1.0.3",
"@vue-leaflet/vue-leaflet": "0.6.1",
"@vueuse/core": "9.9.0",
"alby": "1.0.1",
"core-js": "^3.8.3",
"country-code-emoji": "2.3.0",
"cross-fetch": "3.1.5",
"js-yaml": "4.1.0",
"json-loader": "^0.5.7",
"json-server": "0.17.1",
"leaflet": "1.9.3",
"node-emoji": "1.11.0",
"node-fetch": "3.3.0",
"node-polyfill-webpack-plugin": "2.0.1",
"nostr": "0.2.5",
"nostr-relay-inspector": "0.0.16",
"nostr-tools": "1.0.1",
"onion-regex": "2.0.8",
"pinia": "2.0.28",
"pinia-plugin-persistedstate-2": "2.0.8",
"requests": "0.3.0",
"sass": "1.56.1",
"sass-loader": "13.2.0",
"socks-proxy-agent": "7.0.0",
"stream-browserify": "3.0.0",
"vue": "^3.2.45",
"vue-final-modal": "3",
"vue-grid-responsive": "1.3.0",
"vue-meta": "3.0.0-alpha.8",
"vue-nav-tabs": "0.5.7",
"vue-router": "4.1.6",
"vue-simple-maps": "1.1.3",
"vue3-storage": "0.1.11",
"vue3-tabs-component": "1.1.2",
"write-yaml-file": "4.2.0",
"yaml-js": "0.3.1",
"yaml-loader": "^0.6.0",
"yaml2json": "1.0.2"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@babel/plugin-syntax-top-level-await": "7.14.5",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"autoprefixer": "10.4.13",
"css-minimizer-webpack-plugin": "4.2.2",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"html-webpack-plugin": "5.5.0",
"json-loader": "^0.5.7",
"json-server": "0.17.1",
"mini-css-extract-plugin": "2.7.2",
"node-polyfill-webpack-plugin": "2.0.1",
"postcss": "8.4.20",
"postcss-preset-env": "7.8.3",
"sass": "1.56.1",
"sass-loader": "13.2.0",
"style-loader": "3.3.1",
"tailwindcss": "3.2.4",
"vue-cli-plugin-yaml-loader": "~1.0.0",
"webpack-cli": "5.0.0"
"webpack-cli": "5.0.0",
"yaml-js": "0.3.1",
"yaml-loader": "^0.6.0"
},
"eslintConfig": {
"root": true,

7
postcss.config.js

@ -0,0 +1,7 @@
const tailwindcss = require('tailwindcss');
module.exports = {
plugins: [
'postcss-preset-env',
tailwindcss
],
};

8
relays.yaml

@ -159,9 +159,9 @@ relays:
- wss://btc.klendazu.com
- wss://nostr.hackerman.pro
- wss://relay.realsearch.cc
- wss://nostr3.actn.io
- wss://nostr.pwnshop.cloud
- wss://nostr.mrbits.it
- wss://nostr.coollamer.com
- wss://node01.nostress.cc
- wss://nostr.zenon.wtf
- wss://echo.obsolete.org
- wss://brb.io
- wss://nostr.massmux.com
- wss://no.contry.xyz

30
scripts/geo.js

@ -5,8 +5,9 @@ const fetch = require('cross-fetch'),
let object,
yaml,
result,
file = fs.readFileSync('./relays.yaml', 'utf8'),
geoCache = fs.readFileSync('./cache/geo.yaml', 'utf8')
relayUrls = fs.readFileSync('./relays.yaml', 'utf8'),
geoCache = fs.readFileSync('./cache/geo.yaml', 'utf8'),
continents = fs.readFileSync('./cache/continents.json', 'utf8')
const getDns = async function(relay){
let dns
@ -37,25 +38,40 @@ const getGeo = async function(ip) {
return geo;
}
const getContinent = function(countryCode) {
return JSON.parse(continents)
.filter( c => c.country_code == countryCode )
.map( cont => {
return {
continentCode: cont.continent_code,
continentName: cont.continent_name
}
})[0]
}
const query = async function(){
const relays = YAML.parse(file).relays.reverse(),
const relays = YAML.parse(relayUrls).relays.reverse(),
result = YAML.parse(geoCache).geo || {}
for (const relay of relays) {
await delay(1000).then(async () => {
console.log('getting relay geo', relay)
let dns, ip, geo
dns = await getDns(relay).catch()
ip = await getIp(dns).catch()
// console.log(dns, ip)
geo = await getGeo(ip).catch()
// console.log(geo, ip, dns)
if(geo)
geo = Object.assign(geo, getContinent(geo.countryCode))
if(geo && dns)
if(geo && dns){
geo.dns = dns[dns.length-1]
delete geo.status
}
if(geo && geo.status == 'success')
result[relay] = geo

1
scripts/relays.js

@ -79,7 +79,6 @@ async function discover(){
pool.close()
resolve(true)
}, 10*1000 )
})
}

9
src/App.vue

@ -4,10 +4,17 @@
</template>
<script>
import { useMeta } from 'vue-meta'
export default {
name: 'App',
components: {}
components: {},
setup () {
useMeta({
title: 'nostr.watch registry & monitor',
htmlAttrs: { lang: 'en', amp: true }
})
}
}
</script>

6
src/components/AddRelay.vue

@ -0,0 +1,6 @@
<template>
</template>
<script>
</script>

2
src/components/AuthComponent.vue

@ -1,5 +1,5 @@
<template>
<button v-if="signer" @click="auth">Use Signer</button>
<button v-if="signer" @click="auth">Sign</button>
</template>
<script>

257
src/components/GroupByAvailability.vue

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

214
src/components/GroupByNone.vue

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

36
src/components/HeaderComponent.vue

@ -1,44 +1,30 @@
<template>
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<Row container :gutter="12">
<Column :xs="12" :md="12" :lg="12" class="title-card">
<h1>nostr.watch<sup>{{version}}</sup></h1>
</column>
</row>
</Column>
</Row>
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<NavComponent :relays="relays" />
</column>
</row>
<Row container :gutter="12">
<Column :xs="12" :md="12" :lg="12" class="title-card">
<NavComponent />
</Column>
</Row>
</template>
<script>
import { version } from '../../package.json'
import { defineComponent} from 'vue'
import NavComponent from './NavComponent.vue'
export default defineComponent({
title: "nostr.watch registry & network status",
name: 'GroupByNone',
components: {
NavComponent,
},
props: {
relays: {
type: Array,
default(){
return []
}
},
result: {
type: Object,
default(){
return {}
}
},
},
props: {},
data(){
return {
version: version

30
src/components/LeafletComponent.vue

@ -34,7 +34,6 @@
>
<!-- <l-popup>
{{ relay }}
meopw
</l-popup> -->
</l-circle-marker>
</l-map>
@ -49,6 +48,7 @@
<script>
import "leaflet/dist/leaflet.css"
import { LMap, LTileLayer, LCircleMarker } from "@vue-leaflet/vue-leaflet"
import { setupStore } from '@/store'
export default {
components: {
@ -63,6 +63,15 @@ export default {
expanded: false,
};
},
setup(){
return {
store : setupStore()
}
},
mounted() {
this.result = this.store.relays.getResults
this.geo = this.store.relays.geo
},
methods: {
mapHeight(){
return this.expanded ? "500px" : "250px"
@ -97,23 +106,8 @@ export default {
this.$refs.map.leafletObject.invalidateSize()
}
},
async mounted() {
},
props: {
geo: {
type: Object,
default(){
return {}
}
},
result: {
type: Object,
default(){
return {}
}
},
},
props: {},
};

6
src/components/LoadData.vue

@ -0,0 +1,6 @@
<template>
</template>
<script>
</script>

12
src/components/NavComponent.vue

@ -5,7 +5,7 @@
<a href="https://github.com/dskvr/nostr-watch/edit/main/relays.yaml" target="_blank">Add Relay</a>
<a href="/relays.json"><code>{...}</code></a>
<span>
<PreferencesComponent :relays="relays" />
<PreferencesComponent />
</span>
<span>
<AuthComponent />
@ -46,6 +46,7 @@ nav.menu a:hover {
import { defineComponent } from 'vue'
import PreferencesComponent from '../components/PreferencesComponent.vue'
import AuthComponent from '../components/AuthComponent.vue'
export default defineComponent({
title: "nostr.watch registry & network status",
name: 'NavComponent',
@ -53,13 +54,6 @@ export default defineComponent({
PreferencesComponent,
AuthComponent
},
props: {
relays: {
type: Array,
default(){
return []
}
}
}
props: {}
});
</script>

116
src/components/PreferencesComponent.vue

@ -2,24 +2,24 @@
<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">
<span><input type="checkbox" id="checkbox" v-model="store.prefs.refresh" /><label for="">Refresh Automatically</label></span>
<span v-if="store.prefs.refresh">
Refresh Every
<ul>
<li>
<input type="radio" id="1w" :value="1000*60*60*24*7" v-model="preferences.cacheExpiration" />
<input type="radio" id="1w" :value="1000*60*60*24*7" v-model="store.prefs.duration" />
<label for="1w">1 Week</label>
</li>
<li>
<input type="radio" id="1d" :value="1000*60*60*24" v-model="preferences.cacheExpiration" />
<input type="radio" id="1d" :value="1000*60*60*24" v-model="store.prefs.duration" />
<label for="1d">1 day</label>
</li>
<li>
<input type="radio" id="30m" :value="1000*60*30" v-model="preferences.cacheExpiration" />
<input type="radio" id="30m" :value="1000*60*30" v-model="store.prefs.duration" />
<label for="30m">30 minutes</label>
</li>
<li>
<input type="radio" id="10m" :value="1000*60*10" v-model="preferences.cacheExpiration" />
<input type="radio" id="10m" :value="1000*60*10" v-model="store.prefs.duration" />
<label for="10m">10 minutes</label>
</li>
</ul>
@ -30,6 +30,55 @@
</section>
</template>
<script>
import { defineComponent } from 'vue'
import RelaysLib from '../shared/relays-lib.js'
import { store } from '@/store'
const localMethods = {
toggle() {
this.isActive = !this.isActive;
},
clearData(){
this.store.relays.clearResults()
}
}
export default defineComponent({
name: 'PreferencesComponent',
components: {},
setup(){
return {
store : {
relays: store.useRelaysStore(),
prefs: store.usePrefsStore()
}
}
},
mounted(){
// this.preferences = this
},
updated(){
// this.setCache('preferences')
},
computed: {},
methods: Object.assign(localMethods, RelaysLib),
props: {},
data() {
return {
storage: null,
refresh: true,
preferences: {
refresh: true,
cacheExpiration: 30*60*1000
},
isActive: false,
}
},
})
</script>
<style scoped>
section#preferences {
display:none;
@ -71,58 +120,3 @@ button {
border: none;
}
</style>
<script>
import { defineComponent } from 'vue'
import { useStorage } from "vue3-storage";
import RelaysLib from '../lib/relays-lib.js'
const localMethods = {
toggle() {
this.isActive = !this.isActive;
},
clearData(){
this.relays.forEach( relay => {
// console.log('clearing', relay)
this.removeCache(`${relay}`)
this.removeCache(`${relay}_inbox`)
})
}
}
export default defineComponent({
name: 'PreferencesComponent',
components: {},
mounted(){
this.storage = useStorage()
this.preferences = this.getCache('preferences') || this.preferences
},
updated(){
this.setCache('preferences')
},
computed: {},
methods: Object.assign(localMethods, RelaysLib),
props: {
relays: {
type: Array,
default(){
return []
}
}
},
data() {
return {
storage: null,
refresh: true,
preferences: {
refresh: true,
cacheExpiration: 30*60*1000
},
isActive: false,
}
},
})
</script>

101
src/components/RefreshComponent.vue

@ -1,11 +1,11 @@
<template>
<section id="refresh">
<span>
Updated {{ refreshData?.sinceLast }} ago
Updated {{ sinceLast }} ago
<button :disabled='disabled' @click="refreshNow()">Refresh{{ relay ? ` ${relay}` : "" }} Now</button>
</span>
<span v-if="preferences.refresh">
Next refresh in: {{ refreshData?.untilNext }}
<span v-if="store.prefs.refresh">
Next refresh in: {{ untilNext }}
</span>
</section>
</template>
@ -15,20 +15,21 @@
</style>
<script>
import { defineComponent, reactive } from 'vue'
import RelaysLib from '../lib/relays-lib.js'
import { useStorage } from "vue3-storage";
import { defineComponent } from 'vue'
import RelaysLib from '../shared/relays-lib.js'
// import { useStorage } from "vue3-storage";
import { store } from '../store'
const localMethods = {
timeUntilRefresh(){
return this.timeSince(Date.now()-(this.lastUpdate+this.preferences.cacheExpiration-Date.now()))
return this.timeSince(Date.now()-(this.store.relays.lastUpdate+this.store.prefs.duration-Date.now()))
},
timeSinceRefresh(){
return this.timeSince(this.lastUpdate)
return this.timeSince(this.store.relays.lastUpdate)
},
disableManualRefresh: function(){
//this is a hack.
const lastUpdate = this.getCache('lastUpdate')
const lastUpdate = this.store.relays.lastUpdate
if(Math.floor( ( Date.now()-lastUpdate )/1000 ) < 20)
this.disabled = true
else
@ -37,16 +38,15 @@ const localMethods = {
setRefreshInterval: function(){
clearInterval(this.interval)
this.interval = setInterval(() => {
this.preferences = this.getCache('preferences') || this.preferences
this.prefs = this.store.prefs.get
this.refreshData.untilNext = this.timeUntilRefresh()
this.refreshData.sinceLast = this.timeSinceRefresh()
this.untilNext = this.timeUntilRefresh()
this.sinceLast = this.timeSinceRefresh()
if(this.isExpired() && this.preferences.refresh)
this.invalidate(false, this.relay)
if(this.store.prefs.refresh )
this.invalidate()
this.disableManualRefresh()
}, 1000)
},
refreshNow(){
@ -58,68 +58,43 @@ const localMethods = {
export default defineComponent({
name: 'RefreshComponent',
components: {},
setup(){
return {
store : {
relays: store.useRelaysStore(),
prefs: store.usePrefsStore()
}
}
},
mounted(){
clearInterval(this.interval)
this.relays = this.store.relays.getAll
this.lastUpdate = this.store.relays.lastUpdate
console.log('last update', this.lastUpdate)
this.storage = useStorage()
this.lastUpdate = this.getCache('lastUpdate')|| this.lastUpdate
this.preferences = this.getCache('preferences') || this.preferences
clearInterval(this.interval)
this.refreshData = reactive({
untilNext: this.timeUntilRefresh(),
sinceLast: this.timeSinceRefresh()
})
this.untilNext = this.timeUntilRefresh()
this.sinceLast = this.timeSinceRefresh()
this.setRefreshInterval()
},
updated(){
this.setCache('preferences')
this.refreshData.untilNext = this.timeUntilRefresh()
this.refreshData.sinceLast = this.timeSinceRefresh()
this.untilNext = this.timeUntilRefresh()
this.sinceLast = this.timeSinceRefresh()
},
computed: {},
methods: Object.assign(localMethods, RelaysLib),
props: {
relay: {
type: String,
default(){
return ""
}
},
relaysProp:{
type: Array,
default(){
return []
}
},
messagesProp:{
type: Object,
default(){
return {}
}
},
resultProp: {
type: Object,
default(){
return {}
}
},
},
props: {},
data() {
return {
relays: this.relaysProp,
result: this.resultProp,
messages: this.messagesProp,
storage: null,
relay: "",
relays: [],
refresh: {},
untilNext: null,
lastUpdate: null,
refresh: true,
refreshData: this.refreshDataProp,
sinceLast: null,
interval: null,
preferences: {
refresh: true,
cacheExpiration: 30*60*1000
},
disabled: true
}
},

47
src/components/RelayGroupedListComponent.vue

@ -15,13 +15,7 @@
<TableHeaders />
</tr>
<tr v-for="(relay, index) in sort(section)" :key="{relay}" :class="getResultClass(relay, index)" class="relay">
<RelaySingleComponent
:relay="relay"
:result="result[relay]"
:geo="geo[relay]"
:showColumns="showColumns"
:connection="connections[relay]"
/>
<RelaySingleComponent />
</tr>
</template>
@ -32,7 +26,7 @@ import { VueFinalModal } from 'vue-final-modal'
import RelaySingleComponent from './RelaySingleComponent.vue'
import TableHeaders from './TableHeaders.vue'
import RelaysLib from '../lib/relays-lib.js'
import RelaysLib from '../shared/relays-lib.js'
const localMethods = {
getHeadingClass(){
@ -45,38 +39,13 @@ const localMethods = {
},
getResultClass (relay, index) {
return {
loaded: this.result?.[relay]?.state == 'complete',
loaded: this.store.relays.results?.[relay]?.state == 'complete',
online: this.section != "offline",
offline: this.section == "offline",
public: this.section == "public",
even: index % 2,
}
},
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;
};
},
queryJson(aggregate){
const relays = this.sort(aggregate)
const result = {}
@ -84,17 +53,17 @@ const localMethods = {
return JSON.stringify(result,null,'\t')
},
relaysTotal () {
return this.relays.length
return this.store.relays.urls.length
},
relaysConnected () {
return Object.keys(this.result).length
return Object.keys(this.store.relays.results).length
},
relaysCompleted () {
let value = Object.entries(this.result).map((value) => { return value.state == 'complete' }).length
let value = Object.entries(this.store.relays.results).map((value) => { return value.state == 'complete' }).length
return value
},
isDone(){
return this.relaysTotal()-this.relaysCompleted() == 0
return relaysTotal()-relaysCompleted() == 0
},
}
@ -204,7 +173,7 @@ export default defineComponent({
text-align:left;
position: relative;
display: flex;
flex-direction: column;
flex-direction: Column;
max-height: 500px;
max-width:800px;
margin: 0 1rem;

115
src/components/RelayListComponent.vue

@ -9,18 +9,16 @@
</div>
</vue-final-modal>
<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>
<h2><span class="indicator badge">{{ this.store.relays.urls.length }}</span>Relays <a @click="showModal=true" class="section-json" v-if="showJson">{...}</a></h2>
</td>
</tr>
<tr v-if="this.relays.length > 0">
<tr v-if="this.store.relays.urls.length > 0">
<TableHeaders />
</tr>
<tr v-for="(relay, index) in sort(relays[relay]?.aggregate)" :key="{relay}" class="relay" :class="getResultClass(relay, index)">
<RelaySingleComponent
<tr v-for="(relay, index) in sort(store.relay.result[relay]?.aggregate)" :key="{relay}" class="relay" :class="getResultClass(relay, index)">
<RelaySingleComponent
:relay="relay"
:result="result[relay]"
:geo="geo[relay]"
:connection="connections[relay]" />
/>
</tr>
</table>
</template>
@ -32,7 +30,9 @@ import { VueFinalModal } from 'vue-final-modal'
import RelaySingleComponent from './RelaySingleComponent.vue'
import TableHeaders from './TableHeaders.vue'
import RelaysLib from '../lib/relays-lib.js'
import RelaysLib from '../shared/relays-lib.js'
import { store } from '../store'
const localMethods = {
// getHeadingClass(){
@ -44,17 +44,18 @@ const localMethods = {
// }
// },
getResultClass (relay, index) {
console.log('state', this.store.relay.results?.[relay]?.state)
return {
loaded: this.result?.[relay]?.state == 'complete',
loaded: this.store.relay.results?.[relay]?.state == 'complete',
even: index % 2
}
},
queryJson(){
const result = { relays: this.relays }
const result = { relays: this.store.relays.urls }
return JSON.stringify(result,null,'\t')
},
relaysTotal () {
return this.relays.length //TODO: Figure out WHY?
return this.store.relays.urls.length //TODO: Figure out WHY?
},
relaysConnected () {
@ -62,7 +63,7 @@ const localMethods = {
},
relaysComplete () {
return this.relays.filter(relay => this.results?.[relay]?.state == 'complete').length
return this.store.relays.urls.filter(relay => this.results?.[relay]?.state == 'complete').length
},
sha1 (message) {
@ -77,42 +78,6 @@ const localMethods = {
loadingComplete(){
return this.isDone() ? 'loaded' : ''
},
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 []
},
}
export default defineComponent({
@ -122,6 +87,17 @@ export default defineComponent({
VueFinalModal,
TableHeaders
},
setup(){
return {
store : {
relays: store.useRelaysStore(),
prefs: store.usePrefsStore()
}
}
},
mounted(){
console.log('state', this.store.relay.results)
},
props: {
showJson: {
type: Boolean,
@ -129,49 +105,14 @@ export default defineComponent({
return true
}
},
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 {}
}
},
},
data() {
return {
showModal: false
showModal: false,
relays: []
}
},
mounted(){},
computed: {},
methods: Object.assign(localMethods, RelaysLib)
})
@ -198,7 +139,7 @@ export default defineComponent({
::v-deep(.modal-content) {
position: relative;
display: flex;
flex-direction: column;
flex-direction: Column;
max-height: 90%;
max-width:800px;
margin: 0 1rem;

66
src/components/RelaySingleComponent.vue

@ -19,7 +19,7 @@
<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')">
@ -41,71 +41,59 @@
<span v-tooltip:top.tooltip="alert.description" :class="alert.type" v-if="alert.type == 'caution'"></span>
</li>
</ul>
</td> -->
</td>-->
</template>
<script>
import { defineComponent} from 'vue'
import { InspectorResult } from 'nostr-relay-inspector'
// import { InspectorResult } from 'nostr-relay-inspector'
import { countryCodeEmoji } from 'country-code-emoji';
import emoji from 'node-emoji';
import { store } from '../store'
export default defineComponent({
name: 'RelaySingleComponent',
components: {
},
props: {
relay: String,
result: {
type: Object,
relay:{
type: String,
default(){
return structuredClone(InspectorResult)
}
},
geo: {
type: Object,
default(){
return {}
}
},
showColumns: {
type: Object,
default() {
return {
connectionStatuses: false,
nips: false,
geo: false,
additionalInfo: false
}
}
},
connection: {
type: Object,
default() {
return {
connectionStatuses: false,
nips: false,
geo: false,
additionalInfo: false
}
return ""
}
}
},
data() {
return {
showModal: false
result: {},
geo: {},
showModal: false,
}
},
mounted(){
this.result = this.store.relays.results[this.relay]
this.geo = this.store.relays.geo[this.relay]
},
setup(){
return {
store : {
relays: store.useRelaysStore(),
prefs: store.usePrefsStore()
}
}
},
methods: {
getResultClass (url, key) {
let result = this.result?.check?.[key] === true
let cl = this.result?.check?.[key] === true
? 'success'
: this.result?.check?.[key] === false
? 'failure'
: 'pending'
return `indicator ${result}`
return `indicator ${cl}`
},
getLoadingClass () {
console
return this.result?.state == 'complete' ? "relay loaded" : "relay"
},
generateKey (url, key) {
@ -200,7 +188,7 @@ td.verified span {
::v-deep(.modal-content) {
position: relative;
display: flex;
flex-direction: column;
flex-direction: Column;
max-height: 90%;
margin: 0 1rem;
padding: 1rem;

16
src/components/TableHeaders.vue

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

274
src/lib/relays-lib.js

@ -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 []
},
}

17
src/main.js

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

106
src/pages/HomePage.vue

@ -1,92 +1,63 @@
<template>
<!-- <NavComponent /> -->
<LeafletComponent
:geo="geo"
:result="result"
/>
<LeafletComponent />
<div id="wrapper">
<metainfo>
<template v-slot:title="{ content }">{{ content }}</template>
</metainfo>
<HeaderComponent :relays="relays" />
<HeaderComponent />
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="list">
<Row container :gutter="12">
<Column :xs="12" :md="12" :lg="12" class="list">
<span>Group By:</span>
<tabs :options="{ useUrlFragment: false }" @clicked="tabClicked" @changed="tabChanged" nav-item-class="nav-item">
<tab name="Availability">
<GroupByAvailability
section="processing"
:relays="relays"
:result="result"
:geo="geo"
:messages="messages"
:alerts="alerts"
:connections="connections"
:showJson="false">
</GroupByAvailability>
<GroupByAvailability />
</tab>
<tab name="None">
<GroupByNone
:relays="relays"
:result="result"
:geo="geo"
:messages="messages"
:alerts="alerts"
:connections="connections">
</GroupByNone>
<GroupByNone />
</tab>
</tabs>
</column>
</row>
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="list">
</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> -->
</Column>
</Row>
<section id="footer">
<RefreshComponent
:relaysProp="relays"
v-bind:resultProp="result"
:messagesProp="messages"
/>
<div id="footer">
<RefreshComponent />
<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>
</div>
</template>
<script>
import { defineComponent } from 'vue'
import { useStorage } from "vue3-storage";
import { Row, Column } from 'vue-grid-responsive';
import RelaysLib from '../lib/relays-lib.js'
import RelaysLib from '../shared/relays-lib.js'
import HeaderComponent from '../components/HeaderComponent.vue'
import LeafletComponent from '../components/LeafletComponent.vue'
import RefreshComponent from '../components/RefreshComponent.vue'
import HeaderComponent from '../components/HeaderComponent.vue'
import { relays } from '../../relays.yaml'
import { geo } from '../../cache/geo.yaml'
import { setupStore } from '@/store'
import GroupByNone from '../components/GroupByNone.vue'
import GroupByAvailability from '../components/GroupByAvailability.vue'
import { useMeta } from 'vue-meta'
export default defineComponent({
title: "nostr.watch registry & network status",
name: 'HomePage',
components: {
@ -99,9 +70,20 @@ export default defineComponent({
HeaderComponent
},
setup(){
useMeta({
title: 'nostr.watch',
description: 'A robust client-side nostr relay monitor. Find fast nostr relays, view them on a map and monitor the network status of nostr.',
htmlAttrs: { lang: 'en', amp: true }
})
return {
store : setupStore()
}
},
data() {
return {
relays: Array.from( new Set(relays)),
relays: [],
result: {},
messages: {},
connections: {},
@ -111,22 +93,20 @@ export default defineComponent({
count: 0,
storage: null,
geo,
hasStorage: false,
// cacheExpiration: 10*60*1000, //10 minutes
}
},
updated(){},
async mounted() {
this.storage = useStorage()
this.lastUpdate = this.getCache('lastUpdate')|| this.lastUpdate
this.preferences = this.getCache('preferences') || this.preferences
this.store.relays.setRelays(relays)
this.store.relays.setGeo(geo)
this.relays = this.store.relays.getAll
this.lastUpdate = this.store.relays.lastUpdate
this.preferences = this.store.prefs.get
this.relays.forEach(async relay => {
this.result[relay] = this.getCache(relay)
this.messages[relay] = this.getCache(`${relay}_inbox`)
})
this.invalidate()
},
@ -167,6 +147,10 @@ a:hover {
color: #000;
}
.nav-item {
cursor:pointer
}
.nav-item.is-active a {
background:#f0f0f0;
}

73
src/pages/SingleRelay.vue

@ -1,4 +1,8 @@
<template>
<metainfo>
<template v-slot:title="{ content }">{{ `${cleanUrl(this.relay)} | ${content}` }}</template>
</metainfo>
<LeafletSingleComponent
:geo="geo"
:relay="relay"
@ -11,17 +15,17 @@
<div id="relay-wrapper">
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<Row container :gutter="12">
<Column :xs="12" :md="12" :lg="12" class="title-card">
<span v-tooltip:top.tooltip="'Click to copy'" style="display:block">
<h2 @click="copy(relayUrl())">{{ relayUrl() }}</h2>
</span>
</column>
</row>
</Column>
</Row>
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<Row container :gutter="12">
<Column :xs="12" :md="12" :lg="12" class="title-card">
<span class="badges">
<span><img :src="badgeCheck('connect')" /></span>
<span><img :src="badgeCheck('read')" /></span>
@ -33,17 +37,17 @@
<a :href="nipLink(nip)" target="_blank"><img :src="badgeLink(nip)" /></a>
</span>
</span>
</column>
</row>
</Column>
</Row>
<row container :gutter="12" v-if="!result?.check?.connect">
<column :xs="12" :md="12" :lg="12" class="title-card">
<Row container :gutter="12" v-if="!result?.check?.connect">
<Column :xs="12" :md="12" :lg="12" class="title-card">
This relay appears to be offline.
</column>
</row>
</Column>
</Row>
<row container :gutter="12" v-if="result?.check?.connect">
<column :xs="12" :md="12" :lg="12" class="title-card">
<Row container :gutter="12" v-if="result?.check?.connect">
<Column :xs="12" :md="12" :lg="12" class="title-card">
<table v-if="result.info">
<tr>
<th colspan="2"><h4>Info</h4></th>
@ -62,7 +66,6 @@
</tr>
</table>
<table v-if="result.identities">
<tr>
<th colspan="2"><h4>Identities</h4></th>
@ -78,8 +81,8 @@
</tbody>
</table>
</column>
<column :xs="12" :md="6" :lg="6" class="title-card">
</Column>
<Column :xs="12" :md="6" :lg="6" class="title-card">
<table v-if="geo[relay]">
<tr>
<th colspan="2"><h4>GEO {{geo?.countryCode ? getFlag() : ''}}</h4></th>
@ -91,8 +94,8 @@
</tr>
</tbody>
</table>
</column>
<column :xs="12" :md="6" :lg="6" class="title-card">
</Column>
<Column :xs="12" :md="6" :lg="6" class="title-card">
<table v-if="geo[relay]">
<tr>
<th colspan="2"><h4>DNS</h4></th>
@ -104,8 +107,8 @@
</tr>
</tbody>
</table>
</column>
</row>
</Column>
</Row>
<!-- <RefreshComponent
@ -130,12 +133,17 @@ import HeaderComponent from '../components/HeaderComponent.vue'
import { Row, Column } from 'vue-grid-responsive';
import SafeMail from "@2alheure/vue-safe-mail";
import RelaysLib from '../lib/relays-lib.js'
import RelaysLib from '../shared/relays-lib.js'
import { version } from '../../package.json'
import { relays } from '../../relays.yaml'
import { geo } from '../../cache/geo.yaml'
import { setupStore } from '@/store'
import { useMeta } from 'vue-meta'
const localMethods = {
relayUrl() {
// We will see what `params` is shortly
@ -172,14 +180,12 @@ const localMethods = {
}
export default defineComponent({
title: "nostr.watch registry & network status",
name: 'SingleRelay',
components: {
Row,
Column,
LeafletSingleComponent,
// NavComponent,
SafeMail,
HeaderComponent,
// RefreshComponent,
@ -206,13 +212,24 @@ export default defineComponent({
}
},
setup(){
useMeta({
title: 'nostr.watch',
description: 'A robust client-side nostr relay monitor. Find fast nostr relays, view them on a map and monitor the network status of nostr.',
htmlAttrs: { lang: 'en', amp: true }
})
return {
store : setupStore()
}
},
async mounted() {
this.relay = this.relayUrl()
this.storage = useStorage()
this.lastUpdate = this.storage.getStorageSync('lastUpdate')
this.preferences = this.storage.getStorageSync('preferences')
this.result = this.storage.getStorageSync(this.relay)
this.lastUpdate = this.store.relays.getLastUpdate
// this.preferences = this.store.prefs.
this.result = this.store.relays.getResult(this.relay)
if(this.isExpired())
this.check(this.relay)
@ -237,7 +254,7 @@ table {margin:20px 10px 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; }
body, .grid-Column { padding:0; margin:0; }
.badges { display:block; margin: 10px 0 11px}
.badges > span {margin-right:5px}
#wrapper {max-width:800px}

33
src/lib/events-lib.js → src/shared/events.js

@ -5,6 +5,37 @@ import { RelayPool } from 'nostr'
const events = {}
events.discoverRelays = async function(){
return new Promise(resolve => {
const subid = crypto.randomBytes(40).toString('hex')
const pool = RelayPool(['wss://nostr.sandwich.farm'])
pool
.on('open', relay => {
// console.log('open')
relay.subscribe(subid, {limit: 1000, kinds:[3]})
})
.on('close', () => {
// console.log('close')
})
.on('event', (relay, _subid, event) => {
if(subid == _subid) {
try {
relaysRemote = Object.assign(relaysRemote, JSON.parse(event.content))
relay.close()
} catch(e) {""}
}
})
setTimeout( () => {
pool.close()
resolve(true)
}, 10*1000 )
})
}
events.addRelay = async function(){
}
events.signEvent = async function(event){
let event = {
kind: 10101,
@ -43,4 +74,4 @@ events.publish = async function (){
}
export default events
export default Events

305
src/shared/relays-lib.js

@ -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;
};
},
}

26
src/store/index.js

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

18
src/store/prefs.js

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

39
src/store/relays.js

@ -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() },
},
})

0
src/stores/geo.js → src/store/user.js

27
src/stores/relays.js

@ -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
src/stores/results.js

0
src/stores/userRelays.js

3
src/styles/main.scss

@ -1,4 +1,7 @@
@import "./components/tooltip.scss";
// @tailwind base;
// @tailwind components;
// @tailwind utilities;
td ul { padding:0; margin:0; list-style: none; }
td ul li { list-style: none; }

11
tailwind.config.js

@ -0,0 +1,11 @@
module.exports = {
content: ['./dist/*.html'],
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}

3
vue.config.js

@ -23,6 +23,9 @@ module.exports = defineConfig({
"bufferutil": false
}
},
experiments: {
topLevelAwait: true
},
},
chainWebpack: config => {
config.module

Loading…
Cancel
Save