dskvr
2 years ago
26 changed files with 7091 additions and 228 deletions
@ -1,23 +1,24 @@ |
|||
# nostr-relay-registry |
|||
|
|||
A dynamic registry of nostr relays that tests for very basic tasks in real-time. |
|||
|
|||
## Docker |
|||
|
|||
Build the docker image: |
|||
|
|||
```bash |
|||
docker build -t nostr-relay-registry . |
|||
## Project setup |
|||
``` |
|||
yarn install |
|||
``` |
|||
|
|||
Run the container interactively: |
|||
|
|||
```bash |
|||
docker run --rm -it --name=nostr-relay-registry -p 8080:80 nostr-relay-registry |
|||
### Compiles and hot-reloads for development |
|||
``` |
|||
yarn serve |
|||
``` |
|||
|
|||
Run container in the background: |
|||
### Compiles and minifies for production |
|||
``` |
|||
yarn build |
|||
``` |
|||
|
|||
```bash |
|||
docker run -d --restart unless-stopped --name nostr-relay-registry -p 8080:80 nostr-relay-registry |
|||
### Lints and fixes files |
|||
``` |
|||
yarn lint |
|||
``` |
|||
|
|||
### Customize configuration |
|||
See [Configuration Reference](https://cli.vuejs.org/config/). |
|||
|
@ -0,0 +1,5 @@ |
|||
module.exports = { |
|||
presets: [ |
|||
'@vue/cli-plugin-babel/preset' |
|||
] |
|||
} |
@ -1,29 +0,0 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
const esbuild = require('esbuild') |
|||
const alias = require('esbuild-plugin-alias') |
|||
const nodeGlobals = require('@esbuild-plugins/node-globals-polyfill').default |
|||
const vuePlugin = require("esbuild-vue"); |
|||
|
|||
const prod = process.argv.indexOf('prod') !== -1 |
|||
|
|||
esbuild |
|||
.build({ |
|||
bundle: true, |
|||
entryPoints: ['main.js'], |
|||
outdir: 'public', |
|||
plugins: [ |
|||
alias({ |
|||
stream: require.resolve('readable-stream') |
|||
}), |
|||
nodeGlobals({buffer: true}), |
|||
vuePlugin(), |
|||
], |
|||
minify: true, |
|||
sourcemap: prod ? false : 'inline', |
|||
define: { |
|||
window: 'self', |
|||
global: 'self' |
|||
} |
|||
}) |
|||
.then(() => console.log('build success.')) |
@ -0,0 +1,41 @@ |
|||
messages: |
|||
11cb5ca38038c3eb41bd014814f6e2e18da18ff1: |
|||
text: "we don't accept any events" |
|||
code: "READ_ONLY" |
|||
|
|||
1003d4ec1466033d0dcc4a1babc6c5f409784593: |
|||
text: "NIP-05 verification needed to publish events" |
|||
code: "NIP_05_REQUIRED" |
|||
|
|||
2e36f6955db854ac51105aa198fdf37cec694135: |
|||
text: "[ERROR]: Pubkey is not whitelisted." |
|||
code: "WHITELIST_REQUIRED" |
|||
|
|||
5d6c9cb06d52c3f0456cc08fdf883dbe19b3c782: |
|||
text: "failed to save event from 5a462fa6044b4b8da318528a6987a45e3adf832bd1c64bd6910eacfecdf07541" |
|||
code: "BLOCKS_WRITE_STATUS_CHECK" |
|||
2c791b26eda3205558c235973ca84cc68a80e5e0: |
|||
text: "pubkey is not allowed to publish to this relay" |
|||
code: "BLOCKS_WRITE_STATUS_CHECK" |
|||
203442bfdf9f7f1562f1164b6dc79fefb42790ac: |
|||
text: 'failed to save event 41ce9bc50da77dda5542f020370ecc2b056d8f2be93c1cedf1bf57efcab095b0: pq: duplicate key value violates unique constraint "ididx"' |
|||
code: "BLOCKS_WRITE_STATUS_CHECK" |
|||
af155223f0e51dea64560c4ef6dc341a2775af76: |
|||
text: 'failed to save event from 5a462fa6044b4b8da318528a6987a45e3adf832bd1c64bd6910eacfecdf07541' |
|||
code: 'BLOCKS_WRITE_STATUS_CHECK' |
|||
codes: |
|||
READ_ONLY: |
|||
type: "write_restricted" |
|||
description: "This relay only access read queries" |
|||
WRITE_ONLY: |
|||
type: "write_restricted" |
|||
description: "This relay only accepts the publishing of events" |
|||
NIP_05_REQUIRED: |
|||
type: "write_restricted" |
|||
description: "This relay only accepts the publishing of events from NIP-05 verified public keys" |
|||
WHITELIST_REQUIRED: |
|||
type: "write_restricted" |
|||
description: "This relay onoly accepts the publishing of events from whitelisted public keys" |
|||
BLOCKS_WRITE_STATUS_CHECK: |
|||
type: "maybe_public" |
|||
description: "This relay blocks the events that enable us to test writing to the relay, so there's some uncertainty" |
@ -0,0 +1,9 @@ |
|||
services: |
|||
nostr-relay-registry: |
|||
ports: |
|||
- '80:80' |
|||
restart: always |
|||
logging: |
|||
options: |
|||
max-size: 1g |
|||
image: nrr |
@ -0,0 +1,19 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"target": "es5", |
|||
"module": "esnext", |
|||
"baseUrl": "./", |
|||
"moduleResolution": "node", |
|||
"paths": { |
|||
"@/*": [ |
|||
"src/*" |
|||
] |
|||
}, |
|||
"lib": [ |
|||
"esnext", |
|||
"dom", |
|||
"dom.iterable", |
|||
"scripthost" |
|||
] |
|||
} |
|||
} |
@ -1,125 +0,0 @@ |
|||
import {createApp, h} from 'vue' |
|||
import {relayConnect} from 'nostr-tools/relay' |
|||
import yaml from 'js-yaml' |
|||
import fs from 'fs' |
|||
|
|||
const App = { |
|||
data() { |
|||
return { |
|||
relays: yaml.load(fs.readFileSync('relays.yml', 'utf8')).relays, |
|||
status: {} |
|||
} |
|||
}, |
|||
|
|||
mounted() { |
|||
this.connections = {} |
|||
this.relays.forEach(async url => { |
|||
this.status[url] = {} |
|||
|
|||
try { |
|||
let conn = relayConnect( |
|||
url, |
|||
() => {}, |
|||
() => { |
|||
this.status[url].didConnect = false |
|||
} |
|||
) |
|||
this.status[url].didConnect = true |
|||
|
|||
try { |
|||
await conn.publish({ |
|||
id: '41ce9bc50da77dda5542f020370ecc2b056d8f2be93c1cedf1bf57efcab095b0', |
|||
pubkey: |
|||
'5a462fa6044b4b8da318528a6987a45e3adf832bd1c64bd6910eacfecdf07541', |
|||
created_at: 1640305962, |
|||
kind: 1, |
|||
tags: [], |
|||
content: 'running branle', |
|||
sig: '08e6303565e9282f32bed41eee4136f45418f366c0ec489ef4f90d13de1b3b9fb45e14c74f926441f8155236fb2f6fef5b48a5c52b19298a0585a2c06afe39ed' |
|||
}) |
|||
this.status[url].didPublish = true |
|||
} catch (err) { |
|||
this.status[url].didPublish = false |
|||
} |
|||
|
|||
let {unsub} = conn.sub( |
|||
{ |
|||
cb: () => { |
|||
this.status[url].didQuery = true |
|||
unsub() |
|||
clearTimeout(willUnsub) |
|||
}, |
|||
filter: { |
|||
ids: [ |
|||
'41ce9bc50da77dda5542f020370ecc2b056d8f2be93c1cedf1bf57efcab095b0' |
|||
] |
|||
} |
|||
}, |
|||
'nostr-registry' |
|||
) |
|||
let willUnsub = setTimeout(() => { |
|||
unsub() |
|||
this.status[url].didQuery = false |
|||
}, 3000) |
|||
} catch (err) { |
|||
this.status[url].didConnect = false |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// render() {
|
|||
// return h('table', {style: {fontSize: '28px'}}, [
|
|||
// h('tr', [h('th'), h('th', 'connect'), h('th', 'read'), h('th', 'write')]),
|
|||
// ...this.relays.map(url =>
|
|||
// h('tr', [
|
|||
// h(
|
|||
// 'th',
|
|||
// {
|
|||
// style: {
|
|||
// textAlign: 'right',
|
|||
// whiteSpace: 'pre-wrap',
|
|||
// wordWrap: 'break-word',
|
|||
// wordBreak: 'break-all'
|
|||
// }
|
|||
// },
|
|||
// url
|
|||
// ),
|
|||
// ...['didConnect', 'didQuery', 'didPublish'].map(attr =>
|
|||
// h(
|
|||
// 'td',
|
|||
// {
|
|||
// style: {
|
|||
// fontSize: '50px',
|
|||
// textAlign: 'center',
|
|||
// padding: '5px 35px',
|
|||
// color:
|
|||
// this.status?.[url]?.[attr] === true
|
|||
// ? 'green'
|
|||
// : this.status?.[url]?.[attr] === false
|
|||
// ? 'red'
|
|||
// : 'silver'
|
|||
// }
|
|||
// },
|
|||
// '•'
|
|||
// )
|
|||
// )
|
|||
// ])
|
|||
// ),
|
|||
// h('tr', [
|
|||
// h('th', {style: {textAlign: 'right'}}, 'Your relay here:'),
|
|||
// h('th', {colSpan: 3}, [
|
|||
// h(
|
|||
// 'a',
|
|||
// {
|
|||
// href: 'https://github.com/fiatjaf/nostr-relay-registry',
|
|||
// style: {color: 'black'}
|
|||
// },
|
|||
// '________________'
|
|||
// )
|
|||
// ])
|
|||
// ])
|
|||
// ])
|
|||
// }
|
|||
} |
|||
|
|||
createApp(App).mount('#app') |
@ -0,0 +1,16 @@ |
|||
const url = require('url'); |
|||
const http = require('http'); |
|||
const https = require('https'); |
|||
const SocksProxyAgent = require('socks-proxy-agent'); |
|||
|
|||
// Use the SOCKS_PROXY env var if using a custom bind address or port for your TOR proxy:
|
|||
const proxy = process.env.SOCKS_PROXY || 'socks5h://127.0.0.1:9050'; |
|||
console.log('Using proxy server %j', proxy); |
|||
// The default HTTP endpoint here is DuckDuckGo's v3 onion address:
|
|||
const endpoint = process.argv[2] || 'https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion'; |
|||
console.log('Attempting to GET %j', endpoint); |
|||
// Prepare options for the http/s module by parsing the endpoint URL:
|
|||
let options = url.parse(endpoint); |
|||
const agent = new SocksProxyAgent(proxy); |
|||
// Here we pass the socks proxy agent to the http/s module:
|
|||
options.agent = agent; |
@ -1,20 +1,69 @@ |
|||
{ |
|||
"name": "nostr-relay-registry", |
|||
"version": "0.1.0", |
|||
"private": true, |
|||
"scripts": { |
|||
"serve": "vue-cli-service serve --host localhost", |
|||
"build": "vue-cli-service build", |
|||
"watch": "vue-cli-service build --watch", |
|||
"lint": "vue-cli-service lint" |
|||
}, |
|||
"dependencies": { |
|||
"@esbuild-plugins/node-globals-polyfill": "^0.1.1", |
|||
"buffer": "^6.0.3", |
|||
"esbuild": "^0.14.21", |
|||
"esbuild-plugin-alias": "^0.2.1", |
|||
"events": "^3.3.0", |
|||
"nostr-tools": "^0.22.1", |
|||
"readable-stream": "^3.6.0", |
|||
"vue": "3" |
|||
"core-js": "^3.8.3", |
|||
"country-code-emoji": "2.3.0", |
|||
"doh-resolver": "1.2.8", |
|||
"geoip-lite": "1.4.6", |
|||
"global": "4.4.0", |
|||
"ip-fetch": "1.0.10", |
|||
"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-polyfill-webpack-plugin": "2.0.1", |
|||
"nostr": "https://github.com/dskvr/nostr-js", |
|||
"nostr-tools": "0.24.1", |
|||
"onion-regex": "2.0.8", |
|||
"requests": "0.3.0", |
|||
"socks-proxy-agent": "7.0.0", |
|||
"stream-browserify": "3.0.0", |
|||
"vue": "^3.2.13", |
|||
"vue-grid-responsive": "1.3.0", |
|||
"vue-nav-tabs": "0.5.7", |
|||
"vue-simple-maps": "1.1.3", |
|||
"vue3-popper": "1.5.0", |
|||
"yaml-loader": "^0.6.0", |
|||
"yaml2json": "1.0.2" |
|||
}, |
|||
"devDependencies": { |
|||
"esbuild-vue": "1.2.2", |
|||
"json-server": "0.17.1" |
|||
"@babel/core": "^7.12.16", |
|||
"@babel/eslint-parser": "^7.12.16", |
|||
"@vue/cli-plugin-babel": "~5.0.0", |
|||
"@vue/cli-plugin-eslint": "~5.0.0", |
|||
"@vue/cli-service": "~5.0.0", |
|||
"eslint": "^7.32.0", |
|||
"eslint-plugin-vue": "^8.0.3", |
|||
"vue-cli-plugin-yaml-loader": "~1.0.0", |
|||
"webpack-cli": "5.0.0" |
|||
}, |
|||
"scripts": { |
|||
"build": "./build.js prod", |
|||
"watch": "ag -l --js | entr ./build.js" |
|||
} |
|||
"eslintConfig": { |
|||
"root": true, |
|||
"env": { |
|||
"node": true |
|||
}, |
|||
"extends": [ |
|||
"plugin:vue/vue3-essential", |
|||
"eslint:recommended" |
|||
], |
|||
"parserOptions": { |
|||
"parser": "@babel/eslint-parser" |
|||
}, |
|||
"rules": {} |
|||
}, |
|||
"browserslist": [ |
|||
"> 1%", |
|||
"last 2 versions", |
|||
"not dead", |
|||
"not ie 11" |
|||
] |
|||
} |
|||
|
@ -1,11 +1,25 @@ |
|||
<template> |
|||
<router-view /> |
|||
<BaseRelays /> |
|||
</template> |
|||
|
|||
<script> |
|||
import { defineComponent } from 'vue' |
|||
import BaseRelays from './components/BaseRelays.vue' |
|||
|
|||
export default defineComponent({ |
|||
name: 'App' |
|||
}) |
|||
export default { |
|||
name: 'App', |
|||
components: { |
|||
BaseRelays, |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
#app { |
|||
font-family: Avenir, Helvetica, Arial, sans-serif; |
|||
-webkit-font-smoothing: antialiased; |
|||
-moz-osx-font-smoothing: grayscale; |
|||
text-align: center; |
|||
color: #2c3e50; |
|||
margin-top: 60px; |
|||
} |
|||
</style> |
|||
|
After Width: | Height: | Size: 6.7 KiB |
@ -0,0 +1,603 @@ |
|||
<template> |
|||
<div> |
|||
<div class="text-h5 text-bold q-py-md q-px-sm full-width flex row justify-start"> |
|||
<h1>Nostr Relay Registry</h1> |
|||
<span>Next ping in {{ nextPing }} seconds</span> | |
|||
<span v-if="relays.filter((url) => status[url] && !status[url].complete).length > 0">Processing {{relays.filter((url) => status[url].complete).length}}/{{relays.length}}</span> |
|||
</div> |
|||
|
|||
<row container :gutter="12"> |
|||
<column :xs="12" :md="12" :lg="6"> |
|||
<div> |
|||
<h2><span class="indicator badge readwrite">{{ query('public').length }}</span>Public</h2> |
|||
<table class="online" v-if="query('public').length > 0"> |
|||
<tr> |
|||
<th></th> |
|||
<th></th> |
|||
<th>🔌</th> |
|||
<th>👁️🗨️</th> |
|||
<th>✏️</th> |
|||
<th>🌎</th> |
|||
<!-- <td>wl</td> |
|||
<td>nip-05><td> --> |
|||
<th>⌛️</th> |
|||
<th>ℹ️</th> |
|||
</tr> |
|||
<tr v-for="relay in query('public')" :key="{relay}" :class="getLoadingClass(relay)"> |
|||
<td :key="generateKey(relay, 'aggregate')"><span :class="getAggregateStatusClass(relay)"></span></td> |
|||
|
|||
<td class="left-align relay-url" @click="copy(relay)">{{ relay }}</td> |
|||
<td :key="generateKey(relay, 'didConnect')"><span :class="getStatusClass(relay, 'didConnect')"></span></td> |
|||
<td :key="generateKey(relay, 'didRead')"><span :class="getStatusClass(relay, 'didRead')"></span></td> |
|||
<td :key="generateKey(relay, 'didWrite')"><span :class="getStatusClass(relay, 'didWrite')"></span></td> |
|||
<td>{{status[relay].flag}}</td> |
|||
<td><span v-if="status[relay].didConnect">{{ status[relay].latency }}<span v-if="status[relay].latency">ms</span></span></td> |
|||
<td> |
|||
<Popper v-if="Object.keys(status[relay].messages).length"> |
|||
{{ status[relay].type }} |
|||
<button @mouseover="showPopper">log</button> |
|||
<template #content> |
|||
<ul> |
|||
<li v-for="(message, key) in status[relay].messages" :key="generateKey(relay, key)">{{key}}</li> |
|||
</ul> |
|||
</template> |
|||
</Popper> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</div> |
|||
</column> |
|||
|
|||
<column :xs="12" :md="12" :lg="6"> |
|||
<div> |
|||
<h2><span class="indicator badge write-only">{{ query('restricted').length }}</span>Restricted</h2> |
|||
<table class="online"> |
|||
<tr> |
|||
<th></th> |
|||
<th></th> |
|||
<th>🔌</th> |
|||
<th>👁️🗨️</th> |
|||
<th>✏️</th> |
|||
<th>🌎</th> |
|||
<th>⌛️</th> |
|||
<th>ℹ️</th> |
|||
</tr> |
|||
<tr v-for="relay in query('restricted')" :key="{relay}" :class="getLoadingClass(relay)"> |
|||
<td :key="generateKey(relay, 'aggregate')"><span :class="getAggregateStatusClass(relay)"><span></span><span></span></span></td> |
|||
<td class="left-align relay-url" @click="copy(relay)">{{ relay }}</td> |
|||
<td :key="generateKey(relay, 'didConnect')"><span :class="getStatusClass(relay, 'didConnect')"></span></td> |
|||
<td :key="generateKey(relay, 'didRead')"><span :class="getStatusClass(relay, 'didRead')"></span></td> |
|||
<td :key="generateKey(relay, 'didWrite')"><span :class="getStatusClass(relay, 'didWrite')"></span></td> |
|||
<td>{{status[relay].flag}}</td> |
|||
<td><span v-if="status[relay].didConnect">{{ status[relay].latency }}<span v-if="status[relay].latency">ms</span></span></td> |
|||
<td> |
|||
<Popper v-if="Object.keys(status[relay].messages).length"> |
|||
<button @mouseover="showPopper">log</button> |
|||
<template #content> |
|||
<ul> |
|||
<li v-for="(message, key) in status[relay].messages" :key="generateKey(relay, key)">{{key}}</li> |
|||
</ul> |
|||
</template> |
|||
</Popper> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
|
|||
<h2><span class="indicator badge offline">{{ query('offline').length }}</span>Offline</h2> |
|||
<table v-if="query('offline').length > 0"> |
|||
<tr> |
|||
<th></th> |
|||
<th></th> |
|||
<th>🔌</th> |
|||
<th>👁️🗨️</th> |
|||
<th>✏️</th> |
|||
<th>msg</th> |
|||
</tr> |
|||
<tr v-for="relay in query('offline')" :key="{relay}" :class="getLoadingClass(relay)"> |
|||
<td :key="generateKey(relay, 'aggregate')"><span :class="getAggregateStatusClass(relay)"></span></td> |
|||
<td class="left-align relay-url">{{ relay }}</td> |
|||
<td :key="generateKey(relay, 'didConnect')"><span :class="getStatusClass(relay, 'didConnect')"></span></td> |
|||
<td :key="generateKey(relay, 'didRead')"><span :class="getStatusClass(relay, 'didRead')"></span></td> |
|||
<td :key="generateKey(relay, 'didWrite')"><span :class="getStatusClass(relay, 'didWrite')"></span></td> |
|||
<td> |
|||
<Popper v-if="Object.keys(status[relay].messages).length"> |
|||
<button>log</button> |
|||
<template #content> |
|||
<ul> |
|||
<li v-for="(message, key) in status[relay].messages" :key="generateKey(relay, key)">{{key}}</li> |
|||
</ul> |
|||
</template> |
|||
</Popper> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</div> |
|||
</column> |
|||
</row> |
|||
|
|||
<!-- <h2>Processing</h2> |
|||
<table v-if="relays.filter((url) => !status[url].complete).length > 0"> |
|||
<tr> |
|||
<th></th> |
|||
</tr> |
|||
<tr v-for="relay in relays.filter((url) => !status[url].complete)" :key="{relay}" :class="getLoadingClass(relay)"> |
|||
<td>{{ relay }}</td> |
|||
</tr> |
|||
</table> |
|||
<a href="./relays/">JSON API</a> --> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { defineComponent} from 'vue' |
|||
import { relayConnect } from 'nostr-tools/relay' |
|||
|
|||
import { Row, Column } from 'vue-grid-responsive'; |
|||
import Popper from "vue3-popper"; |
|||
import onionRegex from 'onion-regex'; |
|||
import countryCodeEmoji from 'country-code-emoji' |
|||
import emoji from 'node-emoji' |
|||
|
|||
import { relays } from '../../relays.yaml' |
|||
import { messages as RELAY_MESSAGES, codes as RELAY_CODES } from '../../codes.yaml' |
|||
|
|||
import crypto from "crypto" |
|||
|
|||
const refreshMillis = 3*60*1000 |
|||
|
|||
export default defineComponent({ |
|||
name: 'BaseRelays', |
|||
components: { |
|||
Row, |
|||
Column, |
|||
Popper |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
relays, |
|||
status: {}, |
|||
lastPing: Date.now(), |
|||
nextPing: Date.now() + (60*1000), |
|||
connections: {}, |
|||
latency: {}, |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
|
|||
// query (group, filterType) { |
|||
query (group) { |
|||
let unordered, |
|||
filterFn |
|||
|
|||
// if(filterByType) { |
|||
// filterFn = (relay) => this.status?.[relay]?.aggregate == group || this.status?.[relay]?.[filterType]; |
|||
// } else { |
|||
filterFn = (relay) => this.status?.[relay]?.aggregate == group |
|||
// } |
|||
|
|||
unordered = this.relays.filter(filterFn); |
|||
|
|||
if (unordered.length) { |
|||
return unordered.sort((relay1, relay2) => { |
|||
return this.status?.[relay1]?.latency - this.status?.[relay2]?.latency |
|||
}) |
|||
} |
|||
|
|||
return [] |
|||
}, |
|||
|
|||
getAggregateStatusClass (url) { |
|||
let status = '' |
|||
if (this.status?.[url]?.aggregate == null) { |
|||
status = 'unprocessed' |
|||
} |
|||
else if (this.status?.[url]?.aggregate == 'public') { |
|||
status = 'readwrite' |
|||
} |
|||
else if (this.status?.[url]?.aggregate == 'restricted') { |
|||
if(this.status?.[url]?.didWrite) { |
|||
status = 'write-only' |
|||
} else { |
|||
status = 'read-only' |
|||
} |
|||
} |
|||
else if (this.status?.[url]?.aggregate == 'offline') { |
|||
status = 'offline' |
|||
} |
|||
return `aggregate indicator ${status}` |
|||
}, |
|||
|
|||
getStatusClass (url, key) { |
|||
let status = this.status?.[url]?.[key] === true |
|||
? 'green' |
|||
: this.status?.[url]?.[key] === false |
|||
? 'red' |
|||
: 'silver' |
|||
return `indicator ${status}` |
|||
}, |
|||
|
|||
getLoadingClass (url) { |
|||
return this.status?.[url]?.complete ? "relay loaded" : "relay" |
|||
}, |
|||
|
|||
setAggregateStatus (url) { |
|||
let aggregateTally = 0 |
|||
aggregateTally += this.status?.[url]?.didConnect ? 1 : 0 |
|||
aggregateTally += this.status?.[url]?.didRead ? 1 : 0 |
|||
aggregateTally += this.status?.[url]?.didWrite ? 1 : 0 |
|||
if (aggregateTally == 3) { |
|||
this.status[url].aggregate = 'public' |
|||
} |
|||
else if (aggregateTally == 0) { |
|||
this.status[url].aggregate = 'offline' |
|||
} |
|||
else { |
|||
this.status[url].aggregate = 'restricted' |
|||
} |
|||
}, |
|||
|
|||
async copy(text) { |
|||
try { |
|||
await navigator.clipboard.writeText(text); |
|||
} catch($e) { |
|||
console.log('Cannot copy'); |
|||
} |
|||
}, |
|||
|
|||
setComplete (url) { |
|||
this.setAggregateStatus(url) |
|||
this.status[url].complete = true |
|||
}, |
|||
generateKey (url, key) { |
|||
return `${url}_${key}` |
|||
}, |
|||
|
|||
testConnect (url) { |
|||
console.log(url, "CONNECT", "TEST") |
|||
this.connections[url] = relayConnect( |
|||
url, |
|||
// () => {}, |
|||
(message) => { |
|||
console.log(url, "CONNECT", "SUCCESS") |
|||
const hash = this.sha1(message) |
|||
let message_obj = RELAY_MESSAGES[hash] |
|||
let code_obj = RELAY_CODES[message_obj.code] |
|||
|
|||
console.log(hash) |
|||
console.dir(message_obj) |
|||
console.dir(code_obj) |
|||
|
|||
message_obj.type = code_obj.type |
|||
|
|||
this.status[url].messages[message] = message_obj |
|||
|
|||
this.adjustStatus(url, hash) |
|||
// console.log("RECIEVED MESSAGE!") |
|||
// console.dir(this.status[url].messages) |
|||
}, |
|||
() => { |
|||
console.log(url, "CONNECT", "FAILURE") |
|||
this.status[url].didConnect = false |
|||
this.status[url].didRead = false |
|||
this.status[url].didWrite = false |
|||
this.setComplete(url) |
|||
} |
|||
) |
|||
this.status[url].didConnect = true |
|||
}, |
|||
|
|||
async testRead (url) { |
|||
console.dir(this.connections[url]) |
|||
// console.log(this.connections[url]['get status']()) |
|||
console.log(url, "READ", "TEST") |
|||
let start |
|||
start = Date.now(); |
|||
let {unsub} = await this.connections[url].sub( |
|||
{ |
|||
cb: () => { |
|||
console.log(url, "READ", "SUCCESS") |
|||
this.status[url].didRead = true |
|||
this.setComplete(url) |
|||
this.latency[url].read = Date.now() - start; |
|||
unsub() |
|||
clearTimeout(willUnsub) |
|||
}, |
|||
filter: { |
|||
ids: [ |
|||
'41ce9bc50da77dda5542f020370ecc2b056d8f2be93c1cedf1bf57efcab095b0' |
|||
] |
|||
} |
|||
}, |
|||
'nostr-registry' |
|||
) |
|||
let willUnsub = setTimeout(() => { |
|||
unsub() |
|||
console.log(url, "READ", "FAILURE") |
|||
if(!this.status[url].maybe_public) this.status[url].didRead = false |
|||
this.setComplete(url) |
|||
}, 10000) |
|||
}, |
|||
|
|||
async testWrite (url) { |
|||
console.log(url, "WRITE", "TEST") |
|||
let start |
|||
start = Date.now(); |
|||
await this.connections[url].publish({ |
|||
id: '41ce9bc50da77dda5542f020370ecc2b056d8f2be93c1cedf1bf57efcab095b0', |
|||
pubkey: |
|||
'5a462fa6044b4b8da318528a6987a45e3adf832bd1c64bd6910eacfecdf07541', |
|||
created_at: 1640305962, |
|||
kind: 1, |
|||
tags: [], |
|||
content: 'running branle', |
|||
sig: '08e6303565e9282f32bed41eee4136f45418f366c0ec489ef4f90d13de1b3b9fb45e14c74f926441f8155236fb2f6fef5b48a5c52b19298a0585a2c06afe39ed' |
|||
}) |
|||
this.latency[url].write = Date.now() - start; |
|||
}, |
|||
|
|||
async testRelay (url) { |
|||
this.lastPing = Date.now() |
|||
this.latency[url] = {} |
|||
this.status[url].messages = {} |
|||
try { |
|||
|
|||
//Test Connect |
|||
this.testConnect(url) |
|||
|
|||
//Test Write |
|||
try { |
|||
await this.testWrite(url) |
|||
console.log(url, "WRITE", "SUCCESS") |
|||
this.status[url].didWrite = true |
|||
} catch (err) { |
|||
console.log(url, "WRITE", "FAILURE") |
|||
this.status[url].didWrite = false |
|||
this.setComplete(url) |
|||
} |
|||
|
|||
//Test Read |
|||
this.testRead(url) |
|||
|
|||
} catch (err) { |
|||
this.status[url].didConnect = false |
|||
this.setComplete(url) |
|||
} |
|||
|
|||
if(this.status[url].didRead){ |
|||
this.setLatency(url) |
|||
} |
|||
|
|||
await this.getIP(url) |
|||
await this.setGeo(url) |
|||
this.setFlag(url) |
|||
}, |
|||
|
|||
isOnion(url){ |
|||
return onionRegex().test(url) |
|||
}, |
|||
|
|||
setLatency(url) { |
|||
this.status[url].latency = this.latency[url].read |
|||
}, |
|||
|
|||
testRelayLatency(){ |
|||
console.log('testing latency') |
|||
this.relays.forEach(url => { |
|||
// this.testWrite(url, true) |
|||
this.testRead(url, true) |
|||
this.setLatency(url) |
|||
}) |
|||
this.lastPing = Date.now() |
|||
}, |
|||
|
|||
async getIP(url){ |
|||
let ip |
|||
await fetch(`https://1.1.1.1/dns-query?name=${url.replace('wss://', '')}`, { headers: { 'accept': 'application/dns-json' } }) |
|||
.then(response => response.json()) |
|||
.then((data) => { ip = data.Answer ? data.Answer[data.Answer.length-1].data : false }); |
|||
this.status[url].ip = ip |
|||
console.log('IP:', ip) |
|||
}, |
|||
|
|||
async setGeo(url){ |
|||
if (!this.status[url].ip) return |
|||
await fetch(`http://ip-api.com/json/${this.status[url].ip}`, { headers: { 'accept': 'application/dns-json' } }) |
|||
.then(response => response.json()) |
|||
.then((data) => { this.status[url].geo = data }); |
|||
console.dir(this.status[url].geo) |
|||
}, |
|||
|
|||
setFlag (url) { |
|||
this.status[url].flag = this.status[url].geo?.countryCode ? countryCodeEmoji(this.status[url].geo.countryCode) : emoji.get('shrug'); |
|||
}, |
|||
|
|||
adjustStatus (url, hash) { |
|||
let code = RELAY_MESSAGES[hash].code, |
|||
type = RELAY_CODES[code].type |
|||
|
|||
this.status[url][type] = code |
|||
if (type == "maybe_public") { |
|||
this.status[url].didWrite = true |
|||
this.status[url].didRead = true |
|||
} |
|||
if (type == "write_restricted") { |
|||
this.status[url].didWrite = false |
|||
} |
|||
|
|||
}, |
|||
|
|||
sha1 (message) { |
|||
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex') |
|||
// console.log(message, ':', hash) |
|||
return hash |
|||
}, |
|||
}, |
|||
|
|||
|
|||
|
|||
mounted() { |
|||
this.relays.forEach(async url => { |
|||
this.status[url] = {} //statusInterface |
|||
if (this.isOnion(url)) { |
|||
url = `${url}.to` //add proxy |
|||
} |
|||
await this.testRelay(url) |
|||
}) |
|||
|
|||
// eslint-disable-next-line |
|||
let latencyTimeout = setTimeout(() => { this.testRelayLatency() }, 10000) |
|||
|
|||
// eslint-disable-next-line |
|||
let latencyIntVal = setInterval(() => { this.testRelayLatency() }, refreshMillis) |
|||
// eslint-disable-next-line |
|||
let counterIntVal = setInterval(() => { |
|||
this.nextPing = Math.round((this.lastPing + refreshMillis - Date.now())/1000) |
|||
}, 1000) |
|||
|
|||
}, |
|||
|
|||
}) |
|||
</script> |
|||
|
|||
<style lang='css' scoped> |
|||
.q-tabs { |
|||
border-bottom: 1px solid var(--q-accent) |
|||
} |
|||
|
|||
table { |
|||
width:100%; |
|||
} |
|||
|
|||
.left-align { |
|||
text-align:left; |
|||
} |
|||
|
|||
tr.relay td { |
|||
font-style: italic; |
|||
opacity: 0.5; |
|||
} |
|||
|
|||
tr.relay.loaded td { |
|||
font-style: normal; |
|||
opacity: 1; |
|||
} |
|||
|
|||
.indicator { |
|||
display:block; |
|||
margin: 0 auto; |
|||
height: 14px; |
|||
width: 14px; |
|||
border-radius: 7px; |
|||
border-width:0px; |
|||
} |
|||
|
|||
.badge { |
|||
height:auto; |
|||
width: auto; |
|||
display:inline-block; |
|||
padding: 2px 5px; |
|||
font-size: 15px; |
|||
position: relative; |
|||
top: -3px; |
|||
min-width: 15px; |
|||
margin-right:5px; |
|||
} |
|||
|
|||
.badge.readwrite, |
|||
.badge.offline { |
|||
color: white; |
|||
} |
|||
|
|||
.badge.write-only, |
|||
.badge.read-only { |
|||
background-color:orange !important; |
|||
} |
|||
|
|||
.aggregate.indicator { |
|||
background-color: transparent; |
|||
border-radius: 0px; |
|||
border-style: solid; |
|||
} |
|||
|
|||
.indicator.silver { |
|||
background-color: #c0c0c0; |
|||
border-color: rgba(55,55,55,0.5); |
|||
} |
|||
|
|||
.indicator.green { |
|||
background-color: green; |
|||
border-color: rgba(0,255,0,0.5); |
|||
} |
|||
|
|||
.indicator.red { |
|||
background-color: red; |
|||
border-color: rgba(255,0,0,0.5); |
|||
} |
|||
|
|||
.indicator.orange { |
|||
background-color: orange; |
|||
border-color: rgba(255, 191, 0,0.5); |
|||
} |
|||
|
|||
.indicator.readwrite { |
|||
background-color: green; |
|||
border-color: rgba(0,255,0,0.5); |
|||
} |
|||
|
|||
.indicator.read-only { |
|||
position:relative; |
|||
border-color: transparent; |
|||
background-color: transparent |
|||
} |
|||
|
|||
.indicator.read-only span:first-child { |
|||
position:absolute; |
|||
width: 0; |
|||
height: 0; |
|||
border-top: 14px solid green; |
|||
border-right: 14px solid transparent; |
|||
} |
|||
|
|||
.indicator.read-only span:last-child { |
|||
position:absolute; |
|||
width: 0; |
|||
height: 0; |
|||
border-bottom: 14px solid orange; |
|||
border-left: 14px solid transparent; |
|||
} |
|||
|
|||
|
|||
.indicator.write-only { |
|||
position:relative; |
|||
border-color: transparent; |
|||
background-color: transparent |
|||
} |
|||
|
|||
.indicator.write-only span:first-child { |
|||
position:absolute; |
|||
width: 0; |
|||
height: 0; |
|||
border-bottom: 14px solid orange; |
|||
border-left: 14px solid transparent; |
|||
} |
|||
|
|||
.indicator.write-only span:last-child { |
|||
position:absolute; |
|||
width: 0; |
|||
height: 0; |
|||
border-top: 14px solid green; |
|||
border-right: 14px solid transparent; |
|||
} |
|||
|
|||
.indicator.offline { |
|||
background-color: red; |
|||
border-color: rgba(255,0,0,0.5); |
|||
} |
|||
|
|||
table.online .relay-url { |
|||
cursor: pointer; |
|||
} |
|||
|
|||
</style> |
@ -0,0 +1,39 @@ |
|||
<template> |
|||
<h2><span class="indicator badge readwrite">{{ query('public').length }}</span>Public</h2> |
|||
<table class="online" v-if="query('public').length > 0"> |
|||
<tr> |
|||
<th></th> |
|||
<th></th> |
|||
<th>🔌</th> |
|||
<th>👁️🗨️</th> |
|||
<th>✏️</th> |
|||
<th>🌎</th> |
|||
<!-- <td>wl</td> |
|||
<td>nip-05><td> --> |
|||
<th>⌛️</th> |
|||
<th>ℹ️</th> |
|||
</tr> |
|||
<tr v-for="relay in query('public')" :key="{relay}" :class="getLoadingClass(relay)"> |
|||
<RelaySingle |
|||
:relay="{{relay}}" |
|||
:didConnect="{{didConnect}}" |
|||
:didRead="{{didRead}}" |
|||
:didWrite="{{didWrite}}" |
|||
/> |
|||
</tr> |
|||
</table> |
|||
</template> |
|||
|
|||
<script> |
|||
import { defineComponent} from 'vue' |
|||
export default defineComponent({ |
|||
name: 'RelaySingle', |
|||
components: { |
|||
Popper |
|||
}, |
|||
|
|||
data() { |
|||
|
|||
} |
|||
}) |
|||
</script> |
@ -0,0 +1,38 @@ |
|||
<template> |
|||
<td :key="generateKey(relay, 'aggregate')"><span :class="getAggregateStatusClass(relay)"></span></td> |
|||
|
|||
<td class="left-align relay-url" @click="copy(relay)">{{ relay }}</td> |
|||
<td :key="generateKey(relay, 'didConnect')"><span :class="getStatusClass(relay, 'didConnect')"></span></td> |
|||
<td :key="generateKey(relay, 'didRead')"><span :class="getStatusClass(relay, 'didRead')"></span></td> |
|||
<td :key="generateKey(relay, 'didWrite')"><span :class="getStatusClass(relay, 'didWrite')"></span></td> |
|||
<td>{{status[relay].flag}}</td> |
|||
<td><span v-if="status[relay].didConnect">{{ status[relay].latency }}<span v-if="status[relay].latency">ms</span></span></td> |
|||
<td> |
|||
<Popper v-if="Object.keys(status[relay].messages).length"> |
|||
{{ status[relay].type }} |
|||
<button @mouseover="showPopper">log</button> |
|||
<template #content> |
|||
<ul> |
|||
<li v-for="(message, key) in status[relay].messages" :key="generateKey(relay, key)">{{key}}</li> |
|||
</ul> |
|||
</template> |
|||
</Popper> |
|||
</td> |
|||
</template> |
|||
|
|||
<script> |
|||
import { defineComponent} from 'vue' |
|||
export default defineComponent({ |
|||
name: 'RelaySingle', |
|||
components: { |
|||
Popper |
|||
}, |
|||
props: [ |
|||
'relay', |
|||
'status', |
|||
] |
|||
data() { |
|||
return {} |
|||
} |
|||
}) |
|||
</script> |
@ -0,0 +1,4 @@ |
|||
import { createApp } from 'vue' |
|||
import App from './App.vue' |
|||
|
|||
createApp(App).mount('#app') |
@ -1,23 +0,0 @@ |
|||
import { route } from 'quasar/wrappers' |
|||
import { |
|||
createRouter, |
|||
createWebHistory, |
|||
createWebHashHistory, |
|||
} from 'vue-router' |
|||
import routes from './routes' |
|||
|
|||
export default route(() => { |
|||
const createHistory = |
|||
process.env.VUE_ROUTER_MODE === 'history' |
|||
? createWebHistory |
|||
: createWebHashHistory |
|||
|
|||
const Router = createRouter({ |
|||
routes, |
|||
history: createHistory( |
|||
process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE |
|||
), |
|||
}) |
|||
|
|||
return Router |
|||
}) |
@ -1,9 +0,0 @@ |
|||
const routes = [ |
|||
{ |
|||
path: '/', |
|||
component: () => import('layouts/MainLayout.vue'), |
|||
children: [ |
|||
{ path: '', component: () => import('pages/IndexPage.vue') } |
|||
] |
|||
|
|||
export default routes |
@ -0,0 +1,25 @@ |
|||
const { defineConfig } = require('@vue/cli-service') |
|||
const NodePolyfillPlugin = require("node-polyfill-webpack-plugin"); |
|||
|
|||
module.exports = defineConfig({ |
|||
transpileDependencies: true, |
|||
devServer: { |
|||
port: 8080 |
|||
}, |
|||
configureWebpack: { |
|||
// watch: true,
|
|||
plugins: [new NodePolyfillPlugin()], |
|||
optimization: { |
|||
splitChunks: { |
|||
chunks: "all", |
|||
}, |
|||
}, |
|||
}, |
|||
chainWebpack: config => { |
|||
config.module |
|||
.rule('yaml') |
|||
.test(/\.ya?ml?$/) |
|||
.use('yaml-loader') |
|||
.loader('yaml-loader') |
|||
} |
|||
}) |
File diff suppressed because it is too large
Loading…
Reference in new issue