mirror of
https://github.com/aljazceru/nostr-watch.git
synced 2025-12-17 05:24:19 +01:00
Working refactor
This commit is contained in:
11
Dockerfile
11
Dockerfile
@@ -2,15 +2,18 @@ FROM node:14-alpine as build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY public/index.html /app/public/index.html
|
||||
|
||||
COPY main.js package.json build.js /app/
|
||||
COPY . /app/
|
||||
|
||||
RUN yarn \
|
||||
&& yarn run build
|
||||
&& yarn build
|
||||
|
||||
RUN yarn global add yaml2json
|
||||
|
||||
RUN yaml2json relays.yaml > dist/relays.json
|
||||
|
||||
FROM nginx:stable-alpine as nginx-nostr-relay-registry
|
||||
|
||||
COPY ./nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
COPY --from=build /app/public /app
|
||||
COPY --from=build /app/dist /app
|
||||
|
||||
33
README.md
33
README.md
@@ -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:
|
||||
|
||||
```bash
|
||||
docker run -d --restart unless-stopped --name nostr-relay-registry -p 8080:80 nostr-relay-registry
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
|
||||
5
babel.config.js
Normal file
5
babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
29
build.js
29
build.js
@@ -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.'))
|
||||
41
codes.yaml
Normal file
41
codes.yaml
Normal file
@@ -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"
|
||||
9
docker-compose.yaml
Normal file
9
docker-compose.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
services:
|
||||
nostr-relay-registry:
|
||||
ports:
|
||||
- '80:80'
|
||||
restart: always
|
||||
logging:
|
||||
options:
|
||||
max-size: 1g
|
||||
image: nrr
|
||||
19
jsconfig.json
Normal file
19
jsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
||||
125
main.js
125
main.js
@@ -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')
|
||||
@@ -2,6 +2,10 @@ server {
|
||||
listen 80;
|
||||
root /app;
|
||||
|
||||
location /relays/ {
|
||||
rewrite ^ relays.json break;
|
||||
}
|
||||
|
||||
location / {
|
||||
index index.html;
|
||||
}
|
||||
|
||||
16
onion.js
Normal file
16
onion.js
Normal file
@@ -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;
|
||||
77
package.json
77
package.json
@@ -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,7 +1,4 @@
|
||||
relays:
|
||||
- 'wss://nostr-pub.wellorder.net'
|
||||
- 'wss://relayer.fiatjaf.com'
|
||||
- 'wss://nostr.rocks'
|
||||
- 'wss://rsslay.fiatjaf.com'
|
||||
- 'wss://freedom-relay.herokuapp.com/ws'
|
||||
- 'wss://nostr-relay.freeberty.net'
|
||||
@@ -11,7 +8,6 @@ relays:
|
||||
- 'wss://nostr-relay.untethr.me'
|
||||
- 'wss://nostr.semisol.dev'
|
||||
- 'wss://nostr-pub.semisol.dev'
|
||||
- 'ws://jgqaglhautb4k6e6i2g34jakxiemqp6z4wynlirltuukgkft2xuglmqd.onion'
|
||||
- 'wss://nostr-verified.wellorder.net'
|
||||
- 'wss://nostr.drss.io'
|
||||
- 'wss://nostr.unknown.place'
|
||||
@@ -25,3 +21,7 @@ relays:
|
||||
- 'wss://nostr.ono.re'
|
||||
- 'wss://relay.grunch.dev'
|
||||
- 'wss://relay.cynsar.foundation'
|
||||
- 'wss://nostr-pub.wellorder.net'
|
||||
- 'wss://relayer.fiatjaf.com'
|
||||
- 'wss://nostr.rocks'
|
||||
- 'wss://nostr.sandwich.farm'
|
||||
24
src/App.vue
24
src/App.vue
@@ -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>
|
||||
|
||||
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
|
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>
|
||||
|
||||
39
src/components/RelayListComponent.vue
Normal file
39
src/components/RelayListComponent.vue
Normal file
@@ -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>
|
||||
38
src/components/RelaySingleComponent.vue
Normal file
38
src/components/RelaySingleComponent.vue
Normal file
@@ -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
src/index.js
Normal file
0
src/index.js
Normal file
4
src/main.js
Normal file
4
src/main.js
Normal file
@@ -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
src/utils/test-connection-clearnet.js
Normal file
0
src/utils/test-connection-clearnet.js
Normal file
0
src/utils/test-connection-onion.js
Normal file
0
src/utils/test-connection-onion.js
Normal file
25
vue.config.js
Normal file
25
vue.config.js
Normal file
@@ -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')
|
||||
}
|
||||
})
|
||||
6179
yarn-error.log
Normal file
6179
yarn-error.log
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user