commit 5135dd4874027c3dd0dfcaf61f38c7106c850465 Author: Barry Deen Date: Fri Sep 6 11:16:40 2024 -0400 init wot relay diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e5c869c --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +RELAY_NAME="utxo WoT relay" +RELAY_PUBKEY="e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb" +RELAY_DESCRIPTION="Only notes in utxo WoT" +DB_PATH="db" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89ee3e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +db/ +.env \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9774c02 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# WOT Relay + +WOT Relay is a Nostr relay specialized for managing a web of trust based on trusted public keys. This relay is designed to run on localhost, with events filtered by a defined trust network. + +## Prerequisites + +- **Go**: Ensure you have Go installed on your system. You can download it from [here](https://golang.org/dl/). + +## Setup Instructions + +Follow these steps to get the WOT Relay running on your local machine: + +### 1. Clone the repository + +```bash +git clone https://github.com/bitvora/wot-relay.git +cd wot-relay +``` + +### 2. Copy `.env.example` to `.env` + +You'll need to create an `.env` file based on the example provided in the repository. + +```bash +cp .env.example .env +``` + +### 3. Set your environment variables + +Open the `.env` file and set the necessary environment variables. Example variables include: + +```bash +RELAY_NAME="YourRelayName" +RELAY_PUBKEY="YourPublicKey" +RELAY_DESCRIPTION="Your relay description" +DB_PATH="/path/to/your/database" +``` + +### 4. Build the project + +Run the following command to build the relay: + +```bash +go build +``` + +### 5. Create a Systemd Service (optional) + +To have the relay run as a service, create a systemd unit file. Here's an example: + +1. Create the file: + +```bash +sudo nano /etc/systemd/system/wot-relay.service +``` + +2. Add the following contents: + +```ini +[Unit] +Description=WOT Relay Service +After=network.target + +[Service] +ExecStart=/path/to/wot-relay +WorkingDirectory=/path/to/wot-relay +Restart=always +EnvironmentFile=/path/to/.env + +[Install] +WantedBy=multi-user.target +``` + +Replace `/path/to/` with the actual paths where you cloned the repository and stored the `.env` file. + +3. Reload systemd to recognize the new service: + +```bash +sudo systemctl daemon-reload +``` + +4. Start the service: + +```bash +sudo systemctl start wot-relay +``` + +5. (Optional) Enable the service to start on boot: + +```bash +sudo systemctl enable wot-relay +``` + +### 6. Access the relay + +Once everything is set up, the relay will be running on `localhost:3334`. + +```bash +http://localhost:3334 +``` + +## License + +This project is licensed under the MIT License. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..620210d --- /dev/null +++ b/go.mod @@ -0,0 +1,36 @@ +module github.com/bitvora/wot-relay + +go 1.23.0 + +toolchain go1.23.1 + +require ( + github.com/PowerDNS/lmdb-go v1.9.2 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/fasthttp/websocket v1.5.7 // indirect + github.com/fiatjaf/eventstore v0.8.1 // indirect + github.com/fiatjaf/khatru v0.8.0 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/gobwas/ws v1.3.1 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.17.8 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/nbd-wtf/go-nostr v0.34.14 // indirect + github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect + github.com/rs/cors v1.7.0 // indirect + github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect + github.com/tidwall/gjson v1.17.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/net v0.18.0 // indirect + golang.org/x/sys v0.20.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3249bc3 --- /dev/null +++ b/go.sum @@ -0,0 +1,71 @@ +github.com/PowerDNS/lmdb-go v1.9.2 h1:Cmgerh9y3ZKBZGz1irxSShhfmFyRUh+Zdk4cZk7ZJvU= +github.com/PowerDNS/lmdb-go v1.9.2/go.mod h1:TE0l+EZK8Z1B4dx070ZxkWTlp8RG1mjN0/+FkFRQMtU= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KHau4= +github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU= +github.com/fiatjaf/eventstore v0.5.1 h1:tTh+JYP0RME51VY2QB2Gvtzj6QTaZAnSVhgZtrYqY2A= +github.com/fiatjaf/eventstore v0.5.1/go.mod h1:r5yCFmrVNT2b1xUOuMnDVS3xPGh97y8IgTcLyY2rYP8= +github.com/fiatjaf/eventstore v0.8.1 h1:51LchQNy0Hpb0YQHwqYR5pKBpfDs/KjySlWCbbz2pkc= +github.com/fiatjaf/eventstore v0.8.1/go.mod h1:bsp0Ibv0CIcVuFcoM2AEerMWmXRhF8uWXMf+dClhuow= +github.com/fiatjaf/khatru v0.8.0 h1:hofUi4qbSqkJiKD4rC9EyNdi9obzBvp3ykJOBxuu/h8= +github.com/fiatjaf/khatru v0.8.0/go.mod h1:jRmqbbIbEH+y0unt3wMUBwqY/btVussqx5SmBoGhXtg= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I= +github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU= +github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/nbd-wtf/go-nostr v0.34.14 h1:o4n2LkuAtdIjNYJ23sFbcx68UXLnji4j8hYR1Sd2wgI= +github.com/nbd-wtf/go-nostr v0.34.14/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= +github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew= +github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= +github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= +github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ba5577b --- /dev/null +++ b/main.go @@ -0,0 +1,207 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "sync" + "time" + + "github.com/fiatjaf/eventstore/lmdb" + "github.com/fiatjaf/khatru" + "github.com/joho/godotenv" + "github.com/nbd-wtf/go-nostr" +) + +type Config struct { + RelayName string + RelayPubkey string + RelayDescription string + DBPath string +} + +var pool *nostr.SimplePool +var relays []string +var config Config +var trustNetwork []string +var mu sync.Mutex + +func main() { + fmt.Println("starting") + relay := khatru.NewRelay() + ctx := context.Background() + + config = LoadConfig() + + relay.Info.Name = config.RelayName + relay.Info.PubKey = config.RelayPubkey + relay.Info.Description = config.RelayDescription + trustNetwork = append(trustNetwork, config.RelayPubkey) + + db := lmdb.LMDBBackend{ + Path: getEnv("DB_PATH"), + } + if err := db.Init(); err != nil { + panic(err) + } + + go refreshTrustNetwork() + go archiveTrustedNotes(relay) + + relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent) + relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents) + relay.RejectEvent = append(relay.RejectEvent, func(ctx context.Context, event *nostr.Event) (bool, string) { + for _, pk := range trustNetwork { + if pk == event.PubKey { + return false, "" + } + } + return true, "you are not in the web of trust" + }) + + relays = []string{ + "wss://nos.lol", + "wss://nostr.mom", + "wss://purplepag.es", + "wss://purplerelay.com", + "wss://relay.damus.io", + "wss://relay.mostr.pub", + "wss://relay.nos.social", + "wss://relay.nostr.band", + "wss://relay.snort.social", + "wss://relayable.org", + "wss://pyramid.fiatjaf.com", + "wss://relay.primal.net", + "wss://relay.nostr.bg", + "wss://no.str.cr", + "wss://nostr21.com", + "wss://nostrue.com", + "wss://relay.siamstr.com", + "wss://nostrarchives.com", + } + + pool = nostr.NewSimplePool(ctx) + + fmt.Println("running on :3334") + http.ListenAndServe(":3334", relay) +} + +func LoadConfig() Config { + err := godotenv.Load(".env") + if err != nil { + log.Fatalf("Error loading .env file") + } + + config := Config{ + RelayName: getEnv("RELAY_NAME"), + RelayPubkey: getEnv("RELAY_PUBKEY"), + RelayDescription: getEnv("RELAY_DESCRIPTION"), + DBPath: getEnv("DB_PATH"), + } + + 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 refreshTrustNetwork() []string { + ctx := context.Background() + ticker := time.NewTicker(1 * time.Minute) + for range ticker.C { + + timeoutCtx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + + filters := []nostr.Filter{{ + Authors: []string{config.RelayPubkey}, + Kinds: []int{nostr.KindContactList}, + }} + + for ev := range pool.SubManyEose(timeoutCtx, relays, filters) { + for _, contact := range ev.Event.Tags.GetAll([]string{"p"}) { + appendPubkey(contact[1]) + } + } + + fmt.Println("trust network size:", len(trustNetwork)) + + chunks := make([][]string, 0) + for i := 0; i < len(trustNetwork); i += 100 { + end := i + 100 + if end > len(trustNetwork) { + end = len(trustNetwork) + } + chunks = append(chunks, trustNetwork[i:end]) + } + + for _, chunk := range chunks { + threeTimeoutCtx, tenCancel := context.WithTimeout(ctx, 3*time.Second) + defer tenCancel() + + filters = []nostr.Filter{{ + Authors: chunk, + Kinds: []int{nostr.KindContactList}, + }} + + for ev := range pool.SubManyEose(threeTimeoutCtx, relays, filters) { + for _, contact := range ev.Event.Tags.GetAll([]string{"p"}) { + if len(contact) > 1 { + appendPubkey(contact[1]) + } else { + fmt.Println("Skipping malformed tag: ", contact) + } + } + } + } + + fmt.Println("trust network size:", len(trustNetwork)) + } + + return trustNetwork +} + +func appendPubkey(pubkey string) { + mu.Lock() + defer mu.Unlock() + + for _, pk := range trustNetwork { + if pk == pubkey { + return + } + } + trustNetwork = append(trustNetwork, pubkey) +} + +func archiveTrustedNotes(relay *khatru.Relay) { + ctx := context.Background() + 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, + }, + }} + + for ev := range pool.SubMany(ctx, relays, filters) { + for _, trustedPubkey := range trustNetwork { + if ev.Event.PubKey == trustedPubkey { + relay.AddEvent(ctx, ev.Event) + } + } + } +}