wot-relay/main.go

390 lines
9.9 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
"time"
"github.com/fiatjaf/khatru"
"github.com/fiatjaf/khatru/policies"
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
MinimumFollowers 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 seedRelays []string
var booted bool
var oneHopNetwork []string
2024-09-12 14:16:56 -04:00
var trustNetworkMap map[string]bool
var pubkeyFollowerCount = make(map[string]int)
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)
}
2024-09-12 10:04:06 -04:00
relay.RejectEvent = append(relay.RejectEvent,
policies.RejectEventsWithBase64Media,
2024-09-12 16:04:46 -04:00
policies.EventIPRateLimiter(5, time.Minute*2, 30),
2024-09-12 10:04:06 -04:00
)
relay.RejectFilter = append(relay.RejectFilter,
policies.NoEmptyFilters,
policies.NoComplexFilters,
2024-09-12 10:34:15 -04:00
// policies.FilterIPRateLimiter(50, time.Minute, 250),
2024-09-12 10:04:06 -04:00
)
relay.RejectConnection = append(relay.RejectConnection,
2024-09-12 16:04:46 -04:00
policies.ConnectionRateLimiter(10, time.Minute*1, 30),
2024-09-12 10:04:06 -04:00
)
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"
})
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",
}
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")
if os.Getenv("MINIMUM_FOLLOWERS") == "" {
os.Setenv("MINIMUM_FOLLOWERS", "1")
}
minimumFollowers, _ := strconv.Atoi(os.Getenv("MINIMUM_FOLLOWERS"))
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,
MinimumFollowers: minimumFollowers,
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() {
2024-09-12 14:16:56 -04:00
trustNetworkMap = make(map[string]bool)
log.Println("🌐 updating trust network map")
for pubkey, count := range pubkeyFollowerCount {
if count >= config.MinimumFollowers {
trustNetworkMap[pubkey] = true
appendPubkey(pubkey)
}
}
log.Println("🌐 trust network map updated with", len(trustNetwork), "keys")
}
2024-09-12 11:06:57 -04:00
func refreshProfiles(ctx context.Context, relay *khatru.Relay) {
for i := 0; i < len(trustNetwork); i += 200 {
timeout, cancel := context.WithTimeout(ctx, 4*time.Second)
defer cancel()
end := i + 200
if end > len(trustNetwork) {
end = len(trustNetwork)
}
filters := []nostr.Filter{{
Authors: trustNetwork[i:end],
Kinds: []int{nostr.KindProfileMetadata},
}}
for ev := range pool.SubManyEose(timeout, seedRelays, filters) {
relay.AddEvent(ctx, ev.Event)
}
}
2024-09-12 12:40:45 -04:00
log.Println("👤 profiles refreshed: ", len(trustNetwork))
2024-09-12 11:06:57 -04:00
}
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"}) {
pubkeyFollowerCount[contact[1]]++ // Increment follower count for the pubkey
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"}) {
2024-09-12 15:41:00 -04:00
if len(contact) > 1 {
pubkeyFollowerCount[contact[1]]++ // Increment follower count for the pubkey
}
2024-09-06 11:16:40 -04:00
}
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("🫂 total network size:", len(pubkeyFollowerCount))
log.Println("🔗 relays discovered:", len(relays))
}
for {
runTrustNetworkRefresh()
updateTrustNetworkFilter()
archiveTrustedNotes(relay, ctx)
2024-09-06 11:16:40 -04:00
}
}
func appendRelay(relay string) {
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) {
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) {
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 := time.After(time.Duration(config.RefreshInterval) * time.Hour)
2024-09-12 11:06:57 -04:00
go refreshProfiles(ctx, relay)
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
eventChan := pool.SubMany(ctx, seedRelays, filters)
for {
2024-09-11 12:55:40 -04:00
select {
case <-timeout:
2024-09-11 12:55:40 -04:00
log.Println("⏰ Archive process terminated due to timeout")
log.Println("📦 archived", trustedNotes, "trusted notes and discarded", untrustedNotes, "untrusted notes")
2024-09-11 12:55:40 -04:00
return
case <-ctx.Done():
log.Println("⏰ Archive process terminated due to context cancellation")
log.Println("📦 archived", trustedNotes, "trusted notes and discarded", untrustedNotes, "untrusted notes")
return
case ev, ok := <-eventChan:
if !ok {
log.Println("📦 subscription channel closed")
log.Println("📦 archived", trustedNotes, "trusted notes and discarded", untrustedNotes, "untrusted notes")
return
}
2024-09-12 14:16:56 -04:00
if trustNetworkMap[ev.Event.PubKey] {
2024-09-11 12:55:40 -04:00
if len(ev.Event.Tags) > 3000 {
continue
}
relay.AddEvent(ctx, ev.Event)
trustedNotes++
//log.Println("📦 archived note: ", ev.Event.ID)
2024-09-11 12:55:40 -04:00
} else {
untrustedNotes++
}
2024-09-06 11:16:40 -04:00
}
}
}