├── .dockerignore ├── .gitignore ├── tor ├── data │ └── .gitignore └── torrc ├── templates ├── static │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-150x150.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-256x256.png │ ├── browserconfig.xml │ └── safari-pinned-tab.svg └── index.html ├── docker-compose.yml ├── docker-compose.arm64.yml ├── docker-compose.tor.yml ├── Dockerfile ├── arm64.Dockerfile ├── .github └── workflows │ └── docker-build.yml ├── Dockerfile-optimized ├── .env.example ├── go.mod ├── README.md ├── go.sum └── main.go /.dockerignore: -------------------------------------------------------------------------------- 1 | db 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | db/ 2 | .env 3 | wot-relay 4 | -------------------------------------------------------------------------------- /tor/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tor/torrc: -------------------------------------------------------------------------------- 1 | HiddenServiceDir /var/lib/tor/relay 2 | HiddenServicePort 80 relay:3334 3 | -------------------------------------------------------------------------------- /templates/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvora/wot-relay/HEAD/templates/static/favicon.ico -------------------------------------------------------------------------------- /templates/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvora/wot-relay/HEAD/templates/static/favicon-16x16.png -------------------------------------------------------------------------------- /templates/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvora/wot-relay/HEAD/templates/static/favicon-32x32.png -------------------------------------------------------------------------------- /templates/static/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvora/wot-relay/HEAD/templates/static/mstile-150x150.png -------------------------------------------------------------------------------- /templates/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvora/wot-relay/HEAD/templates/static/apple-touch-icon.png -------------------------------------------------------------------------------- /templates/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvora/wot-relay/HEAD/templates/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /templates/static/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvora/wot-relay/HEAD/templates/static/android-chrome-256x256.png -------------------------------------------------------------------------------- /templates/static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | relay: 3 | container_name: wot-relay 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | env_file: 8 | - .env 9 | volumes: 10 | - "./db:/app/db" 11 | - "./templates/index.html:${INDEX_PATH}" 12 | - "./templates/static:${STATIC_PATH}" 13 | ports: 14 | - "3334:3334" 15 | restart: unless-stopped 16 | init: true 17 | -------------------------------------------------------------------------------- /docker-compose.arm64.yml: -------------------------------------------------------------------------------- 1 | services: 2 | relay: 3 | container_name: wot-relay 4 | build: 5 | context: . 6 | dockerfile: arm64.Dockerfile 7 | env_file: 8 | - .env 9 | volumes: 10 | - "./db:/app/db" 11 | - "./templates/index.html:${INDEX_PATH}" 12 | - "./templates/static:${STATIC_PATH}" 13 | ports: 14 | - "3334:3334" 15 | restart: unless-stopped 16 | init: true 17 | -------------------------------------------------------------------------------- /templates/static/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docker-compose.tor.yml: -------------------------------------------------------------------------------- 1 | services: 2 | relay: 3 | container_name: wot-relay 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | env_file: 8 | - .env 9 | volumes: 10 | - "./db:/app/db" 11 | - "./templates/index.html:${INDEX_PATH}" 12 | - "./templates/static:${STATIC_PATH}" 13 | ports: 14 | - "3334" 15 | restart: unless-stopped 16 | init: true 17 | 18 | tor: 19 | image: lncm/tor:0.4.7.9@sha256:86c2fe9d9099e6376798979110b8b9a3ee5d8adec27289ac4a5ee892514ffe92 20 | container_name: wot-relay-tor 21 | depends_on: 22 | - relay 23 | volumes: 24 | - ./tor/torrc:/etc/tor/torrc 25 | - ./tor/data:/var/lib/tor 26 | restart: on-failure 27 | stop_grace_period: 10m30s 28 | init: true -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Golang image based on Debian Bookworm 2 | FROM golang:bookworm 3 | 4 | # Set the working directory within the container 5 | WORKDIR /app 6 | 7 | # Copy go.mod and go.sum files 8 | COPY go.mod go.sum ./ 9 | 10 | # Download dependencies 11 | RUN go mod download 12 | 13 | # Copy the rest of the application source code 14 | COPY . . 15 | 16 | # Set fixed environment variables 17 | ENV DB_PATH="db" 18 | ENV INDEX_PATH="templates/index.html" 19 | ENV STATIC_PATH="templates/static" 20 | 21 | # touch a .env (https://github.com/bitvora/wot-relay/pull/4) 22 | RUN touch .env 23 | 24 | # Build the Go application 25 | RUN go build -o main . 26 | 27 | # Expose the port that the application will run on 28 | EXPOSE 3334 29 | 30 | # Set the command to run the executable 31 | CMD ["./main"] 32 | -------------------------------------------------------------------------------- /arm64.Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Golang image based on Debian Bookworm 2 | FROM golang:bookworm 3 | 4 | # Set the working directory within the container 5 | WORKDIR /app 6 | 7 | # Copy go.mod and go.sum files 8 | COPY go.mod go.sum ./ 9 | 10 | # Download dependencies 11 | RUN go mod download 12 | 13 | # Copy the rest of the application source code 14 | COPY . . 15 | 16 | # Set fixed environment variables 17 | ENV DB_PATH="db" 18 | ENV INDEX_PATH="templates/index.html" 19 | ENV STATIC_PATH="templates/static" 20 | 21 | # touch a .env (https://github.com/bitvora/wot-relay/pull/4) 22 | RUN touch .env 23 | 24 | # Build the Go application 25 | RUN go build -tags badger -o main . 26 | 27 | # Expose the port that the application will run on 28 | EXPOSE 3334 29 | 30 | # Set the command to run the executable 31 | CMD ["./main"] 32 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build and Publish 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | jobs: 10 | build-and-publish: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v2 18 | 19 | - name: Login to GitHub Container Registry 20 | uses: docker/login-action@v2 21 | with: 22 | registry: ghcr.io 23 | username: ${{ github.actor }} 24 | password: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | - name: Build and push Docker image 27 | uses: docker/build-push-action@v4 28 | with: 29 | context: . 30 | file: ./Dockerfile-optimized 31 | push: ${{ github.event_name != 'pull_request' }} 32 | tags: | 33 | ghcr.io/${{ github.repository }}:latest 34 | ghcr.io/${{ github.repository }}:${{ github.sha }} 35 | cache-from: type=gha 36 | cache-to: type=gha,mode=max 37 | -------------------------------------------------------------------------------- /Dockerfile-optimized: -------------------------------------------------------------------------------- 1 | # Stage 1: Build the Go application 2 | FROM golang:bookworm AS builder 3 | 4 | # Set the working directory within the container 5 | WORKDIR /app 6 | 7 | # Copy go.mod and go.sum files 8 | COPY go.mod go.sum ./ 9 | 10 | # Download dependencies 11 | RUN go mod download 12 | 13 | # Copy the rest of the application source code 14 | COPY . . 15 | 16 | # Set fixed environment variables for build 17 | ENV DB_PATH="db" 18 | ENV INDEX_PATH="templates/index.html" 19 | ENV STATIC_PATH="templates/static" 20 | 21 | # touch a .env 22 | RUN touch .env 23 | 24 | # Build the Go application 25 | RUN go build -o main . 26 | 27 | # Stage 2: Create a minimal image to run the Go application 28 | FROM debian:bookworm-slim 29 | 30 | # Set the working directory within the container 31 | WORKDIR /app 32 | 33 | # Copy the Go binary from the builder stage 34 | COPY --from=builder /app/main /app/ 35 | 36 | # Copy any necessary files like templates, static assets, etc. 37 | COPY --from=builder /app/templates /app/templates 38 | COPY --from=builder /app/.env /app/ 39 | 40 | # Expose the port that the application will run on 41 | EXPOSE 3334 42 | 43 | # Set the command to run the executable 44 | CMD ["./main"] 45 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Relay Metadata 2 | RELAY_NAME="utxo WoT relay" 3 | RELAY_PUBKEY="e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb" # the owner's hexkey, not npub. Convert npub to hex here: https://nostrcheck.me/converter/ 4 | RELAY_DESCRIPTION="Only notes in utxo WoT" 5 | RELAY_URL="wss://wot.utxo.one" 6 | RELAY_ICON="https://nostr.build/i/53866b44135a27d624e99c6165cabd76ac8f72797209700acb189fce75021f47.jpg" 7 | RELAY_CONTACT="https://utxo.one" 8 | 9 | # where we should store the database 10 | DB_PATH="db" 11 | 12 | # where we should store the index.html and static files 13 | INDEX_PATH="/mnt/dev/bitvora/wot-relay/templates/index.html" 14 | STATIC_PATH="/mnt/dev/bitvora/wot-relay/templates/static/" 15 | 16 | # relay behavior 17 | 18 | # how often to refresh the relay's view of the WoT in HOURS 19 | REFRESH_INTERVAL_HOURS=1 20 | MINIMUM_FOLLOWERS=5 21 | 22 | # archive all notes from everyone in your WoT from other relays 23 | ARCHIVAL_SYNC="FALSE" 24 | ARCHIVE_REACTIONS="FALSE" # optional, reactions take up a lot of space and compute 25 | 26 | # optional, certain note kinds older than this many days will be deleted 27 | MAX_AGE_DAYS=365 28 | 29 | # comma delimited list of pubkeys who follow bots and ruin the WoT 30 | IGNORE_FOLLOWS_LIST="" -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{.RelayName}} 9 | 10 | 19 | 20 | 21 |
22 | 23 |
24 | 25 |

