diff --git a/.env.example b/.env.example index 6eeee36..f18144d 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,17 @@ +# Relay Metadata RELAY_NAME="utxo WoT relay" -RELAY_PUBKEY="e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb" +RELAY_PUBKEY="e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb" # not your npub! RELAY_DESCRIPTION="Only notes in utxo WoT" RELAY_URL="wss://wot.utxo.one" + +# where we should store the database DB_PATH="db" + +# where we should store the index.html and static files INDEX_PATH="templates/index.html" STATIC_PATH="templates/static" + +# relay behavior + +# how often to refresh the relay's view of the WoT in HOURS +REFRESH_INTERVAL_HOURS=24 \ No newline at end of file diff --git a/README.md b/README.md index 46a4aa8..c0dfc2e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# WOT Relay +# WoT Relay -WOT Relay is a Nostr relay that saves all the notes that people you follow, and people they follow are posting. +WOT Relay is a Nostr relay that saves all the notes that people you follow, and people they follow are posting. It's built on the [Khatru](https://khatru.nostr.technology) framework. ## Prerequisites @@ -37,6 +37,7 @@ RELAY_DESCRIPTION="Your relay description" DB_PATH="/home/ubuntu/wot-relay/db" # any path you would like the database to be saved. INDEX_PATH="/home/ubuntu/wot-relay/templates/index.html" # path to the index.html file STATIC_PATH="/home/ubuntu/wot-relay/templates/static" # path to the static folder +REFRESH_INTERVAL=24 # interval in hours to refresh the web of trust ``` ### 4. Build the project @@ -65,10 +66,9 @@ Description=WOT Relay Service After=network.target [Service] -ExecStart=/path/to/wot-relay -WorkingDirectory=/path/to/wot-relay +ExecStart=/home/ubuntu/wot-relay/wot-relay #change this to your path +WorkingDirectory=/home/ubuntu/wot-relay #change this to your path Restart=always -EnvironmentFile=/path/to/.env [Install] WantedBy=multi-user.target @@ -94,7 +94,63 @@ sudo systemctl start wot-relay sudo systemctl enable wot-relay ``` -### 6. Start the Project with Docker Compose (optional) +#### Permission Issues on Some Systems + +the relay may not have permissions to read and write to the database. To fix this, you can change the permissions of the database folder: + +```bash +sudo chmod -R 777 /path/to/db +``` + +### 6. Serving over nginx (optional) + +You can serve the relay over nginx by adding the following configuration to your nginx configuration file: + +```nginx +server { + listen 80; + server_name yourdomain.com; + + location / { + proxy_pass http://localhost:3334; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +Replace `yourdomain.com` with your actual domain name. + +After adding the configuration, restart nginx: + +```bash +sudo systemctl restart nginx +``` + +### 7. Install Certbot (optional) + +If you want to serve the relay over HTTPS, you can use Certbot to generate an SSL certificate. + +```bash +sudo apt-get update +sudo apt-get install certbot python3-certbot-nginx +``` + +After installing Certbot, run the following command to generate an SSL certificate: + +```bash +sudo certbot --nginx +``` + +Follow the instructions to generate the certificate. + +### 8. Access the relay + +Once everything is set up, the relay will be running on `localhost:3334` or your domain name if you set up nginx. + +## Start the Project with Docker Compose To start the project using Docker Compose, follow these steps: diff --git a/main.go b/main.go index 83b376a..81c5899 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "log" "net/http" "os" + "strconv" "sync" "time" @@ -26,6 +27,7 @@ type Config struct { RelayURL string IndexPath string StaticPath string + RefreshInterval int } var pool *nostr.SimplePool @@ -36,19 +38,27 @@ var mu sync.Mutex var trustNetworkFilter *blobloom.Filter var trustNetworkFilterMu sync.Mutex var seedRelays []string +var booted bool +var oneHopNetwork []string func main() { + booted = false green := "\033[32m" reset := "\033[0m" - art := ` - __ __ ___. _____ ___________ __ -/ \ / \ ____\_ |__ _____/ ____\ \__ ___/______ __ __ _______/ |_ -\ \/\/ // __ \| __ \ / _ \ __\ | | \_ __ \ | \/ ___/\ __\ - \ /\ ___/| \_\ \ ( <_> ) | | | | | \/ | /\___ \ | | - \__/\ / \___ >___ / \____/|__| |____| |__| |____//____ > |__| - \/ \/ \/ \/ - ` + art := ` +888 888 88888888888 8888888b. 888 +888 o 888 888 888 Y88b 888 +888 d8b 888 888 888 888 888 +888 d888b 888 .d88b. 888 888 d88P .d88b. 888 8888b. 888 888 +888d88888b888 d88""88b 888 8888888P" d8P Y8b 888 "88b 888 888 +88888P Y88888 888 888 888 888 T88b 88888888 888 .d888888 888 888 +8888P Y8888 Y88..88P 888 888 T88b Y8b. 888 888 888 Y88b 888 +888P Y888 "Y88P" 888 888 T88b "Y8888 888 "Y888888 "Y88888 + 888 + Y8b d88P + powered by: khatru "Y88P" + ` fmt.Println(green + art + reset) log.Println("🚀 booting up web of trust relay") @@ -104,7 +114,6 @@ func main() { mu.Unlock() go refreshTrustNetwork(relay, ctx) - go archiveTrustedNotes(relay, ctx) mux := relay.Router() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { @@ -138,6 +147,13 @@ func main() { func LoadConfig() Config { godotenv.Load(".env") + if os.Getenv("REFRESH_INTERVAL_HOURS") == "" { + os.Setenv("REFRESH_INTERVAL_HOURS", "24") + } + + refreshInterval, _ := strconv.Atoi(os.Getenv("REFRESH_INTERVAL_HOURS")) + log.Println("🔄 refresh interval set to", refreshInterval, "hours") + config := Config{ RelayName: getEnv("RELAY_NAME"), RelayPubkey: getEnv("RELAY_PUBKEY"), @@ -146,6 +162,7 @@ func LoadConfig() Config { RelayURL: getEnv("RELAY_URL"), IndexPath: getEnv("INDEX_PATH"), StaticPath: getEnv("STATIC_PATH"), + RefreshInterval: refreshInterval, } return config @@ -174,10 +191,10 @@ func updateTrustNetworkFilter() { } } -func refreshTrustNetwork(relay *khatru.Relay, ctx context.Context) []string { +func refreshTrustNetwork(relay *khatru.Relay, ctx context.Context) { runTrustNetworkRefresh := func() { - timeoutCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + timeoutCtx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() filters := []nostr.Filter{{ @@ -188,25 +205,22 @@ func refreshTrustNetwork(relay *khatru.Relay, ctx context.Context) []string { log.Println("🔍 fetching owner's follows") for ev := range pool.SubManyEose(timeoutCtx, seedRelays, filters) { for _, contact := range ev.Event.Tags.GetAll([]string{"p"}) { - appendPubkey(contact[1]) + appendOneHopNetwork(contact[1]) } } - follows := make([]string, len(trustNetwork)) - copy(follows, trustNetwork) - log.Println("🌐 building web of trust graph") - for i := 0; i < len(follows); i += 200 { + for i := 0; i < len(oneHopNetwork); i += 100 { timeout, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() - end := i + 200 - if end > len(follows) { - end = len(follows) + end := i + 100 + if end > len(oneHopNetwork) { + end = len(oneHopNetwork) } filters = []nostr.Filter{{ - Authors: follows[i:end], + Authors: oneHopNetwork[i:end], Kinds: []int{nostr.KindContactList, nostr.KindRelayListMetadata, nostr.KindProfileMetadata}, }} @@ -231,18 +245,11 @@ func refreshTrustNetwork(relay *khatru.Relay, ctx context.Context) []string { log.Println("🔗 relays discovered:", len(relays)) } - runTrustNetworkRefresh() - updateTrustNetworkFilter() - - ticker := time.NewTicker(24 * time.Hour) - defer ticker.Stop() - - for range ticker.C { + for { runTrustNetworkRefresh() updateTrustNetworkFilter() + archiveTrustedNotes(relay, ctx) } - - return trustNetwork } func appendRelay(relay string) { @@ -274,10 +281,25 @@ func appendPubkey(pubkey string) { trustNetwork = append(trustNetwork, pubkey) } +func appendOneHopNetwork(pubkey string) { + mu.Lock() + defer mu.Unlock() + + for _, pk := range oneHopNetwork { + if pk == pubkey { + return + } + } + + if len(pubkey) != 64 { + return + } + + oneHopNetwork = append(oneHopNetwork, pubkey) +} + func archiveTrustedNotes(relay *khatru.Relay, ctx context.Context) { - log.Println("⏳ waiting for trust network to be populated") - time.Sleep(1 * time.Minute) - timeout, cancel := context.WithTimeout(ctx, 24*time.Hour) + timeout, cancel := context.WithTimeout(ctx, time.Duration(config.RefreshInterval)*time.Hour) defer cancel() filters := []nostr.Filter{{ @@ -297,7 +319,8 @@ func archiveTrustedNotes(relay *khatru.Relay, ctx context.Context) { }} log.Println("📦 archiving trusted notes...") - var i int64 + var trustedNotes uint64 + var untrustedNotes uint64 trustNetworkFilterMu.Lock() for ev := range pool.SubMany(timeout, seedRelays, filters) { if trustNetworkFilter.Has(xxhash.Sum64([]byte(ev.Event.PubKey))) { @@ -305,9 +328,11 @@ func archiveTrustedNotes(relay *khatru.Relay, ctx context.Context) { continue } relay.AddEvent(ctx, ev.Event) - i++ + trustedNotes++ + } else { + untrustedNotes++ } } trustNetworkFilterMu.Unlock() - fmt.Println("📦 archived", i, "trusted notes") + log.Println("📦 archived", trustedNotes, "trusted notes and discarded", untrustedNotes, "untrusted notes") }