wot-relay/main.go

352 lines
8.5 KiB
Go
Raw Normal View History

2024-09-06 11:16:40 -04:00
package main
import (
"context"
"fmt"
2024-09-06 19:25:40 -04:00
"html/template"
"io"
2024-09-06 11:16:40 -04:00
"log"
"net/http"
"os"
"strconv"
2024-09-06 11:16:40 -04:00
"sync"
"time"
2024-09-07 13:21:22 -04:00
"github.com/cespare/xxhash"
2024-09-06 11:16:40 -04:00
"github.com/fiatjaf/khatru"
"github.com/fiatjaf/khatru/policies"
2024-09-07 13:21:22 -04:00
"github.com/greatroar/blobloom"
2024-09-06 11:16:40 -04:00
"github.com/joho/godotenv"
"github.com/nbd-wtf/go-nostr"
)
type Config struct {
RelayName string
RelayPubkey string
RelayDescription string
DBPath string
2024-09-06 19:25:40 -04:00
RelayURL string
2024-09-06 19:33:29 -04:00
IndexPath string
StaticPath string
RefreshInterval int
2024-09-06 11:16:40 -04:00
}
var pool *nostr.SimplePool
2024-09-06 11:16:40 -04:00
var relays []string
var config Config
var trustNetwork []string
var mu sync.Mutex
var trustNetworkFilter *blobloom.Filter
var trustNetworkFilterMu sync.Mutex
var seedRelays []string
var booted bool
var oneHopNetwork []string
2024-09-06 11:16:40 -04:00
func main() {
nostr.InfoLogger = log.New(io.Discard, "", 0)
booted = false
green := "\033[32m"
reset := "\033[0m"
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")
2024-09-06 11:16:40 -04:00
relay := khatru.NewRelay()
ctx := context.Background()
pool = nostr.NewSimplePool(ctx)
2024-09-06 11:16:40 -04:00
config = LoadConfig()
relay.Info.Name = config.RelayName
relay.Info.PubKey = config.RelayPubkey
relay.Info.Description = config.RelayDescription
appendPubkey(config.RelayPubkey)
2024-09-06 11:16:40 -04:00
2024-09-07 22:01:13 -03:00
db := getDB()
2024-09-06 11:16:40 -04:00
if err := db.Init(); err != nil {
panic(err)
}
policies.ApplySaneDefaults(relay)
2024-09-06 11:16:40 -04:00
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
2024-09-06 11:16:40 -04:00
relay.RejectEvent = append(relay.RejectEvent, func(ctx context.Context, event *nostr.Event) (bool, string) {
2024-09-06 17:39:39 -04:00
for _, pk := range trustNetwork {
2024-09-06 11:16:40 -04:00
if pk == event.PubKey {
return false, ""
}
}
return true, "you are not in the web of trust"
})
mu.Lock()
seedRelays = []string{
2024-09-06 11:16:40 -04:00
"wss://nos.lol",
"wss://nostr.mom",
"wss://purplepag.es",
"wss://purplerelay.com",
"wss://relay.damus.io",
"wss://relay.nostr.band",
"wss://relay.snort.social",
"wss://relayable.org",
"wss://relay.primal.net",
"wss://relay.nostr.bg",
"wss://no.str.cr",
"wss://nostr21.com",
"wss://nostrue.com",
"wss://relay.siamstr.com",
}
mu.Unlock()
2024-09-06 11:16:40 -04:00
go refreshTrustNetwork(relay, ctx)
2024-09-06 11:16:40 -04:00
2024-09-06 19:25:40 -04:00
mux := relay.Router()
static := http.FileServer(http.Dir(config.StaticPath))
mux.Handle("GET /static/", http.StripPrefix("/static/", static))
mux.Handle("GET /favicon.ico", http.StripPrefix("/", static))
2024-09-06 19:25:40 -04:00
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles(os.Getenv("INDEX_PATH")))
data := struct {
RelayName string
RelayPubkey string
RelayDescription string
RelayURL string
}{
RelayName: config.RelayName,
RelayPubkey: config.RelayPubkey,
RelayDescription: config.RelayDescription,
RelayURL: config.RelayURL,
}
err := tmpl.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
log.Println("🎉 relay running on port :3334")
2024-09-06 19:38:39 -04:00
err := http.ListenAndServe(":3334", relay)
if err != nil {
log.Fatal(err)
}
2024-09-06 11:16:40 -04:00
}
func LoadConfig() Config {
godotenv.Load(".env")
2024-09-06 11:16:40 -04:00
if os.Getenv("REFRESH_INTERVAL_HOURS") == "" {
2024-09-11 12:55:40 -04:00
os.Setenv("REFRESH_INTERVAL_HOURS", "3")
}
refreshInterval, _ := strconv.Atoi(os.Getenv("REFRESH_INTERVAL_HOURS"))
log.Println("🔄 refresh interval set to", refreshInterval, "hours")
2024-09-06 11:16:40 -04:00
config := Config{
RelayName: getEnv("RELAY_NAME"),
RelayPubkey: getEnv("RELAY_PUBKEY"),
RelayDescription: getEnv("RELAY_DESCRIPTION"),
DBPath: getEnv("DB_PATH"),
2024-09-06 19:25:40 -04:00
RelayURL: getEnv("RELAY_URL"),
2024-09-06 19:33:29 -04:00
IndexPath: getEnv("INDEX_PATH"),
StaticPath: getEnv("STATIC_PATH"),
RefreshInterval: refreshInterval,
2024-09-06 11:16:40 -04:00
}
return config
}
func getEnv(key string) string {
value, exists := os.LookupEnv(key)
if !exists {
log.Fatalf("Environment variable %s not set", key)
}
return value
}
func updateTrustNetworkFilter() {
trustNetworkFilterMu.Lock()
defer trustNetworkFilterMu.Unlock()
nKeys := uint64(len(trustNetwork))
log.Println("🌐 updating trust network filter with", nKeys, "keys")
trustNetworkFilter = blobloom.NewOptimized(blobloom.Config{
Capacity: nKeys,
FPRate: 1e-4,
})
for _, trustedPubkey := range trustNetwork {
trustNetworkFilter.Add(xxhash.Sum64([]byte(trustedPubkey)))
}
}
func refreshTrustNetwork(relay *khatru.Relay, ctx context.Context) {
2024-09-06 11:16:40 -04:00
runTrustNetworkRefresh := func() {
2024-09-11 12:55:40 -04:00
timeoutCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
2024-09-06 11:16:40 -04:00
defer cancel()
filters := []nostr.Filter{{
Authors: []string{config.RelayPubkey},
Kinds: []int{nostr.KindContactList},
}}
log.Println("🔍 fetching owner's follows")
for ev := range pool.SubManyEose(timeoutCtx, seedRelays, filters) {
2024-09-06 11:16:40 -04:00
for _, contact := range ev.Event.Tags.GetAll([]string{"p"}) {
appendOneHopNetwork(contact[1])
2024-09-06 11:16:40 -04:00
}
}
log.Println("🌐 building web of trust graph")
for i := 0; i < len(oneHopNetwork); i += 100 {
2024-09-11 12:55:40 -04:00
timeout, cancel := context.WithTimeout(ctx, 4*time.Second)
defer cancel()
end := i + 100
if end > len(oneHopNetwork) {
end = len(oneHopNetwork)
}
2024-09-06 11:16:40 -04:00
filters = []nostr.Filter{{
Authors: oneHopNetwork[i:end],
Kinds: []int{nostr.KindContactList, nostr.KindRelayListMetadata, nostr.KindProfileMetadata},
2024-09-06 11:16:40 -04:00
}}
for ev := range pool.SubManyEose(timeout, seedRelays, filters) {
2024-09-06 11:16:40 -04:00
for _, contact := range ev.Event.Tags.GetAll([]string{"p"}) {
if len(contact) > 1 {
appendPubkey(contact[1])
}
}
for _, relay := range ev.Event.Tags.GetAll([]string{"r"}) {
appendRelay(relay[1])
}
if ev.Event.Kind == nostr.KindProfileMetadata {
relay.AddEvent(ctx, ev.Event)
}
2024-09-06 11:16:40 -04:00
}
}
log.Println("🫂 network size:", len(trustNetwork))
log.Println("🔗 relays discovered:", len(relays))
}
for {
runTrustNetworkRefresh()
updateTrustNetworkFilter()
archiveTrustedNotes(relay, ctx)
2024-09-06 11:16:40 -04:00
}
}
func appendRelay(relay string) {
mu.Lock()
defer mu.Unlock()
2024-09-06 13:24:33 -04:00
for _, r := range relays {
if r == relay {
return
2024-09-06 13:24:33 -04:00
}
}
relays = append(relays, relay)
2024-09-06 13:24:33 -04:00
}
2024-09-06 11:16:40 -04:00
func appendPubkey(pubkey string) {
mu.Lock()
defer mu.Unlock()
for _, pk := range trustNetwork {
if pk == pubkey {
return
}
}
if len(pubkey) != 64 {
return
}
2024-09-06 11:16:40 -04:00
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) {
timeout, cancel := context.WithTimeout(ctx, time.Duration(config.RefreshInterval)*time.Hour)
defer cancel()
filters := []nostr.Filter{{
Kinds: []int{
nostr.KindArticle,
nostr.KindDeletion,
nostr.KindContactList,
nostr.KindEncryptedDirectMessage,
nostr.KindMuteList,
nostr.KindReaction,
nostr.KindRelayListMetadata,
nostr.KindRepost,
nostr.KindZapRequest,
nostr.KindZap,
nostr.KindTextNote,
},
}}
log.Println("📦 archiving trusted notes...")
var trustedNotes uint64
var untrustedNotes uint64
trustNetworkFilterMu.Lock()
2024-09-11 12:55:40 -04:00
defer trustNetworkFilterMu.Unlock()
2024-09-11 13:02:07 -04:00
for ev := range pool.SubMany(timeout, seedRelays, filters) {
2024-09-11 12:55:40 -04:00
select {
case <-ctx.Done():
log.Println("⏰ Archive process terminated due to timeout")
return
default:
if trustNetworkFilter.Has(xxhash.Sum64([]byte(ev.Event.PubKey))) {
if len(ev.Event.Tags) > 3000 {
continue
}
relay.AddEvent(ctx, ev.Event)
log.Println("📦 archived note: ", ev.Event.ID)
trustedNotes++
} else {
untrustedNotes++
}
2024-09-06 11:16:40 -04:00
}
}
2024-09-11 12:55:40 -04:00
log.Println("📦 archived", trustedNotes, "trusted notes and discarded", untrustedNotes, "untrusted notes")
2024-09-11 13:02:07 -04:00
return
2024-09-06 11:16:40 -04:00
}