26 | {{.RelayName}} 27 |

28 | 29 | 30 |

31 | {{.RelayDescription}} 32 |

33 | 34 | 35 | 39 | {{.RelayURL}} 40 | 41 |
42 |
43 | 44 | 45 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bitvora/wot-relay 2 | 3 | go 1.23.1 4 | 5 | require ( 6 | github.com/fiatjaf/eventstore v0.14.4 7 | github.com/fiatjaf/khatru v0.14.0 8 | github.com/joho/godotenv v1.5.1 9 | github.com/nbd-wtf/go-nostr v0.44.2 10 | ) 11 | 12 | require ( 13 | fiatjaf.com/lib v0.2.0 // indirect 14 | github.com/andybalholm/brotli v1.1.1 // indirect 15 | github.com/bep/debounce v1.2.1 // indirect 16 | github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect 17 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect 18 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 19 | github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect 20 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect 21 | github.com/dgraph-io/badger/v4 v4.5.0 // indirect 22 | github.com/dgraph-io/ristretto v1.0.0 // indirect 23 | github.com/dgraph-io/ristretto/v2 v2.0.0 // indirect 24 | github.com/dustin/go-humanize v1.0.1 // indirect 25 | github.com/fasthttp/websocket v1.5.11 // indirect 26 | github.com/gobwas/httphead v0.1.0 // indirect 27 | github.com/gobwas/pool v0.2.1 // indirect 28 | github.com/gobwas/ws v1.4.0 // indirect 29 | github.com/gogo/protobuf v1.3.2 // indirect 30 | github.com/golang/glog v1.1.2 // indirect 31 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 32 | github.com/golang/protobuf v1.5.4 // indirect 33 | github.com/golang/snappy v0.0.4 // indirect 34 | github.com/google/flatbuffers v24.3.25+incompatible // indirect 35 | github.com/josharian/intern v1.0.0 // indirect 36 | github.com/json-iterator/go v1.1.12 // indirect 37 | github.com/klauspost/compress v1.17.11 // indirect 38 | github.com/mailru/easyjson v0.9.0 // indirect 39 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 40 | github.com/modern-go/reflect2 v1.0.2 // indirect 41 | github.com/pkg/errors v0.9.1 // indirect 42 | github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect 43 | github.com/rs/cors v1.11.1 // indirect 44 | github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect 45 | github.com/tidwall/gjson v1.18.0 // indirect 46 | github.com/tidwall/match v1.1.1 // indirect 47 | github.com/tidwall/pretty v1.2.1 // indirect 48 | github.com/valyala/bytebufferpool v1.0.0 // indirect 49 | github.com/valyala/fasthttp v1.58.0 // indirect 50 | go.opencensus.io v0.24.0 // indirect 51 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect 52 | golang.org/x/net v0.33.0 // indirect 53 | golang.org/x/sys v0.28.0 // indirect 54 | google.golang.org/protobuf v1.35.2 // indirect 55 | ) 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WoT Relay 2 | 3 | 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. 4 | 5 | # Available Relays 6 | 7 | Don't want to run the relay, just want to connect to some? Here are some available relays: 8 | 9 | - [wss://wot.utxo.one](https://wot.utxo.one) 10 | - [wss://nostrelites.org](https://nostrelites.org) 11 | - [wss://wot.nostr.party](https://wot.nostr.party) 12 | - [wss://wot.sovbit.host](https://wot.sovbit.host) 13 | - [wss://wot.girino.org](https://wot.girino.org) 14 | - [wss://relay.lnau.net](https://relay.lnau.net) 15 | - [wss://wot.siamstr.com](https://wot.siamstr.com) 16 | - [wss://relay.lexingtonbitcoin.org](https://relay.lexingtonbitcoin.org) 17 | - [wss://wot.azzamo.net](https://wot.azzamo.net) 18 | - [wss://wot.swarmstr.com](https://wot.swarmstr.com) 19 | - [wss://zap.watch](https://zap.watch) 20 | - [wss://satsage.xyz](https://satsage.xyz) 21 | - [wss://wons.calva.dev](https://wons.calva.dev) 22 | - [wss://wot.zacoos.com](https://wot.zacoos.com) 23 | - [wss://wot.shaving.kiwi](https://wot.shaving.kiwi) 24 | - [wss://wot.tealeaf.dev](https://wot.tealeaf.dev) 25 | - [wss://wot.nostr.net](https://wot.nostr.net) 26 | - [wss://relay.goodmorningbitcoin.com](https://relay.goodmorningbitcoin.com) 27 | - [wss://wot.sudocarlos.com](wss://wot.sudocarlos.com) 28 | 29 | ## Prerequisites 30 | 31 | - **Go**: Ensure you have Go installed on your system. You can download it from [here](https://golang.org/dl/). 32 | - **Build Essentials**: If you're using Linux, you may need to install build essentials. You can do this by running `sudo apt install build-essential`. 33 | 34 | ## Setup Instructions 35 | 36 | Follow these steps to get the WOT Relay running on your local machine: 37 | 38 | ### 1. Clone the repository 39 | 40 | ```bash 41 | git clone https://github.com/bitvora/wot-relay.git 42 | cd wot-relay 43 | ``` 44 | 45 | ### 2. Copy `.env.example` to `.env` 46 | 47 | You'll need to create an `.env` file based on the example provided in the repository. 48 | 49 | ```bash 50 | cp .env.example .env 51 | ``` 52 | 53 | ### 3. Set your environment variables 54 | 55 | Open the `.env` file and set the necessary environment variables. Example variables include: 56 | 57 | ```bash 58 | RELAY_NAME="YourRelayName" 59 | RELAY_PUBKEY="YourPublicKey" # the owner's hexkey, not npub. Convert npub to hex here: https://nostrcheck.me/converter/ 60 | RELAY_DESCRIPTION="Your relay description" 61 | DB_PATH="/home/ubuntu/wot-relay/db" # any path you would like the database to be saved. 62 | INDEX_PATH="/home/ubuntu/wot-relay/templates/index.html" # path to the index.html file 63 | STATIC_PATH="/home/ubuntu/wot-relay/templates/static" # path to the static folder 64 | REFRESH_INTERVAL_HOURS=24 # interval in hours to refresh the web of trust 65 | MINIMUM_FOLLOWERS=3 #how many followers before they're allowed in the WoT 66 | ARCHIVAL_SYNC="FALSE" # set to TRUE to archive every note from every person in the WoT (not recommended) 67 | ARCHIVE_REACTIONS="FALSE" # set to TRUE to archive every reaction from every person in the WoT (not recommended) 68 | IGNORE_FOLLOWS_LIST="" # comma separated list of pubkeys who follow too many bots and ruin the WoT 69 | ``` 70 | 71 | ### 4. Build the project 72 | 73 | Run the following command to build the relay: 74 | 75 | ```bash 76 | go build -ldflags "-X main.version=$(git describe --tags --always)" 77 | ``` 78 | 79 | ### 5. Create a Systemd Service (optional) 80 | 81 | To have the relay run as a service, create a systemd unit file. Make sure to limit the memory usage to less than your system's total memory to prevent the relay from crashing the system. 82 | 83 | 1. Create the file: 84 | 85 | ```bash 86 | sudo nano /etc/systemd/system/wot-relay.service 87 | ``` 88 | 89 | 2. Add the following contents: 90 | 91 | ```ini 92 | [Unit] 93 | Description=WOT Relay Service 94 | After=network.target 95 | 96 | [Service] 97 | ExecStart=/home/ubuntu/wot-relay/wot-relay 98 | WorkingDirectory=/home/ubuntu/wot-relay 99 | Restart=always 100 | MemoryLimit=2G 101 | 102 | [Install] 103 | WantedBy=multi-user.target 104 | ``` 105 | 106 | Replace `/path/to/` with the actual paths where you cloned the repository and stored the `.env` file. 107 | 108 | 3. Reload systemd to recognize the new service: 109 | 110 | ```bash 111 | sudo systemctl daemon-reload 112 | ``` 113 | 114 | 4. Start the service: 115 | 116 | ```bash 117 | sudo systemctl start wot-relay 118 | ``` 119 | 120 | 5. (Optional) Enable the service to start on boot: 121 | 122 | ```bash 123 | sudo systemctl enable wot-relay 124 | ``` 125 | 126 | #### Permission Issues on Some Systems 127 | 128 | 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: 129 | 130 | ```bash 131 | sudo chmod -R 777 /path/to/db 132 | ``` 133 | 134 | ### 6. Serving over nginx (optional) 135 | 136 | You can serve the relay over nginx by adding the following configuration to your nginx configuration file: 137 | 138 | ```nginx 139 | server { 140 | listen 80; 141 | server_name yourdomain.com; 142 | 143 | location / { 144 | proxy_pass http://localhost:3334; 145 | proxy_set_header Host $host; 146 | proxy_set_header X-Real-IP $remote_addr; 147 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 148 | proxy_set_header X-Forwarded-Proto $scheme; 149 | proxy_http_version 1.1; 150 | proxy_set_header Upgrade $http_upgrade; 151 | proxy_set_header Connection "upgrade"; 152 | } 153 | } 154 | ``` 155 | 156 | Replace `yourdomain.com` with your actual domain name. 157 | 158 | After adding the configuration, restart nginx: 159 | 160 | ```bash 161 | sudo systemctl restart nginx 162 | ``` 163 | 164 | ### 7. Install Certbot (optional) 165 | 166 | If you want to serve the relay over HTTPS, you can use Certbot to generate an SSL certificate. 167 | 168 | ```bash 169 | sudo apt-get update 170 | sudo apt-get install certbot python3-certbot-nginx 171 | ``` 172 | 173 | After installing Certbot, run the following command to generate an SSL certificate: 174 | 175 | ```bash 176 | sudo certbot --nginx 177 | ``` 178 | 179 | Follow the instructions to generate the certificate. 180 | 181 | ### 8. Access the relay 182 | 183 | Once everything is set up, the relay will be running on `localhost:3334` or your domain name if you set up nginx. 184 | 185 | ## Start the Project with Docker Compose 186 | 187 | To start the project using Docker Compose, follow these steps: 188 | 189 | 1. Ensure Docker and Docker Compose are installed on your system. 190 | 2. Navigate to the project directory. 191 | 3. Ensure the `.env` file is present in the project directory and has the necessary environment variables set. 192 | 4. You can also change the paths of the `db` folder and `templates` folder in the `docker-compose.yml` file. 193 | 194 | ```yaml 195 | volumes: 196 | - "./db:/app/db" # only change the left side before the colon 197 | - "./templates/index.html:${INDEX_PATH}" # only change the left side before the colon 198 | - "./templates/static:${INDEX_PATH}" # only change the left side before the colon 199 | ``` 200 | 201 | 5. Run the following command: 202 | 203 | ```sh 204 | # in foreground 205 | docker compose up --build 206 | # in background 207 | docker compose up --build -d 208 | ``` 209 | 210 | 6. For updating the relay, run the following command: 211 | 212 | ```sh 213 | git pull 214 | docker compose build --no-cache 215 | # in foreground 216 | docker compose up 217 | # in background 218 | docker compose up -d 219 | ``` 220 | 221 | This will build the Docker image and start the `wot-relay` service as defined in the `docker-compose.yml` file. The application will be accessible on port 3334. 222 | 223 | ### 7. Hidden Service with Tor (optional) 224 | 225 | Same as the step 6, but with the following command: 226 | 227 | ```sh 228 | # in foreground 229 | docker compose -f docker-compose.tor.yml up --build 230 | # in background 231 | docker compose -f docker-compose.tor.yml up --build -d 232 | ``` 233 | 234 | You can find the onion address here: `tor/data/relay/hostname` 235 | 236 | ### 8. Access the relay 237 | 238 | Once everything is set up, the relay will be running on `localhost:3334`. 239 | 240 | ```bash 241 | http://localhost:3334 242 | ``` 243 | 244 | ## License 245 | 246 | This project is licensed under the MIT License. 247 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | fiatjaf.com/lib v0.2.0 h1:TgIJESbbND6GjOgGHxF5jsO6EMjuAxIzZHPo5DXYexs= 3 | fiatjaf.com/lib v0.2.0/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 6 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 7 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 8 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 9 | github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= 10 | github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= 11 | github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= 12 | github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= 13 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= 14 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 15 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 16 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 17 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 18 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 19 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 20 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 21 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 23 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= 25 | github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 26 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= 27 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 28 | github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= 29 | github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= 30 | github.com/dgraph-io/badger/v4 v4.3.1 h1:7r5wKqmoRpGgSxqa0S/nGdpOpvvzuREGPLSua73C8tw= 31 | github.com/dgraph-io/badger/v4 v4.3.1/go.mod h1:oObz97DImXpd6O/Dt8BqdKLLTDmEmarAimo72VV5whQ= 32 | github.com/dgraph-io/badger/v4 v4.5.0 h1:TeJE3I1pIWLBjYhIYCA1+uxrjWEoJXImFBMEBVSm16g= 33 | github.com/dgraph-io/badger/v4 v4.5.0/go.mod h1:ysgYmIeG8dS/E8kwxT7xHyc7MkmwNYLRoYnFbr7387A= 34 | github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= 35 | github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= 36 | github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84= 37 | github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc= 38 | github.com/dgraph-io/ristretto/v2 v2.0.0 h1:l0yiSOtlJvc0otkqyMaDNysg8E9/F/TYZwMbxscNOAQ= 39 | github.com/dgraph-io/ristretto/v2 v2.0.0/go.mod h1:FVFokF2dRqXyPyeMnK1YDy8Fc6aTe0IKgbcd03CYeEk= 40 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= 41 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 42 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 43 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 44 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 45 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 46 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 47 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 48 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 49 | github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KHau4= 50 | github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU= 51 | github.com/fasthttp/websocket v1.5.11 h1:TCO3H2VSxeTJQ+Ij+w8q7UBvdVedMOy/G7aZ0a6V19s= 52 | github.com/fasthttp/websocket v1.5.11/go.mod h1:QWILjDXurHFN5519nH2Pe9rtRuKZ/OIx/rlBF9coYds= 53 | github.com/fiatjaf/eventstore v0.10.1 h1:zgXrRhpyjzX0Eub4T6zkMZ60zRnIPJqy/Ipfx5h+yWo= 54 | github.com/fiatjaf/eventstore v0.10.1/go.mod h1:h5CdLSF7mEQ7/rWpEABTRIrNuFoSwdQDi/nZkW/vVFU= 55 | github.com/fiatjaf/eventstore v0.11.2 h1:gJTATGOk7RtDGt1qs47cLyTzko9phFyXlmWQb0zR7Lg= 56 | github.com/fiatjaf/eventstore v0.11.2/go.mod h1:oCHPB4TprrNjbhH2kjMKt1O48O1pk3VxAy5iZkB5Fb0= 57 | github.com/fiatjaf/eventstore v0.14.0 h1:eAyugJGFRCrXYJLCc2nC/BIApmBbQN/Z4dxvNz1SIvI= 58 | github.com/fiatjaf/eventstore v0.14.0/go.mod h1:XOl5B6WGBX1a0ww6s3WT94QVOmye/6zDTtyWHVtHQ5U= 59 | github.com/fiatjaf/eventstore v0.14.4 h1:bqJQit/M5E6vwbWwgrL4kTPoWCbt1Hb9H/AH4xf9uVQ= 60 | github.com/fiatjaf/eventstore v0.14.4/go.mod h1:3Kkujc6A8KjpNvSKu1jNCcFjSgEEyCxaDJVgShHz0J8= 61 | github.com/fiatjaf/khatru v0.8.2-0.20240913013357-18fc0dc1dd58 h1:F5Cy44IzxeIhzY8bf34rnORw0pYn6ZT/pIuyMO02Kjs= 62 | github.com/fiatjaf/khatru v0.8.2-0.20240913013357-18fc0dc1dd58/go.mod h1:jRmqbbIbEH+y0unt3wMUBwqY/btVussqx5SmBoGhXtg= 63 | github.com/fiatjaf/khatru v0.14.0 h1:zpWlAA87XBpDKBPIDbAuNw/HpKXzyt5XHVDbSvUbmDo= 64 | github.com/fiatjaf/khatru v0.14.0/go.mod h1:uxE5e8DBXPZqbHjr/gfatQas5bEJIMmsOCDcdF4LoRQ= 65 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 66 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 67 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 68 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 69 | github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= 70 | github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= 71 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 72 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 73 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 74 | github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= 75 | github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= 76 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 77 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 78 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 79 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 80 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 81 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 82 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 83 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 84 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 85 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 86 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 87 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 88 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 89 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 90 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 91 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 92 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 93 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 94 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 95 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 96 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 97 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 98 | github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= 99 | github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 100 | github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= 101 | github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 102 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 103 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 104 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 105 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 106 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 107 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 108 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 109 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 110 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 111 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 112 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 113 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 114 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 115 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 116 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 117 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 118 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 119 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 120 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 121 | github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= 122 | github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 123 | github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= 124 | github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 125 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 126 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 127 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 128 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 129 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 130 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 131 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 132 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 133 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 134 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 135 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 136 | github.com/nbd-wtf/go-nostr v0.38.1 h1:D0moEtIpjhWs2zbgeRyokA4TOLzBdumtpL1/O7/frww= 137 | github.com/nbd-wtf/go-nostr v0.38.1/go.mod h1:TGKGj00BmJRXvRe0LlpDN3KKbELhhPXgBwUEhzu3Oq0= 138 | github.com/nbd-wtf/go-nostr v0.42.0 h1:EofWfXEhKic9AYVf4RHuXZr+kKUZE2jVyJtJByNe1rE= 139 | github.com/nbd-wtf/go-nostr v0.42.0/go.mod h1:FBa4FBJO7NuANvkeKSlrf0BIyxGufmrUbuelr6Q4Ick= 140 | github.com/nbd-wtf/go-nostr v0.44.2 h1:+DHDbHuUZS3Fkh9w4j2v64sTLC4fY8ktuiBsNg9GcXg= 141 | github.com/nbd-wtf/go-nostr v0.44.2/go.mod h1:m0ID2gSA2Oak/uaPnM1uN22JhDRZS4UVJG2c8jo19rg= 142 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 143 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 144 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 145 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 146 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 147 | github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= 148 | github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= 149 | github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= 150 | github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 151 | github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= 152 | github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 153 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= 154 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= 155 | github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= 156 | github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= 157 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 158 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 159 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 160 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 161 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 162 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 163 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 164 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 165 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 166 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 167 | github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= 168 | github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 169 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 170 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 171 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 172 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 173 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 174 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 175 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 176 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 177 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 178 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= 179 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= 180 | github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= 181 | github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= 182 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 183 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 184 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 185 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 186 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 187 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 188 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 189 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 190 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 191 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= 192 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= 193 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= 194 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= 195 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 196 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 197 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 198 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 199 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 200 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 201 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 202 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 203 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 204 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 205 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 206 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 207 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 208 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 209 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= 210 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 211 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 212 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 213 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 214 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 215 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 216 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 217 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 218 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 219 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 220 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 221 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 222 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 223 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 224 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 225 | golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 226 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 227 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 228 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 229 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 230 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 231 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 232 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 233 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 234 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 235 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 236 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 237 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 238 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 239 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 240 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 241 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 242 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 243 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 244 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 245 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 246 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 247 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 248 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 249 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 250 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 251 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 252 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 253 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 254 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 255 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 256 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 257 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 258 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 259 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 260 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 261 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 262 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 263 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 264 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 265 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 266 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 267 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 268 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 269 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 270 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 271 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 272 | google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= 273 | google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 274 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 275 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 276 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 277 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 278 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 279 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 280 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 281 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "html/template" 7 | "io" 8 | "log" 9 | "net/http" 10 | _ "net/http/pprof" 11 | "os" 12 | "runtime" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | "sync/atomic" 17 | "time" 18 | 19 | "github.com/fiatjaf/eventstore" 20 | "github.com/fiatjaf/eventstore/badger" 21 | "github.com/fiatjaf/khatru" 22 | "github.com/fiatjaf/khatru/policies" 23 | "github.com/joho/godotenv" 24 | "github.com/nbd-wtf/go-nostr" 25 | ) 26 | 27 | var ( 28 | version string 29 | ) 30 | 31 | type Config struct { 32 | RelayName string 33 | RelayPubkey string 34 | RelayDescription string 35 | DBPath string 36 | RelayURL string 37 | IndexPath string 38 | StaticPath string 39 | RefreshInterval int 40 | MinimumFollowers int 41 | ArchivalSync bool 42 | RelayContact string 43 | RelayIcon string 44 | MaxAgeDays int 45 | ArchiveReactions bool 46 | IgnoredPubkeys []string 47 | MaxTrustNetwork int 48 | MaxRelays int 49 | MaxOneHopNetwork int 50 | } 51 | 52 | var pool *nostr.SimplePool 53 | var wdb nostr.RelayStore 54 | var relays []string 55 | var relaySet = make(map[string]bool) // O(1) lookup 56 | var config Config 57 | var trustNetwork []string 58 | var trustNetworkSet = make(map[string]bool) // O(1) lookup 59 | var seedRelays []string 60 | var booted bool 61 | var oneHopNetwork []string 62 | var oneHopNetworkSet = make(map[string]bool) // O(1) lookup 63 | var trustNetworkMap map[string]bool 64 | var pubkeyFollowerCount = make(map[string]int) 65 | var trustedNotes uint64 66 | var untrustedNotes uint64 67 | var archiveEventSemaphore = make(chan struct{}, 20) // Reduced from 100 to 20 68 | 69 | // Performance counters 70 | var ( 71 | totalEvents uint64 72 | rejectedEvents uint64 73 | archivedEvents uint64 74 | profileRefreshCount uint64 75 | networkRefreshCount uint64 76 | ) 77 | 78 | // Mutexes for thread safety 79 | var ( 80 | relayMutex sync.RWMutex 81 | trustNetworkMutex sync.RWMutex 82 | oneHopMutex sync.RWMutex 83 | followerMutex sync.RWMutex 84 | ) 85 | 86 | func main() { 87 | nostr.InfoLogger = log.New(io.Discard, "", 0) 88 | booted = false 89 | green := "\033[32m" 90 | reset := "\033[0m" 91 | 92 | art := ` 93 | 888 888 88888888888 8888888b. 888 94 | 888 o 888 888 888 Y88b 888 95 | 888 d8b 888 888 888 888 888 96 | 888 d888b 888 .d88b. 888 888 d88P .d88b. 888 8888b. 888 888 97 | 888d88888b888 d88""88b 888 8888888P" d8P Y8b 888 "88b 888 888 98 | 88888P Y88888 888 888 888 888 T88b 88888888 888 .d888888 888 888 99 | 8888P Y8888 Y88..88P 888 888 T88b Y8b. 888 888 888 Y88b 888 100 | 888P Y888 "Y88P" 888 888 T88b "Y8888 888 "Y888888 "Y88888 101 | 888 102 | Y8b d88P 103 | powered by: khatru "Y88P" 104 | ` 105 | 106 | fmt.Println(green + art + reset) 107 | log.Println("🚀 booting up web of trust relay") 108 | relay := khatru.NewRelay() 109 | ctx := context.Background() 110 | pool = nostr.NewSimplePool(ctx) 111 | config = LoadConfig() 112 | 113 | relay.Info.Name = config.RelayName 114 | relay.Info.PubKey = config.RelayPubkey 115 | relay.Info.Icon = config.RelayIcon 116 | relay.Info.Contact = config.RelayContact 117 | relay.Info.Description = config.RelayDescription 118 | relay.Info.Software = "https://github.com/bitvora/wot-relay" 119 | relay.Info.Version = version 120 | 121 | appendPubkey(config.RelayPubkey) 122 | 123 | db := getDB() 124 | if err := db.Init(); err != nil { 125 | panic(err) 126 | } 127 | wdb = eventstore.RelayWrapper{Store: &db} 128 | 129 | relay.RejectEvent = append(relay.RejectEvent, 130 | policies.RejectEventsWithBase64Media, 131 | policies.EventIPRateLimiter(5, time.Minute*1, 30), 132 | ) 133 | 134 | relay.RejectFilter = append(relay.RejectFilter, 135 | policies.NoEmptyFilters, 136 | policies.NoComplexFilters, 137 | policies.FilterIPRateLimiter(5, time.Minute*1, 30), 138 | ) 139 | 140 | relay.RejectConnection = append(relay.RejectConnection, 141 | policies.ConnectionRateLimiter(10, time.Minute*2, 30), 142 | ) 143 | 144 | relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent) 145 | relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents) 146 | relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent) 147 | relay.RejectEvent = append(relay.RejectEvent, func(ctx context.Context, event *nostr.Event) (bool, string) { 148 | atomic.AddUint64(&totalEvents, 1) 149 | 150 | // Don't reject events if we haven't booted yet or if trust network is empty 151 | if !booted { 152 | return false, "" 153 | } 154 | 155 | trustNetworkMutex.RLock() 156 | trusted := trustNetworkMap[event.PubKey] 157 | hasNetwork := len(trustNetworkMap) > 0 158 | trustNetworkMutex.RUnlock() 159 | 160 | // If we don't have a trust network yet, allow all events 161 | if !hasNetwork { 162 | return false, "" 163 | } 164 | 165 | if !trusted { 166 | atomic.AddUint64(&rejectedEvents, 1) 167 | return true, "not in web of trust" 168 | } 169 | if event.Kind == nostr.KindEncryptedDirectMessage { 170 | atomic.AddUint64(&rejectedEvents, 1) 171 | return true, "only gift wrapped DMs are allowed" 172 | } 173 | 174 | return false, "" 175 | }) 176 | 177 | seedRelays = []string{ 178 | "wss://nos.lol", 179 | "wss://nostr.mom", 180 | "wss://purplepag.es", 181 | "wss://purplerelay.com", 182 | "wss://relay.damus.io", 183 | "wss://relay.nostr.band", 184 | "wss://relay.snort.social", 185 | "wss://relayable.org", 186 | "wss://relay.primal.net", 187 | "wss://relay.nostr.bg", 188 | "wss://no.str.cr", 189 | "wss://nostr21.com", 190 | "wss://nostrue.com", 191 | "wss://relay.siamstr.com", 192 | } 193 | 194 | go refreshTrustNetwork(ctx, relay) 195 | go monitorMemoryUsage() // Add memory monitoring 196 | go monitorPerformance() // Add performance monitoring 197 | 198 | mux := relay.Router() 199 | static := http.FileServer(http.Dir(config.StaticPath)) 200 | 201 | mux.Handle("GET /static/", http.StripPrefix("/static/", static)) 202 | mux.Handle("GET /favicon.ico", http.StripPrefix("/", static)) 203 | 204 | // Add debug endpoints 205 | mux.HandleFunc("GET /debug/stats", debugStatsHandler) 206 | mux.HandleFunc("GET /debug/goroutines", debugGoroutinesHandler) 207 | 208 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 209 | tmpl := template.Must(template.ParseFiles(os.Getenv("INDEX_PATH"))) 210 | data := struct { 211 | RelayName string 212 | RelayPubkey string 213 | RelayDescription string 214 | RelayURL string 215 | }{ 216 | RelayName: config.RelayName, 217 | RelayPubkey: config.RelayPubkey, 218 | RelayDescription: config.RelayDescription, 219 | RelayURL: config.RelayURL, 220 | } 221 | err := tmpl.Execute(w, data) 222 | if err != nil { 223 | http.Error(w, err.Error(), http.StatusInternalServerError) 224 | } 225 | }) 226 | 227 | log.Println("🎉 relay running on port :3334") 228 | log.Println("🔍 debug endpoints available at:") 229 | log.Println(" http://localhost:3334/debug/pprof/ (CPU/memory profiling)") 230 | log.Println(" http://localhost:3334/debug/stats (application stats)") 231 | log.Println(" http://localhost:3334/debug/goroutines (goroutine info)") 232 | err := http.ListenAndServe(":3334", relay) 233 | if err != nil { 234 | log.Fatal(err) 235 | } 236 | } 237 | 238 | func LoadConfig() Config { 239 | godotenv.Load(".env") 240 | 241 | if os.Getenv("REFRESH_INTERVAL_HOURS") == "" { 242 | os.Setenv("REFRESH_INTERVAL_HOURS", "3") 243 | } 244 | 245 | refreshInterval, _ := strconv.Atoi(os.Getenv("REFRESH_INTERVAL_HOURS")) 246 | log.Println("🔄 refresh interval set to", refreshInterval, "hours") 247 | 248 | if os.Getenv("MINIMUM_FOLLOWERS") == "" { 249 | os.Setenv("MINIMUM_FOLLOWERS", "1") 250 | } 251 | 252 | if os.Getenv("ARCHIVAL_SYNC") == "" { 253 | os.Setenv("ARCHIVAL_SYNC", "TRUE") 254 | } 255 | 256 | if os.Getenv("RELAY_ICON") == "" { 257 | os.Setenv("RELAY_ICON", "https://pfp.nostr.build/56306a93a88d4c657d8a3dfa57b55a4ed65b709eee927b5dafaab4d5330db21f.png") 258 | } 259 | 260 | if os.Getenv("RELAY_CONTACT") == "" { 261 | os.Setenv("RELAY_CONTACT", getEnv("RELAY_PUBKEY")) 262 | } 263 | 264 | if os.Getenv("MAX_AGE_DAYS") == "" { 265 | os.Setenv("MAX_AGE_DAYS", "0") 266 | } 267 | 268 | if os.Getenv("ARCHIVE_REACTIONS") == "" { 269 | os.Setenv("ARCHIVE_REACTIONS", "FALSE") 270 | } 271 | 272 | if os.Getenv("MAX_TRUST_NETWORK") == "" { 273 | os.Setenv("MAX_TRUST_NETWORK", "40000") 274 | } 275 | 276 | if os.Getenv("MAX_RELAYS") == "" { 277 | os.Setenv("MAX_RELAYS", "1000") 278 | } 279 | 280 | if os.Getenv("MAX_ONE_HOP_NETWORK") == "" { 281 | os.Setenv("MAX_ONE_HOP_NETWORK", "50000") 282 | } 283 | 284 | ignoredPubkeys := []string{} 285 | if ignoreList := os.Getenv("IGNORE_FOLLOWS_LIST"); ignoreList != "" { 286 | ignoredPubkeys = splitAndTrim(ignoreList) 287 | } 288 | 289 | minimumFollowers, _ := strconv.Atoi(os.Getenv("MINIMUM_FOLLOWERS")) 290 | maxAgeDays, _ := strconv.Atoi(os.Getenv("MAX_AGE_DAYS")) 291 | maxTrustNetwork, _ := strconv.Atoi(os.Getenv("MAX_TRUST_NETWORK")) 292 | maxRelays, _ := strconv.Atoi(os.Getenv("MAX_RELAYS")) 293 | maxOneHopNetwork, _ := strconv.Atoi(os.Getenv("MAX_ONE_HOP_NETWORK")) 294 | 295 | config := Config{ 296 | RelayName: getEnv("RELAY_NAME"), 297 | RelayPubkey: getEnv("RELAY_PUBKEY"), 298 | RelayDescription: getEnv("RELAY_DESCRIPTION"), 299 | RelayContact: getEnv("RELAY_CONTACT"), 300 | RelayIcon: getEnv("RELAY_ICON"), 301 | DBPath: getEnv("DB_PATH"), 302 | RelayURL: getEnv("RELAY_URL"), 303 | IndexPath: getEnv("INDEX_PATH"), 304 | StaticPath: getEnv("STATIC_PATH"), 305 | RefreshInterval: refreshInterval, 306 | MinimumFollowers: minimumFollowers, 307 | ArchivalSync: getEnv("ARCHIVAL_SYNC") == "TRUE", 308 | MaxAgeDays: maxAgeDays, 309 | ArchiveReactions: getEnv("ARCHIVE_REACTIONS") == "TRUE", 310 | IgnoredPubkeys: ignoredPubkeys, 311 | MaxTrustNetwork: maxTrustNetwork, 312 | MaxRelays: maxRelays, 313 | MaxOneHopNetwork: maxOneHopNetwork, 314 | } 315 | 316 | return config 317 | } 318 | 319 | func getEnv(key string) string { 320 | value, exists := os.LookupEnv(key) 321 | if !exists { 322 | log.Fatalf("Environment variable %s not set", key) 323 | } 324 | return value 325 | } 326 | 327 | func updateTrustNetworkFilter() { 328 | // Build new trust network in temporary variables 329 | newTrustNetworkMap := make(map[string]bool) 330 | var newTrustNetwork []string 331 | newTrustNetworkSet := make(map[string]bool) 332 | 333 | log.Println("🌐 building new trust network map") 334 | 335 | followerMutex.RLock() 336 | for pubkey, count := range pubkeyFollowerCount { 337 | if count >= config.MinimumFollowers { 338 | newTrustNetworkMap[pubkey] = true 339 | if !newTrustNetworkSet[pubkey] && len(pubkey) == 64 && len(newTrustNetwork) < config.MaxTrustNetwork { 340 | newTrustNetwork = append(newTrustNetwork, pubkey) 341 | newTrustNetworkSet[pubkey] = true 342 | } 343 | } 344 | } 345 | followerMutex.RUnlock() 346 | 347 | // Now atomically replace the active trust network 348 | trustNetworkMutex.Lock() 349 | trustNetworkMap = newTrustNetworkMap 350 | trustNetwork = newTrustNetwork 351 | trustNetworkSet = newTrustNetworkSet 352 | trustNetworkMutex.Unlock() 353 | 354 | log.Println("🌐 trust network map updated with", len(newTrustNetwork), "keys") 355 | 356 | // Cleanup follower count map periodically to prevent unbounded growth 357 | followerMutex.Lock() 358 | if len(pubkeyFollowerCount) > config.MaxOneHopNetwork*2 { 359 | log.Println("🧹 cleaning follower count map") 360 | newFollowerCount := make(map[string]int) 361 | for pubkey, count := range pubkeyFollowerCount { 362 | if count >= config.MinimumFollowers || newTrustNetworkMap[pubkey] { 363 | newFollowerCount[pubkey] = count 364 | } 365 | } 366 | oldCount := len(pubkeyFollowerCount) 367 | pubkeyFollowerCount = newFollowerCount 368 | log.Printf("🧹 cleaned follower count map: %d -> %d entries", oldCount, len(newFollowerCount)) 369 | } 370 | followerMutex.Unlock() 371 | } 372 | 373 | func refreshProfiles(ctx context.Context) { 374 | atomic.AddUint64(&profileRefreshCount, 1) 375 | start := time.Now() 376 | 377 | // Get a snapshot of current trust network to avoid holding locks during network operations 378 | trustNetworkMutex.RLock() 379 | currentTrustNetwork := make([]string, len(trustNetwork)) 380 | copy(currentTrustNetwork, trustNetwork) 381 | trustNetworkMutex.RUnlock() 382 | 383 | for i := 0; i < len(currentTrustNetwork); i += 200 { 384 | timeout, cancel := context.WithTimeout(ctx, 4*time.Second) 385 | 386 | end := i + 200 387 | if end > len(currentTrustNetwork) { 388 | end = len(currentTrustNetwork) 389 | } 390 | 391 | filters := []nostr.Filter{{ 392 | Authors: currentTrustNetwork[i:end], 393 | Kinds: []int{nostr.KindProfileMetadata}, 394 | }} 395 | 396 | for ev := range pool.SubManyEose(timeout, seedRelays, filters) { 397 | wdb.Publish(ctx, *ev.Event) 398 | } 399 | 400 | cancel() // Cancel after each iteration 401 | } 402 | duration := time.Since(start) 403 | log.Printf("👤 profiles refreshed: %d profiles in %v", len(currentTrustNetwork), duration) 404 | } 405 | 406 | func refreshTrustNetwork(ctx context.Context, relay *khatru.Relay) { 407 | runTrustNetworkRefresh := func() { 408 | atomic.AddUint64(&networkRefreshCount, 1) 409 | start := time.Now() 410 | 411 | // Build new networks in temporary variables to avoid disrupting the active network 412 | var newOneHopNetwork []string 413 | newOneHopNetworkSet := make(map[string]bool) 414 | newPubkeyFollowerCount := make(map[string]int) 415 | 416 | // Copy existing follower counts to preserve data 417 | followerMutex.RLock() 418 | for k, v := range pubkeyFollowerCount { 419 | newPubkeyFollowerCount[k] = v 420 | } 421 | followerMutex.RUnlock() 422 | 423 | timeoutCtx, cancel := context.WithTimeout(ctx, 3*time.Second) 424 | defer cancel() 425 | 426 | filters := []nostr.Filter{{ 427 | Authors: []string{config.RelayPubkey}, 428 | Kinds: []int{nostr.KindFollowList}, 429 | }} 430 | 431 | log.Println("🔍 fetching owner's follows") 432 | eventCount := 0 433 | for ev := range pool.SubManyEose(timeoutCtx, seedRelays, filters) { 434 | eventCount++ 435 | for _, contact := range ev.Event.Tags.GetAll([]string{"p"}) { 436 | pubkey := contact[1] 437 | if isIgnored(pubkey, config.IgnoredPubkeys) { 438 | fmt.Println("ignoring follows from pubkey: ", pubkey) 439 | continue 440 | } 441 | newPubkeyFollowerCount[contact[1]]++ 442 | 443 | // Add to new one-hop network 444 | if !newOneHopNetworkSet[contact[1]] && len(contact[1]) == 64 && len(newOneHopNetwork) < config.MaxOneHopNetwork { 445 | newOneHopNetwork = append(newOneHopNetwork, contact[1]) 446 | newOneHopNetworkSet[contact[1]] = true 447 | } 448 | } 449 | } 450 | log.Printf("🔍 processed %d follow list events", eventCount) 451 | 452 | log.Println("🌐 building web of trust graph") 453 | totalProcessed := 0 454 | for i := 0; i < len(newOneHopNetwork); i += 100 { 455 | timeout, cancel := context.WithTimeout(ctx, 4*time.Second) 456 | 457 | end := i + 100 458 | if end > len(newOneHopNetwork) { 459 | end = len(newOneHopNetwork) 460 | } 461 | 462 | filters = []nostr.Filter{{ 463 | Authors: newOneHopNetwork[i:end], 464 | Kinds: []int{nostr.KindFollowList, nostr.KindRelayListMetadata, nostr.KindProfileMetadata}, 465 | }} 466 | 467 | batchCount := 0 468 | for ev := range pool.SubManyEose(timeout, seedRelays, filters) { 469 | batchCount++ 470 | totalProcessed++ 471 | for _, contact := range ev.Event.Tags.GetAll([]string{"p"}) { 472 | if len(contact) > 1 { 473 | newPubkeyFollowerCount[contact[1]]++ 474 | } 475 | } 476 | 477 | for _, relay := range ev.Event.Tags.GetAll([]string{"r"}) { 478 | appendRelay(relay[1]) 479 | } 480 | 481 | if ev.Event.Kind == nostr.KindProfileMetadata { 482 | wdb.Publish(ctx, *ev.Event) 483 | } 484 | } 485 | cancel() // Cancel after each iteration 486 | 487 | if i%500 == 0 { // Log progress every 5 batches 488 | log.Printf("🌐 processed batch %d-%d (%d events in this batch)", i, end, batchCount) 489 | } 490 | } 491 | 492 | // Now atomically replace the active data structures 493 | oneHopMutex.Lock() 494 | oneHopNetwork = newOneHopNetwork 495 | oneHopNetworkSet = newOneHopNetworkSet 496 | oneHopMutex.Unlock() 497 | 498 | followerMutex.Lock() 499 | pubkeyFollowerCount = newPubkeyFollowerCount 500 | followerMutex.Unlock() 501 | 502 | duration := time.Since(start) 503 | log.Printf("🫂 total network size: %d (processed %d events in %v)", len(newPubkeyFollowerCount), totalProcessed, duration) 504 | relayMutex.RLock() 505 | log.Println("🔗 relays discovered:", len(relays)) 506 | relayMutex.RUnlock() 507 | } 508 | 509 | ticker := time.NewTicker(time.Duration(config.RefreshInterval) * time.Hour) 510 | defer ticker.Stop() 511 | 512 | // Run initial refresh 513 | log.Println("🚀 performing initial trust network build...") 514 | runTrustNetworkRefresh() 515 | updateTrustNetworkFilter() 516 | 517 | // Mark as booted after initial trust network is built 518 | booted = true 519 | log.Println("✅ trust network initialized, relay is now active") 520 | 521 | deleteOldNotes(relay) 522 | archiveTrustedNotes(ctx, relay) 523 | 524 | // Then run on timer 525 | for { 526 | select { 527 | case <-ticker.C: 528 | log.Println("🔄 refreshing trust network in background...") 529 | runTrustNetworkRefresh() 530 | updateTrustNetworkFilter() 531 | deleteOldNotes(relay) 532 | archiveTrustedNotes(ctx, relay) 533 | log.Println("✅ trust network refresh completed") 534 | case <-ctx.Done(): 535 | return 536 | } 537 | } 538 | } 539 | 540 | func appendRelay(relay string) { 541 | relayMutex.Lock() 542 | defer relayMutex.Unlock() 543 | 544 | if len(relays) >= config.MaxRelays { 545 | return // Prevent unbounded growth 546 | } 547 | 548 | if relaySet[relay] { 549 | return // Already exists 550 | } 551 | 552 | relays = append(relays, relay) 553 | relaySet[relay] = true 554 | } 555 | 556 | func appendPubkey(pubkey string) { 557 | trustNetworkMutex.Lock() 558 | defer trustNetworkMutex.Unlock() 559 | 560 | if len(trustNetwork) >= config.MaxTrustNetwork { 561 | return // Prevent unbounded growth 562 | } 563 | 564 | if trustNetworkSet[pubkey] { 565 | return // Already exists 566 | } 567 | 568 | if len(pubkey) != 64 { 569 | return 570 | } 571 | 572 | trustNetwork = append(trustNetwork, pubkey) 573 | trustNetworkSet[pubkey] = true 574 | } 575 | 576 | func archiveTrustedNotes(ctx context.Context, relay *khatru.Relay) { 577 | timeout, cancel := context.WithTimeout(ctx, time.Duration(config.RefreshInterval)*time.Hour) 578 | defer cancel() 579 | 580 | done := make(chan struct{}) 581 | 582 | go func() { 583 | defer close(done) 584 | if config.ArchivalSync { 585 | go refreshProfiles(ctx) 586 | 587 | var filters []nostr.Filter 588 | since := nostr.Now() 589 | if config.ArchiveReactions { 590 | filters = []nostr.Filter{{ 591 | Kinds: []int{ 592 | nostr.KindArticle, 593 | nostr.KindDeletion, 594 | nostr.KindFollowList, 595 | nostr.KindEncryptedDirectMessage, 596 | nostr.KindMuteList, 597 | nostr.KindReaction, 598 | nostr.KindRelayListMetadata, 599 | nostr.KindRepost, 600 | nostr.KindZapRequest, 601 | nostr.KindZap, 602 | nostr.KindTextNote, 603 | }, 604 | Since: &since, 605 | }} 606 | } else { 607 | filters = []nostr.Filter{{ 608 | Kinds: []int{ 609 | nostr.KindArticle, 610 | nostr.KindDeletion, 611 | nostr.KindFollowList, 612 | nostr.KindEncryptedDirectMessage, 613 | nostr.KindMuteList, 614 | nostr.KindRelayListMetadata, 615 | nostr.KindRepost, 616 | nostr.KindZapRequest, 617 | nostr.KindZap, 618 | nostr.KindTextNote, 619 | }, 620 | Since: &since, 621 | }} 622 | } 623 | 624 | log.Println("📦 archiving trusted notes...") 625 | 626 | eventCount := 0 627 | for ev := range pool.SubMany(timeout, seedRelays, filters) { 628 | eventCount++ 629 | 630 | // Check GC pressure every 1000 events 631 | if eventCount%1000 == 0 { 632 | var m runtime.MemStats 633 | runtime.ReadMemStats(&m) 634 | if m.NumGC > 0 && eventCount > 1000 { 635 | // If we're doing more than 2 GCs per 1000 events, slow down 636 | gcRate := float64(m.NumGC) / float64(eventCount/1000) 637 | if gcRate > 2.0 { 638 | log.Printf("⚠️ High GC pressure (%.1f GC/1000 events), slowing archive process", gcRate) 639 | time.Sleep(100 * time.Millisecond) // Brief pause 640 | } 641 | } 642 | } 643 | 644 | // Use semaphore to limit concurrent goroutines 645 | select { 646 | case archiveEventSemaphore <- struct{}{}: 647 | go func(event nostr.Event) { 648 | defer func() { <-archiveEventSemaphore }() 649 | archiveEvent(ctx, relay, event) 650 | }(*ev.Event) 651 | case <-timeout.Done(): 652 | log.Printf("📦 archive timeout reached, processed %d events", eventCount) 653 | return 654 | default: 655 | // If semaphore is full, process synchronously to avoid buildup 656 | archiveEvent(ctx, relay, *ev.Event) 657 | } 658 | } 659 | 660 | log.Printf("📦 archived %d trusted notes and discarded %d untrusted notes (processed %d total events)", 661 | atomic.LoadUint64(&trustedNotes), atomic.LoadUint64(&untrustedNotes), eventCount) 662 | } else { 663 | log.Println("🔄 web of trust will refresh in", config.RefreshInterval, "hours") 664 | select { 665 | case <-timeout.Done(): 666 | } 667 | } 668 | }() 669 | 670 | select { 671 | case <-timeout.Done(): 672 | log.Println("restarting process") 673 | case <-done: 674 | log.Println("📦 archiving process completed") 675 | } 676 | } 677 | 678 | func archiveEvent(ctx context.Context, relay *khatru.Relay, ev nostr.Event) { 679 | trustNetworkMutex.RLock() 680 | trusted := trustNetworkMap[ev.PubKey] 681 | trustNetworkMutex.RUnlock() 682 | 683 | if trusted { 684 | wdb.Publish(ctx, ev) 685 | relay.BroadcastEvent(&ev) 686 | atomic.AddUint64(&trustedNotes, 1) 687 | atomic.AddUint64(&archivedEvents, 1) 688 | } else { 689 | atomic.AddUint64(&untrustedNotes, 1) 690 | } 691 | } 692 | 693 | func deleteOldNotes(relay *khatru.Relay) error { 694 | ctx := context.TODO() 695 | 696 | if config.MaxAgeDays <= 0 { 697 | log.Printf("MAX_AGE_DAYS disabled") 698 | return nil 699 | } 700 | 701 | maxAgeSecs := nostr.Timestamp(config.MaxAgeDays * 86400) 702 | oldAge := nostr.Now() - maxAgeSecs 703 | if oldAge <= 0 { 704 | log.Printf("MAX_AGE_DAYS too large") 705 | return nil 706 | } 707 | 708 | filter := nostr.Filter{ 709 | Until: &oldAge, 710 | Kinds: []int{ 711 | nostr.KindArticle, 712 | nostr.KindDeletion, 713 | nostr.KindFollowList, 714 | nostr.KindEncryptedDirectMessage, 715 | nostr.KindMuteList, 716 | nostr.KindReaction, 717 | nostr.KindRelayListMetadata, 718 | nostr.KindRepost, 719 | nostr.KindZapRequest, 720 | nostr.KindZap, 721 | nostr.KindTextNote, 722 | }, 723 | Limit: 1000, // Process in batches to avoid memory issues 724 | } 725 | 726 | ch, err := relay.QueryEvents[0](ctx, filter) 727 | if err != nil { 728 | log.Printf("query error %s", err) 729 | return err 730 | } 731 | 732 | // Process events in batches to avoid memory issues 733 | batchSize := 100 734 | events := make([]*nostr.Event, 0, batchSize) 735 | count := 0 736 | 737 | for evt := range ch { 738 | events = append(events, evt) 739 | count++ 740 | 741 | if len(events) >= batchSize { 742 | // Delete this batch 743 | for num_evt, del_evt := range events { 744 | for _, del := range relay.DeleteEvent { 745 | if err := del(ctx, del_evt); err != nil { 746 | log.Printf("error deleting note %d of batch. event id: %s", num_evt, del_evt.ID) 747 | return err 748 | } 749 | } 750 | } 751 | events = events[:0] // Reset slice but keep capacity 752 | } 753 | } 754 | 755 | // Delete remaining events 756 | if len(events) > 0 { 757 | for num_evt, del_evt := range events { 758 | for _, del := range relay.DeleteEvent { 759 | if err := del(ctx, del_evt); err != nil { 760 | log.Printf("error deleting note %d of final batch. event id: %s", num_evt, del_evt.ID) 761 | return err 762 | } 763 | } 764 | } 765 | } 766 | 767 | if count == 0 { 768 | log.Println("0 old notes found") 769 | } else { 770 | log.Printf("%d old (until %d) notes deleted", count, oldAge) 771 | } 772 | 773 | return nil 774 | } 775 | 776 | func getDB() badger.BadgerBackend { 777 | return badger.BadgerBackend{ 778 | Path: getEnv("DB_PATH"), 779 | } 780 | } 781 | 782 | func splitAndTrim(input string) []string { 783 | items := strings.Split(input, ",") 784 | for i, item := range items { 785 | items[i] = strings.TrimSpace(item) 786 | } 787 | return items 788 | } 789 | 790 | func isIgnored(pubkey string, ignoredPubkeys []string) bool { 791 | for _, ignored := range ignoredPubkeys { 792 | if pubkey == ignored { 793 | return true 794 | } 795 | } 796 | return false 797 | } 798 | 799 | // Add memory monitoring 800 | func monitorMemoryUsage() { 801 | ticker := time.NewTicker(5 * time.Minute) 802 | defer ticker.Stop() 803 | 804 | for { 805 | select { 806 | case <-ticker.C: 807 | var m runtime.MemStats 808 | runtime.ReadMemStats(&m) 809 | 810 | relayMutex.RLock() 811 | relayCount := len(relays) 812 | relayMutex.RUnlock() 813 | 814 | trustNetworkMutex.RLock() 815 | trustNetworkCount := len(trustNetwork) 816 | trustNetworkMutex.RUnlock() 817 | 818 | oneHopMutex.RLock() 819 | oneHopCount := len(oneHopNetwork) 820 | oneHopMutex.RUnlock() 821 | 822 | followerMutex.RLock() 823 | followerCount := len(pubkeyFollowerCount) 824 | followerMutex.RUnlock() 825 | 826 | log.Printf("📊 Memory: Alloc=%d KB, Sys=%d KB, NumGC=%d", 827 | m.Alloc/1024, m.Sys/1024, m.NumGC) 828 | log.Printf("📊 Data structures: Relays=%d, TrustNetwork=%d, OneHop=%d, Followers=%d", 829 | relayCount, trustNetworkCount, oneHopCount, followerCount) 830 | } 831 | } 832 | } 833 | 834 | // Add performance monitoring 835 | func monitorPerformance() { 836 | ticker := time.NewTicker(1 * time.Minute) 837 | defer ticker.Stop() 838 | 839 | var lastGC uint32 840 | var lastEvents, lastRejected, lastArchived uint64 841 | 842 | for { 843 | select { 844 | case <-ticker.C: 845 | var m runtime.MemStats 846 | runtime.ReadMemStats(&m) 847 | 848 | currentEvents := atomic.LoadUint64(&totalEvents) 849 | currentRejected := atomic.LoadUint64(&rejectedEvents) 850 | currentArchived := atomic.LoadUint64(&archivedEvents) 851 | 852 | eventsPerMin := currentEvents - lastEvents 853 | rejectedPerMin := currentRejected - lastRejected 854 | archivedPerMin := currentArchived - lastArchived 855 | gcPerMin := m.NumGC - lastGC 856 | 857 | numGoroutines := runtime.NumGoroutine() 858 | 859 | log.Printf("⚡ Performance: Events/min=%d, Rejected/min=%d, Archived/min=%d, GC/min=%d, Goroutines=%d", 860 | eventsPerMin, rejectedPerMin, archivedPerMin, gcPerMin, numGoroutines) 861 | 862 | if gcPerMin > 60 { 863 | log.Printf("⚠️ HIGH GC ACTIVITY: %d garbage collections in last minute!", gcPerMin) 864 | } 865 | 866 | if numGoroutines > 1000 { 867 | log.Printf("⚠️ HIGH GOROUTINE COUNT: %d goroutines active!", numGoroutines) 868 | } 869 | 870 | lastGC = m.NumGC 871 | lastEvents = currentEvents 872 | lastRejected = currentRejected 873 | lastArchived = currentArchived 874 | } 875 | } 876 | } 877 | 878 | // Debug handlers 879 | func debugStatsHandler(w http.ResponseWriter, r *http.Request) { 880 | var m runtime.MemStats 881 | runtime.ReadMemStats(&m) 882 | 883 | stats := fmt.Sprintf(`Debug Statistics: 884 | 885 | Memory: 886 | Allocated: %d KB 887 | System: %d KB 888 | Total Allocations: %d 889 | GC Cycles: %d 890 | Goroutines: %d 891 | 892 | Events: 893 | Total Events: %d 894 | Rejected Events: %d 895 | Archived Events: %d 896 | Trusted Notes: %d 897 | Untrusted Notes: %d 898 | 899 | Refreshes: 900 | Profile Refreshes: %d 901 | Network Refreshes: %d 902 | 903 | Data Structures: 904 | Relays: %d 905 | Trust Network: %d 906 | One Hop Network: %d 907 | Follower Count Map: %d 908 | `, 909 | m.Alloc/1024, 910 | m.Sys/1024, 911 | m.Mallocs, 912 | m.NumGC, 913 | runtime.NumGoroutine(), 914 | atomic.LoadUint64(&totalEvents), 915 | atomic.LoadUint64(&rejectedEvents), 916 | atomic.LoadUint64(&archivedEvents), 917 | atomic.LoadUint64(&trustedNotes), 918 | atomic.LoadUint64(&untrustedNotes), 919 | atomic.LoadUint64(&profileRefreshCount), 920 | atomic.LoadUint64(&networkRefreshCount), 921 | len(relays), 922 | len(trustNetwork), 923 | len(oneHopNetwork), 924 | len(pubkeyFollowerCount), 925 | ) 926 | 927 | w.Header().Set("Content-Type", "text/plain") 928 | w.Write([]byte(stats)) 929 | } 930 | 931 | func debugGoroutinesHandler(w http.ResponseWriter, r *http.Request) { 932 | buf := make([]byte, 1<<20) // 1MB buffer 933 | stackSize := runtime.Stack(buf, true) 934 | 935 | w.Header().Set("Content-Type", "text/plain") 936 | w.Write(buf[:stackSize]) 937 | } 938 | --------------------------------------------------------------------------------