├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── backup.go ├── blastr.go ├── config.go ├── go.mod ├── go.sum ├── import.go ├── init.go ├── limits.go ├── main.go ├── relays_blastr.example.json ├── relays_import.example.json ├── templates ├── index.html └── static │ ├── android-chrome-192x192.png │ ├── android-chrome-256x256.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ └── safari-pinned-tab.svg ├── util.go └── wot.go /.env.example: -------------------------------------------------------------------------------- 1 | OWNER_NPUB="npub1utx00neqgqln72j22kej3ux7803c2k986henvvha4thuwfkper4s7r50e8" 2 | RELAY_URL="relay.utxo.one" 3 | RELAY_PORT=3355 4 | RELAY_BIND_ADDRESS="0.0.0.0" # Can be set to a specific IP4 or IP6 address ("" for all interfaces) 5 | DB_ENGINE="badger" # badger, lmdb (lmdb works best with an nvme, otherwise you might have stability issues) 6 | LMDB_MAPSIZE=0 # 0 for default (currently ~273GB), or set to a different size in bytes, e.g. 10737418240 for 10GB 7 | BLOSSOM_PATH="blossom/" 8 | 9 | ## Private Relay Settings 10 | PRIVATE_RELAY_NAME="utxo's private relay" 11 | PRIVATE_RELAY_NPUB="npub1utx00neqgqln72j22kej3ux7803c2k986henvvha4thuwfkper4s7r50e8" 12 | PRIVATE_RELAY_DESCRIPTION="A safe place to store my drafts and ecash" 13 | PRIVATE_RELAY_ICON="https://i.nostr.build/6G6wW.gif" 14 | 15 | ## Private Relay Rate Limiters 16 | PRIVATE_RELAY_EVENT_IP_LIMITER_TOKENS_PER_INTERVAL=50 17 | PRIVATE_RELAY_EVENT_IP_LIMITER_INTERVAL=1 18 | PRIVATE_RELAY_EVENT_IP_LIMITER_MAX_TOKENS=100 19 | PRIVATE_RELAY_ALLOW_EMPTY_FILTERS=true 20 | PRIVATE_RELAY_ALLOW_COMPLEX_FILTERS=true 21 | PRIVATE_RELAY_CONNECTION_RATE_LIMITER_TOKENS_PER_INTERVAL=3 22 | PRIVATE_RELAY_CONNECTION_RATE_LIMITER_INTERVAL=5 23 | PRIVATE_RELAY_CONNECTION_RATE_LIMITER_MAX_TOKENS=9 24 | 25 | ## Chat Relay Settings 26 | CHAT_RELAY_NAME="utxo's chat relay" 27 | CHAT_RELAY_NPUB="npub1utx00neqgqln72j22kej3ux7803c2k986henvvha4thuwfkper4s7r50e8" 28 | CHAT_RELAY_DESCRIPTION="a relay for private chats" 29 | CHAT_RELAY_ICON="https://i.nostr.build/6G6wW.gif" 30 | CHAT_RELAY_WOT_DEPTH=3 31 | CHAT_RELAY_WOT_REFRESH_INTERVAL_HOURS=24 32 | CHAT_RELAY_MINIMUM_FOLLOWERS=3 33 | 34 | ## Chat Relay Rate Limiters 35 | CHAT_RELAY_EVENT_IP_LIMITER_TOKENS_PER_INTERVAL=50 36 | CHAT_RELAY_EVENT_IP_LIMITER_INTERVAL=1 37 | CHAT_RELAY_EVENT_IP_LIMITER_MAX_TOKENS=100 38 | CHAT_RELAY_ALLOW_EMPTY_FILTERS=false 39 | CHAT_RELAY_ALLOW_COMPLEX_FILTERS=false 40 | CHAT_RELAY_CONNECTION_RATE_LIMITER_TOKENS_PER_INTERVAL=3 41 | CHAT_RELAY_CONNECTION_RATE_LIMITER_INTERVAL=3 42 | CHAT_RELAY_CONNECTION_RATE_LIMITER_MAX_TOKENS=9 43 | 44 | ## Outbox Relay Settings 45 | OUTBOX_RELAY_NAME="utxo's outbox relay" 46 | OUTBOX_RELAY_NPUB="npub1utx00neqgqln72j22kej3ux7803c2k986henvvha4thuwfkper4s7r50e8" 47 | OUTBOX_RELAY_DESCRIPTION="a relay and Blossom server for public messages and media" 48 | OUTBOX_RELAY_ICON="https://i.nostr.build/6G6wW.gif" 49 | 50 | ## Outbox Relay Rate Limiters 51 | OUTBOX_RELAY_EVENT_IP_LIMITER_TOKENS_PER_INTERVAL=10 52 | OUTBOX_RELAY_EVENT_IP_LIMITER_INTERVAL=60 53 | OUTBOX_RELAY_EVENT_IP_LIMITER_MAX_TOKENS=100 54 | OUTBOX_RELAY_ALLOW_EMPTY_FILTERS=false 55 | OUTBOX_RELAY_ALLOW_COMPLEX_FILTERS=false 56 | OUTBOX_RELAY_CONNECTION_RATE_LIMITER_TOKENS_PER_INTERVAL=3 57 | OUTBOX_RELAY_CONNECTION_RATE_LIMITER_INTERVAL=1 58 | OUTBOX_RELAY_CONNECTION_RATE_LIMITER_MAX_TOKENS=9 59 | 60 | ## Inbox Relay Settings 61 | INBOX_RELAY_NAME="utxo's inbox relay" 62 | INBOX_RELAY_NPUB="npub1utx00neqgqln72j22kej3ux7803c2k986henvvha4thuwfkper4s7r50e8" 63 | INBOX_RELAY_DESCRIPTION="send your interactions with my notes here" 64 | INBOX_RELAY_ICON="https://i.nostr.build/6G6wW.gif" 65 | INBOX_PULL_INTERVAL_SECONDS=600 66 | 67 | ## Inbox Relay Rate Limiters 68 | INBOX_RELAY_EVENT_IP_LIMITER_TOKENS_PER_INTERVAL=10 69 | INBOX_RELAY_EVENT_IP_LIMITER_INTERVAL=1 70 | INBOX_RELAY_EVENT_IP_LIMITER_MAX_TOKENS=20 71 | INBOX_RELAY_ALLOW_EMPTY_FILTERS=false 72 | INBOX_RELAY_ALLOW_COMPLEX_FILTERS=false 73 | INBOX_RELAY_CONNECTION_RATE_LIMITER_TOKENS_PER_INTERVAL=3 74 | INBOX_RELAY_CONNECTION_RATE_LIMITER_INTERVAL=1 75 | INBOX_RELAY_CONNECTION_RATE_LIMITER_MAX_TOKENS=9 76 | 77 | 78 | ## Import Settings 79 | IMPORT_START_DATE="2023-01-20" 80 | IMPORT_QUERY_INTERVAL_SECONDS=600 81 | IMPORT_OWNER_NOTES_FETCH_TIMEOUT_SECONDS=60 82 | IMPORT_TAGGED_NOTES_FETCH_TIMEOUT_SECONDS=120 83 | IMPORT_SEED_RELAYS_FILE="relays_import.json" 84 | 85 | ## Backup Settings 86 | BACKUP_PROVIDER="none" # s3, none (or leave blank to disable) 87 | BACKUP_INTERVAL_HOURS=1 88 | 89 | ## Generic S3 Bucket Backup Settings - REQUIRED IF BACKUP_PROVIDER="s3" 90 | S3_ACCESS_KEY_ID="access" 91 | S3_SECRET_KEY="secret" 92 | S3_ENDPOINT="nyc3.digitaloceanspaces.com" 93 | S3_REGION="nyc3" 94 | S3_BUCKET_NAME="backups" 95 | 96 | ## Blastr Settings 97 | BLASTR_RELAYS_FILE="relays_blastr.json" 98 | 99 | ## WOT Settings 100 | WOT_FETCH_TIMEOUT_SECONDS=60 101 | 102 | ## LOGGING 103 | HAVEN_LOG_LEVEL="INFO" # DEBUG, INFO, WARNING or ERROR 104 | 105 | # Docker 106 | LOG_FORMAT="$$host $$remote_addr - $$remote_user [$$time_local] \"$$request\" $$status $$body_bytes_sent \"$$http_referer\" \"$$http_user_agent\" \"$$upstream_addr\"" 107 | TZ="UTC" 108 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | relays_import.json 3 | relays_blastr.json 4 | haven 5 | .DS_Store 6 | .idea/ 7 | db.zip 8 | db/ 9 | blossom/ 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-2025 The HAVEN Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | associated documentation files (the "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial 12 | portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 15 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 16 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 18 | USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HAVEN 2 | 3 | HAVEN (High Availability Vault for Events on Nostr) is the most sovereign personal relay for the Nostr protocol, for storing and backing up sensitive notes like eCash, private chats and drafts. It is a relay that is not so dumb, with features like web of trust, inbox relay, cloud backups, blastr and the ability to import old notes. It even includes it's own blossom media server! 4 | 5 | ## Four Relays in One + Blossom Media Server 6 | 7 | **Private Relay**: This relay is only accessible by the owner of the relay. It is used for drafts, ecash and other private notes that nobody can read or write to. It is protected by Auth. 8 | 9 | **Chat Relay**: This relay is used to contact the owner by DM. Only people in the web of trust can interact with this relay, protected by Auth. It only accepts encrypted DMs and group chat kinds. 10 | 11 | **Inbox Relay**: This relay is where the owner of the relay reads from. Send your zaps, reactions and replies to this relay when you're tagging the owner. You can also pull notes from this relay if you want notes where the owner is tagged. This relay automatically pulls notes from other relays. Only notes where the owner is tagged will be accepted to this relay. 12 | 13 | **Outbox Relay**: This relay is where the owner's notes all live and are publicly accessible. You can import all your old notes to this relay. All notes sent to this relay are blasted to other relays. Only the owner can send to this relay, but anyone can read. 14 | 15 | **Blossom Media Server**: This relay also includes a media server for hosting images and videos. You can upload images and videos to this relay and get a link to share them. Only the relay owner can upload to this relay, but anyone can view the images and videos. 16 | 17 | ## Not So Dumb Relay Features 18 | 19 | **Web of Trust**: Protected from DM and Inbox spam by using a web of trust. 20 | 21 | **Inbox Relay**: Notes are pulled from other relays and stored in the inbox relay. 22 | 23 | **Cloud Backups**: Notes are backed up in the cloud and can be restored if the relay is lost. 24 | 25 | **Blastr**: Notes sent to the outbox are also blasted to other relays. 26 | 27 | **Import Old Notes**: Import your old notes and notes you're tagged in from other relays. 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 | 33 | ```bash 34 | sudo apt update #Update Package List 35 | sudo apt install snapd #install snapd to get a newer version of Go 36 | sudo snap install go --classic #Install Go 37 | go version #check if go was installed correctly 38 | ``` 39 | 40 | - **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`. 41 | 42 | ## Setup Instructions 43 | 44 | Follow these steps to get the Haven Relay running on your local machine: 45 | 46 | ### 1. Clone the repository 47 | 48 | ```bash 49 | git clone https://github.com/bitvora/haven.git 50 | cd haven 51 | ``` 52 | 53 | ### 2. Copy `.env.example` to `.env` 54 | 55 | You'll need to create an `.env` file based on the example provided in the repository. 56 | 57 | ```bash 58 | cp .env.example .env 59 | ``` 60 | 61 | ### 3. Set your environment variables 62 | 63 | Open the `.env` file and set the necessary environment variables. 64 | 65 | ### 4. Create the relays JSON files 66 | 67 | Copy the example relays JSON files for your seed and blastr relays: 68 | 69 | ```bash 70 | cp relays_import.example.json relays_import.json 71 | ``` 72 | 73 | ```bash 74 | cp relays_blastr.example.json relays_blastr.json 75 | ``` 76 | 77 | The JSON should contain an array of relay URLs, which default to wss:// if you don't explicitly specify the protocol. 78 | 79 | ### 4. Build the project 80 | 81 | Run the following command to build the relay: 82 | 83 | ```bash 84 | go build 85 | ``` 86 | 87 | ### 5. Create a Systemd Service 88 | 89 | 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. 90 | and Replace the values for `ExecStart` and `WorkingDirectory` with the actual paths where you cloned the repository and stored the `.env` file. 91 | 92 | 93 | 1. Create the file: 94 | 95 | ```bash 96 | sudo nano /etc/systemd/system/haven.service 97 | ``` 98 | 99 | 2. Add the following contents: 100 | 101 | ```ini 102 | [Unit] 103 | Description=Haven Relay 104 | After=network.target 105 | 106 | [Service] 107 | ExecStart=/home/ubuntu/haven/haven #Edit path to point to the path of where the haven git was pulled 108 | WorkingDirectory=/home/ubuntu/haven #Edit path to point to the path of where the haven git was pulled 109 | MemoryLimit=1000M # Example, Limit memory usage to 1000 MB | Edit this to fit your machine 110 | Restart=always 111 | 112 | [Install] 113 | WantedBy=multi-user.target 114 | ``` 115 | 116 | 117 | 3. Reload systemd to recognize the new service: 118 | 119 | ```bash 120 | sudo systemctl daemon-reload 121 | ``` 122 | 123 | 4. Start the service: 124 | 125 | ```bash 126 | sudo systemctl start haven 127 | ``` 128 | 129 | 5. (Optional) Enable the service to start on boot: 130 | 131 | ```bash 132 | sudo systemctl enable haven 133 | ``` 134 | 135 | ### 6. Serving over nginx (optional) 136 | 137 | To have a domain name (example: relay.domain.com) point to your machine, you will need to setup an nginx. 138 | 139 | 1. Install nginx on your relay: 140 | 141 | ```bash 142 | sudo apt-get update 143 | sudo apt-get install nginx 144 | ``` 145 | 146 | 2. Remove default config: `sudo rm -rf /etc/nginx/sites-available/default` 147 | 148 | 3. Create new default config: `sudo nano /etc/nginx/sites-available/default` 149 | 150 | 4. Add new reverse proxy config by adding the following configuration to your nginx configuration file: 151 | 152 | ```nginx 153 | server { 154 | listen 80; 155 | server_name yourdomain.com; 156 | client_max_body_size 100m; 157 | 158 | location / { 159 | proxy_pass http://localhost:3355; 160 | proxy_set_header Host $host; 161 | proxy_set_header X-Real-IP $remote_addr; 162 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 163 | proxy_set_header X-Forwarded-Proto $scheme; 164 | proxy_http_version 1.1; 165 | proxy_set_header Upgrade $http_upgrade; 166 | proxy_set_header Connection "upgrade"; 167 | } 168 | } 169 | ``` 170 | 171 | Replace `yourdomain.com` with your actual domain name. 172 | 173 | > [!NOTE] 174 | > [`client_max_body_size`](https://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size) is set to 100m 175 | > to allow for larger media files to be uploaded to Blossom. `0` can be used to allow for unlimited file sizes. If you are 176 | > using Cloudflare proxy, be mindful of [upload limits](https://community.cloudflare.com/t/maximum-upload-size-is-limit/418490/2). 177 | 178 | After adding the configuration, restart nginx: 179 | 180 | ```bash 181 | sudo systemctl restart nginx 182 | ``` 183 | 184 | ### Alternative: Serving over Caddy 185 |
Click here to view the installation routine for Caddy 186 |

187 | 188 | Preparation: Set the A record (for your domain) to point to the server's IP address. 189 | 190 | 1. Install caddy: 191 | 192 | ```bash 193 | sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https 194 | curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg 195 | curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list 196 | sudo apt update 197 | sudo apt install caddy 198 | ``` 199 | 200 | 2. Open Caddyfile: 201 | 202 | ```bash 203 | sudo nano /etc/caddy/Caddyfile 204 | ``` 205 | 206 | 3. Add configuration: 207 | 208 | ```bash 209 | # Configuration for HAVEN Relay 210 | yourdomain.com { 211 | reverse_proxy localhost:3355 { 212 | header_up Host {host} 213 | header_up X-Real-IP {remote_host} 214 | header_up X-Forwarded-For {remote_host} 215 | header_up X-Forwarded-Proto {scheme} 216 | transport http { 217 | versions 1.1 218 | } 219 | } 220 | request_body { 221 | max_size 100MB 222 | } 223 | } 224 | ``` 225 | 226 | 4. Reload Caddy: 227 | 228 | ```bash 229 | sudo systemctl reload caddy 230 | ``` 231 | 232 | 5. Check status logs: 233 | 234 | ```bash 235 | sudo systemctl status caddy 236 | sudo journalctl -u caddy -f --since "2 hour ago" 237 | ``` 238 | 239 | **Note:** Caddy automatically manages certificates and WebSocket connections. Certbot is not required. 240 | 241 |

242 |
243 | 244 | ### 7. Install Certbot (optional) 245 | 246 | If you want to serve the relay over HTTPS, you can use Certbot to generate an SSL certificate. 247 | 248 | ```bash 249 | sudo apt-get update 250 | sudo apt-get install certbot python3-certbot-nginx 251 | ``` 252 | 253 | After installing Certbot, run the following command to generate an SSL certificate: 254 | 255 | ```bash 256 | sudo certbot --nginx 257 | ``` 258 | 259 | Follow the instructions to generate the certificate. 260 | 261 | Note: Command will fail if the Domain you added to nginx is not yet pointing at your machine's IP address. 262 | This is done by adding an A record subdomain pointing to your IP address through your DNS recrods Manager. 263 | 264 | ### 8. Run The Import (optional) 265 | 266 | If you want to import your old notes and notes you're tagged in from other relays, run the following command: 267 | 268 | ```bash 269 | sudo systemctl stop haven 270 | ./haven --import 271 | sudo systemctl start haven 272 | ``` 273 | 274 | ### 9. Access the relay 275 | 276 | Once everything is set up, the relay will be running on `localhost:3355` with the following endpoints: 277 | 278 | - `localhost:3355` (outbox and Blossom server) 279 | - `localhost:3355/private` 280 | - `localhost:3355/chat` 281 | - `localhost:3355/inbox` 282 | 283 | ## Start the Project with Docker Compose 284 | 285 | To start the project using Docker Compose, follow these steps: 286 | 287 | 1. Ensure Docker and Docker Compose are installed on your system. 288 | 2. Navigate to the project directory. 289 | 3. Ensure the `.env` file is present in the project directory and has the necessary environment variables set. 290 | 4. You'll also need to expose ports 80 and 443 to the internet and set up your DNS A and AAAA (if you are using IPv6) 291 | records to point to your server's IP address. 292 | 5. (Optional) You can also change the paths of the `blossom`, `db`, and `templates` folders in the `compose.yml` file. 293 | 294 | ```yaml 295 | volumes: 296 | - ./blossom:/haven/blossom # only change the left side before the colon 297 | - ./db:/haven/db 298 | - ./templates:/haven/templates 299 | ``` 300 | 6. (Optional) Nginx is pre-configured to reject uploads larger than 100MB. If you want to change this, modify the `client_max_body_size` 301 | directive in the `nginx/haven_proxy.conf file`. 302 | 303 | ```nginx 304 | client_max_body_size 0; 305 | ``` 306 | 307 | 7. Run the following command: 308 | 309 | ```sh 310 | # in foreground 311 | docker compose up --build 312 | # in background 313 | docker compose up --build -d 314 | ``` 315 | 316 | 8. For updating the relay, run the following commands: 317 | 318 | ```sh 319 | git pull 320 | docker compose down 321 | docker compose build 322 | # in foreground 323 | docker compose up 324 | # in background 325 | docker compose up -d 326 | ``` 327 | 328 | ## Database 329 | 330 | Haven currently supports [BadgerDB](https://github.com/dgraph-io/badger) and [LMDB](https://www.symas.com/mdb) as embedded 331 | databases, meaning no external database is required. 332 | 333 | By default, Haven uses BadgerDB. To switch to LMDB, set the `DB_ENGINE` environment variable to `lmdb` in the `.env` file. 334 | 335 | LMDB can be faster than BadgerDB but performs best with NVMe drives and may require fine-tuning based on factors such as 336 | database size, operating system, file system, and hardware. 337 | 338 | ### LMDB Map Size 339 | 340 | There is no one-size-fits-all value for LMDB’s map size. Windows and macOS users, in particular, may need 341 | to adjust the `LMDB_MAPSIZE` environment variable to a value lower than the available free disk space if the default 342 | value of 273 GB is too high. Otherwise, Haven will fail to bootstrap. Users with large databases may also need to 343 | increase the `LMDB_MAPSIZE` value above the default. On most systems, the default value should work fine. 344 | 345 | Despite the large default value, on most modern systems LMDB will only use the disk space it needs. The map size simply 346 | defines an upper limit for the database size. For more information about LMDB’s map size, refer to the 347 | [LMDB documentation](http://www.lmdb.tech/doc/group__mdb.html#gaa2506ec8dab3d969b0e609cd82e619e5). 348 | 349 | ### Migrating from databases created in older versions of Haven 350 | 351 | Haven versions 1.0.3 and earlier did not replace outdated notes. While this does not impact the relay's core 352 | functionality, it can lead to a bloated database, reduced performance, and bugs in certain clients. For this reason, it 353 | is recommended to delete old databases and start fresh, optionally [re-importing](#8-run-the-import-optional) previous notes. 354 | 355 | ## Blossom Media Server 356 | 357 | The outbox relay also functions as a media server for hosting images and videos. You can upload media files to the relay and obtain a shareable link. 358 | Only the relay owner has upload permissions to the media server, but anyone can view the hosted images and videos. 359 | 360 | Media files are stored in the file system based on the `BLOSSOM_PATH` environment variable set in the `.env` file. The default path is `./blossom`. 361 | 362 | ## Cloud Backups 363 | 364 | The relay automatically backs up your database to a cloud provider of your choice. 365 | 366 | ### S3-Compatible Object Storage 367 | 368 | To back up your database to S3 compatible storage such as [AWS S3](https://aws.amazon.com/s3/), 369 | [GCP Cloud Storage] or 370 | [DigitalOcean Spaces](https://www.digitalocean.com/products/spaces). 371 | 372 | First need to create the bucket on your provider. After creating the Bucket you will be provided with: 373 | 374 | - Access Key ID 375 | - Secret Key 376 | - URL Endpoint 377 | - Region 378 | - Bucket Name 379 | 380 | Once you have this data, update your `.env` file with the appropriate information: 381 | 382 | ```Dotenv 383 | S3_ACCESS_KEY_ID="your_access_key_id" 384 | S3_SECRET_KEY="your_secret_key" 385 | S3_ENDPOINT="your_endpoint" 386 | S3_REGION="your_region" 387 | S3_BUCKET_NAME="your_bucket" 388 | ``` 389 | 390 | Replace `your_access_key_id`, `your_secret_access_key`, `your_region`, and `your_bucket` with your actual credentials. 391 | 392 | You may also want to set the `BACKUP_INTERVAL_HOURS` environment variable to specify how often the relay should back up 393 | the database. 394 | 395 | ```Dotenv 396 | BACKUP_INTERVAL_HOURS=24 397 | ``` 398 | 399 | Finally, you need to specifiy `s3` as the backup provider: 400 | 401 | ```Dotenv 402 | BACKUP_PROVIDER="s3" # s3, none (or leave blank to disable) 403 | ``` 404 | 405 | #### AWS S3 406 | 407 | For AWS S3, set the appropriate endpoint for your region/availability zone: 408 | 409 | ```Dotenv 410 | S3_ACCESS_KEY="AKIAIOSFODNN7EXAMPLE" 411 | S3_SECRET_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" 412 | S3_ENDPOINT="s3.us-east-1.amazonaws.com"" 413 | S3_REGION="us-east-1" 414 | S3_BUCKET_NAME="haven_backup" 415 | ``` 416 | 417 | #### GCP Cloud Storage 418 | 419 | For GCP, you can set `S3_ENDPOINT` to `storage.googleapis.com`. 420 | 421 | `S3_REGION` can be left blank. `S3_ACCESS_KEY_ID` and `S3_SECRET_KEY` needs to be set to a [HMAC key]( 422 | https://cloud.google.com/storage/docs/authentication/hmackeys), see GCP's official documentation on [how to create a HMAC 423 | key for a service account](https://cloud.google.com/storage/docs/authentication/managing-hmackeys#create). 424 | 425 | ```Dotenv 426 | S3_ACCESS_KEY_ID="GOOGXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 427 | S3_SECRET_KEY="Yyy+YYY0/yYYYYyyyy0+YyyYyyYyyYyyyyYyyYyy" 428 | S3_ENDPOINT="storage.googleapis.com" 429 | S3_REGION="" 430 | S3_BUCKET_NAME="haven_backup" 431 | ``` 432 | 433 | #### DigitalOcean Spaces 434 | 435 | To back up your database to DigitalOcean Spaces, you'll first need to create a bucket in the DigitalOcean dashboard. 436 | This can be done in the "Spaces Object Storage" tab or by visiting https://cloud.digitalocean.com/spaces. 437 | 438 | Once you have created a bucket you will be shown an access key ID and a secret key. Additionally, 439 | while creating the bucket you will have selected a region to host this bucket which has a URL. For example, 440 | if you choose the datacenter region "Amsterdam - Datacenter 3 - AMS3", your region will be `ams3` and 441 | the endpoint will be `ams3.digitalocean.com`. 442 | 443 | ### Deprecation warning 444 | 445 | The old `aws` and `gcp` backup providers have been deprecated in favor of the new `s3` provider. If you are using the 446 | old providers, please update your `.env` file to use the new `s3` provider. The old providers will be removed in a future 447 | release. 448 | 449 | ## License 450 | 451 | This project is licensed under the [MIT](./LICENSE) License. 452 | -------------------------------------------------------------------------------- /backup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/zip" 5 | "context" 6 | "io" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "time" 11 | 12 | "cloud.google.com/go/storage" 13 | "github.com/minio/minio-go/v7" 14 | "github.com/minio/minio-go/v7/pkg/credentials" 15 | ) 16 | 17 | func backupDatabase() { 18 | if config.BackupProvider == "none" || config.BackupProvider == "" { 19 | log.Println("🚫 no backup provider set") 20 | return 21 | } 22 | 23 | ticker := time.NewTicker(time.Duration(config.BackupIntervalHours) * time.Hour) 24 | defer ticker.Stop() 25 | 26 | zipFileName := "db.zip" 27 | for { 28 | select { 29 | case <-ticker.C: 30 | if err := ZipDirectory("db", zipFileName); err != nil { 31 | log.Println("🚫 error zipping database folder:", err) 32 | continue 33 | } 34 | switch config.BackupProvider { 35 | case "s3": 36 | S3Upload(zipFileName) 37 | case "aws": 38 | AwsUpload(zipFileName) 39 | case "gcp": 40 | GCPBucketUpload(zipFileName) 41 | default: 42 | log.Println("🚫 we only support AWS, GCP, and S3 at this time") 43 | } 44 | } 45 | } 46 | } 47 | 48 | // Deprecated: Use S3Upload instead 49 | // 50 | //goland:noinspection GoUnhandledErrorResult 51 | func GCPBucketUpload(zipFileName string) { 52 | if config.GcpConfig == nil { 53 | log.Fatal("🚫 GCP specified as backup provider but no GCP config found. Check environment variables.") 54 | } 55 | 56 | bucket := config.GcpConfig.Bucket 57 | 58 | ctx := context.Background() 59 | 60 | client, err := storage.NewClient(ctx) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | defer client.Close() 65 | 66 | // open the zip db file. 67 | f, err := os.Open(zipFileName) 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | defer f.Close() 72 | 73 | obj := client.Bucket(bucket).Object(zipFileName) 74 | 75 | // Upload an object with storage.Writer. 76 | wc := obj.NewWriter(ctx) 77 | if _, err = io.Copy(wc, f); err != nil { 78 | log.Fatal(err) 79 | } 80 | 81 | if err := wc.Close(); err != nil { 82 | log.Fatal(err) 83 | } 84 | 85 | log.Printf("✅ Successfully uploaded %q to %q\n", zipFileName, bucket) 86 | 87 | // delete the file. 88 | err = os.Remove(zipFileName) 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | } 93 | 94 | // Deprecated: Use S3Upload instead 95 | // 96 | //goland:noinspection GoUnhandledErrorResult 97 | func AwsUpload(zipFileName string) { 98 | if config.AwsConfig == nil { 99 | log.Fatal("🚫 AWS specified as backup provider but no AWS config found. Check environment variables.") 100 | } 101 | 102 | s3UploadShared( 103 | zipFileName, 104 | config.AwsConfig.AccessKeyID, 105 | config.AwsConfig.SecretAccessKey, 106 | "s3.amazonaws.com", 107 | config.AwsConfig.Region, 108 | config.AwsConfig.Bucket, 109 | true, 110 | ) 111 | } 112 | 113 | func S3Upload(zipFileName string) { 114 | if config.S3Config == nil { 115 | log.Fatal("🚫 S3 specified as backup provider but no S3 config found. Check environment variables.") 116 | } 117 | 118 | s3UploadShared( 119 | zipFileName, 120 | config.S3Config.AccessKeyID, 121 | config.S3Config.SecretKey, 122 | config.S3Config.Endpoint, 123 | config.S3Config.Region, 124 | config.S3Config.BucketName, 125 | true, 126 | ) 127 | } 128 | 129 | func s3UploadShared( 130 | zipFileName string, 131 | accessKey string, 132 | secret string, 133 | endpoint string, 134 | region string, 135 | bucketName string, 136 | secure bool, 137 | ) { 138 | log.Println("🚀 uploading to S3 Bucket...") 139 | 140 | // Create MinIO client 141 | client, err := minio.New(endpoint, &minio.Options{ 142 | Creds: credentials.NewStaticV4(accessKey, secret, ""), 143 | Region: region, 144 | Secure: secure, 145 | }) 146 | if err != nil { 147 | log.Fatal(err) 148 | } 149 | 150 | // Upload the file to the S3 bucket 151 | file, err := os.Open(zipFileName) 152 | if err != nil { 153 | log.Fatal(err) 154 | } 155 | defer func(file *os.File) { 156 | if err := file.Close(); err != nil { 157 | log.Println("🚫 error closing db zip file:", err) 158 | } 159 | }(file) 160 | 161 | fileInfo, err := file.Stat() 162 | if err != nil { 163 | log.Fatal(err) 164 | } 165 | 166 | _, err = client.PutObject( 167 | context.Background(), 168 | bucketName, 169 | zipFileName, 170 | file, 171 | fileInfo.Size(), 172 | minio.PutObjectOptions{ 173 | ContentType: "application/octet-stream", 174 | }, 175 | ) 176 | if err != nil { 177 | log.Fatal(err) 178 | } 179 | 180 | log.Printf("✅ Successfully uploaded %q to %q\n", zipFileName, bucketName) 181 | 182 | // delete the file 183 | err = os.Remove(zipFileName) 184 | if err != nil { 185 | log.Fatal(err) 186 | } 187 | } 188 | 189 | //goland:noinspection GoUnhandledErrorResult 190 | func ZipDirectory(sourceDir, zipFileName string) error { 191 | log.Println("📦 zipping up the database") 192 | file, err := os.Create(zipFileName) 193 | if err != nil { 194 | panic(err) 195 | } 196 | defer file.Close() 197 | 198 | w := zip.NewWriter(file) 199 | defer w.Close() 200 | 201 | walker := func(path string, info os.FileInfo, err error) error { 202 | if err != nil { 203 | return err 204 | } 205 | if info.IsDir() { 206 | return nil 207 | } 208 | file, err := os.Open(path) 209 | if err != nil { 210 | return err 211 | } 212 | defer file.Close() 213 | f, err := w.Create(path) 214 | if err != nil { 215 | return err 216 | } 217 | 218 | _, err = io.Copy(f, file) 219 | if err != nil { 220 | return err 221 | } 222 | 223 | return nil 224 | } 225 | err = filepath.Walk(sourceDir, walker) 226 | if err != nil { 227 | //panic(err) 228 | } 229 | 230 | log.Println("📦 database zipped up!") 231 | return nil 232 | } 233 | -------------------------------------------------------------------------------- /blastr.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/nbd-wtf/go-nostr" 9 | ) 10 | 11 | func blast(ev *nostr.Event) { 12 | ctx := context.Background() 13 | for _, url := range config.BlastrRelays { 14 | ctx, cancel := context.WithTimeout(ctx, time.Second*5) 15 | relay, err := pool.EnsureRelay(url) 16 | if err != nil { 17 | cancel() 18 | log.Println("error connecting to relay", relay, err) 19 | continue 20 | } 21 | if err := relay.Publish(ctx, *ev); err != nil { 22 | log.Println("🚫 error publishing to relay", relay, err) 23 | } 24 | cancel() 25 | } 26 | log.Println("🔫 blasted", ev.ID, "to", len(config.BlastrRelays), "relays") 27 | } 28 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/joho/godotenv" 11 | ) 12 | 13 | type AwsConfig struct { 14 | AccessKeyID string `json:"access"` 15 | SecretAccessKey string `json:"secret"` 16 | Region string `json:"region"` 17 | Bucket string `json:"bucket"` 18 | } 19 | 20 | type GcpConfig struct { 21 | Bucket string `json:"bucket"` 22 | } 23 | 24 | type S3Config struct { 25 | AccessKeyID string `json:"access_key_id"` 26 | SecretKey string `json:"secret_key"` 27 | Endpoint string `json:"endpoint"` 28 | BucketName string `json:"bucket_name"` 29 | Region string `json:"region"` 30 | } 31 | 32 | type Config struct { 33 | OwnerNpub string `json:"owner_npub"` 34 | DBEngine string `json:"db_engine"` 35 | LmdbMapSize int64 `json:"lmdb_map_size"` 36 | BlossomPath string `json:"blossom_path"` 37 | RelayURL string `json:"relay_url"` 38 | RelayPort int `json:"relay_port"` 39 | RelayBindAddress string `json:"relay_bind_address"` 40 | RelaySoftware string `json:"relay_software"` 41 | RelayVersion string `json:"relay_version"` 42 | PrivateRelayName string `json:"private_relay_name"` 43 | PrivateRelayNpub string `json:"private_relay_npub"` 44 | PrivateRelayDescription string `json:"private_relay_description"` 45 | PrivateRelayIcon string `json:"private_relay_icon"` 46 | ChatRelayName string `json:"chat_relay_name"` 47 | ChatRelayNpub string `json:"chat_relay_npub"` 48 | ChatRelayDescription string `json:"chat_relay_description"` 49 | ChatRelayIcon string `json:"chat_relay_icon"` 50 | ChatRelayWotDepth int `json:"chat_relay_wot_depth"` 51 | ChatRelayWotRefreshIntervalHours int `json:"chat_relay_wot_refresh_interval_hours"` 52 | ChatRelayMinimumFollowers int `json:"chat_relay_minimum_followers"` 53 | OutboxRelayName string `json:"outbox_relay_name"` 54 | OutboxRelayNpub string `json:"outbox_relay_npub"` 55 | OutboxRelayDescription string `json:"outbox_relay_description"` 56 | OutboxRelayIcon string `json:"outbox_relay_icon"` 57 | InboxRelayName string `json:"inbox_relay_name"` 58 | InboxRelayNpub string `json:"inbox_relay_npub"` 59 | InboxRelayDescription string `json:"inbox_relay_description"` 60 | InboxRelayIcon string `json:"inbox_relay_icon"` 61 | InboxPullIntervalSeconds int `json:"inbox_pull_interval_seconds"` 62 | ImportStartDate string `json:"import_start_date"` 63 | ImportOwnerNotesFetchTimeoutSeconds int `json:"import_owned_notes_fetch_timeout_seconds"` 64 | ImportTaggedNotesFetchTimeoutSeconds int `json:"import_tagged_fetch_timeout_seconds"` 65 | ImportQueryIntervalSeconds int `json:"import_query_interval_seconds"` 66 | ImportSeedRelays []string `json:"import_seed_relays"` 67 | BackupProvider string `json:"backup_provider"` 68 | BackupIntervalHours int `json:"backup_interval_hours"` 69 | WotFetchTimeoutSeconds int `json:"wot_fetch_timeout_seconds"` 70 | LogLevel string `json:"log_level"` 71 | BlastrRelays []string `json:"blastr_relays"` 72 | AwsConfig *AwsConfig `json:"aws_config"` 73 | S3Config *S3Config `json:"s3_config"` 74 | GcpConfig *GcpConfig `json:"gcp_config"` 75 | } 76 | 77 | func loadConfig() Config { 78 | _ = godotenv.Load(".env") 79 | 80 | return Config{ 81 | OwnerNpub: getEnv("OWNER_NPUB"), 82 | DBEngine: getEnvString("DB_ENGINE", "lmdb"), 83 | LmdbMapSize: getEnvInt64("LMDB_MAPSIZE", 0), 84 | BlossomPath: getEnvString("BLOSSOM_PATH", "blossom"), 85 | RelayURL: getEnv("RELAY_URL"), 86 | RelayPort: getEnvInt("RELAY_PORT", 3355), 87 | RelayBindAddress: getEnvString("RELAY_BIND_ADDRESS", "0.0.0.0"), 88 | RelaySoftware: "https://github.com/bitvora/haven", 89 | RelayVersion: "v1.0.5", 90 | PrivateRelayName: getEnv("PRIVATE_RELAY_NAME"), 91 | PrivateRelayNpub: getEnv("PRIVATE_RELAY_NPUB"), 92 | PrivateRelayDescription: getEnv("PRIVATE_RELAY_DESCRIPTION"), 93 | PrivateRelayIcon: getEnv("PRIVATE_RELAY_ICON"), 94 | ChatRelayName: getEnv("CHAT_RELAY_NAME"), 95 | ChatRelayNpub: getEnv("CHAT_RELAY_NPUB"), 96 | ChatRelayDescription: getEnv("CHAT_RELAY_DESCRIPTION"), 97 | ChatRelayIcon: getEnv("CHAT_RELAY_ICON"), 98 | ChatRelayWotDepth: getEnvInt("CHAT_RELAY_WOT_DEPTH", 0), 99 | ChatRelayWotRefreshIntervalHours: getEnvInt("CHAT_RELAY_WOT_REFRESH_INTERVAL_HOURS", 0), 100 | ChatRelayMinimumFollowers: getEnvInt("CHAT_RELAY_MINIMUM_FOLLOWERS", 0), 101 | OutboxRelayName: getEnv("OUTBOX_RELAY_NAME"), 102 | OutboxRelayNpub: getEnv("OUTBOX_RELAY_NPUB"), 103 | OutboxRelayDescription: getEnv("OUTBOX_RELAY_DESCRIPTION"), 104 | OutboxRelayIcon: getEnv("OUTBOX_RELAY_ICON"), 105 | InboxRelayName: getEnv("INBOX_RELAY_NAME"), 106 | InboxRelayNpub: getEnv("INBOX_RELAY_NPUB"), 107 | InboxRelayDescription: getEnv("INBOX_RELAY_DESCRIPTION"), 108 | InboxRelayIcon: getEnv("INBOX_RELAY_ICON"), 109 | InboxPullIntervalSeconds: getEnvInt("INBOX_PULL_INTERVAL_SECONDS", 3600), 110 | ImportStartDate: getEnv("IMPORT_START_DATE"), 111 | ImportOwnerNotesFetchTimeoutSeconds: getEnvInt("IMPORT_OWNER_NOTES_FETCH_TIMEOUT_SECONDS", 60), 112 | ImportTaggedNotesFetchTimeoutSeconds: getEnvInt("IMPORT_TAGGED_NOTES_FETCH_TIMEOUT_SECONDS", 120), 113 | ImportQueryIntervalSeconds: getEnvInt("IMPORT_QUERY_INTERVAL_SECONDS", 360000), 114 | ImportSeedRelays: getRelayListFromFile(getEnv("IMPORT_SEED_RELAYS_FILE")), 115 | BackupProvider: getEnv("BACKUP_PROVIDER"), 116 | BackupIntervalHours: getEnvInt("BACKUP_INTERVAL_HOURS", 24), 117 | WotFetchTimeoutSeconds: getEnvInt("WOT_FETCH_TIMEOUT_SECONDS", 60), 118 | LogLevel: getEnvString("HAVEN_LOG_LEVEL", "INFO"), 119 | BlastrRelays: getRelayListFromFile(getEnv("BLASTR_RELAYS_FILE")), 120 | AwsConfig: getAwsConfig(), 121 | S3Config: getS3Config(), 122 | GcpConfig: getGcpConfig(), 123 | } 124 | } 125 | 126 | func getAwsConfig() *AwsConfig { 127 | backupProvider := getEnv("BACKUP_PROVIDER") 128 | 129 | if backupProvider == "aws" { 130 | return &AwsConfig{ 131 | AccessKeyID: getEnv("AWS_ACCESS_KEY_ID"), 132 | SecretAccessKey: getEnv("AWS_SECRET_ACCESS_KEY"), 133 | Region: getEnv("AWS_REGION"), 134 | Bucket: getEnv("AWS_BUCKET"), 135 | } 136 | } 137 | 138 | return nil 139 | } 140 | 141 | func getS3Config() *S3Config { 142 | backupProvider := getEnv("BACKUP_PROVIDER") 143 | 144 | if backupProvider == "s3" { 145 | return &S3Config{ 146 | AccessKeyID: getEnv("S3_ACCESS_KEY_ID"), 147 | SecretKey: getEnv("S3_SECRET_KEY"), 148 | Endpoint: getEnv("S3_ENDPOINT"), 149 | BucketName: getEnv("S3_BUCKET_NAME"), 150 | Region: getEnv("S3_REGION"), 151 | } 152 | } 153 | 154 | return nil 155 | } 156 | 157 | func getGcpConfig() *GcpConfig { 158 | backupProvider := getEnv("BACKUP_PROVIDER") 159 | 160 | if backupProvider == "gcp" { 161 | return &GcpConfig{ 162 | Bucket: getEnv("GCP_BUCKET_NAME"), 163 | } 164 | } 165 | 166 | return nil 167 | } 168 | 169 | func getRelayListFromFile(filePath string) []string { 170 | file, err := os.ReadFile(filePath) 171 | if err != nil { 172 | log.Fatalf("Failed to read file: %s", err) 173 | } 174 | 175 | var relayList []string 176 | if err := json.Unmarshal(file, &relayList); err != nil { 177 | log.Fatalf("Failed to parse JSON: %s", err) 178 | } 179 | 180 | for i, relay := range relayList { 181 | relay = strings.TrimSpace(relay) 182 | if !strings.HasPrefix(relay, "wss://") && !strings.HasPrefix(relay, "ws://") { 183 | relay = "wss://" + relay 184 | } 185 | relayList[i] = relay 186 | } 187 | return relayList 188 | } 189 | 190 | func getEnv(key string) string { 191 | value, exists := os.LookupEnv(key) 192 | if !exists { 193 | log.Fatalf("Environment variable %s not set", key) 194 | } 195 | return value 196 | } 197 | 198 | func getEnvString(key string, defaultValue string) string { 199 | if value, ok := os.LookupEnv(key); ok { 200 | return value 201 | } 202 | return defaultValue 203 | } 204 | 205 | func getEnvInt(key string, defaultValue int) int { 206 | if value, ok := os.LookupEnv(key); ok { 207 | intValue, err := strconv.Atoi(value) 208 | if err != nil { 209 | panic(err) 210 | } 211 | return intValue 212 | } 213 | return defaultValue 214 | } 215 | 216 | func getEnvInt64(key string, defaultValue int64) int64 { 217 | if value, ok := os.LookupEnv(key); ok { 218 | intValue, err := strconv.ParseInt(value, 10, 64) 219 | if err != nil { 220 | panic(err) 221 | } 222 | return intValue 223 | } 224 | return defaultValue 225 | } 226 | 227 | func getEnvBool(key string, defaultValue bool) bool { 228 | if value, ok := os.LookupEnv(key); ok { 229 | boolValue, err := strconv.ParseBool(value) 230 | if err != nil { 231 | panic(err) 232 | } 233 | return boolValue 234 | } 235 | return defaultValue 236 | } 237 | 238 | var art = ` 239 | ██╗ ██╗ █████╗ ██╗ ██╗███████╗███╗ ██╗ 240 | ██║ ██║██╔══██╗██║ ██║██╔════╝████╗ ██║ 241 | ███████║███████║██║ ██║█████╗ ██╔██╗ ██║ 242 | ██╔══██║██╔══██║╚██╗ ██╔╝██╔══╝ ██║╚██╗██║ 243 | ██║ ██║██║ ██║ ╚████╔╝ ███████╗██║ ╚████║ 244 | ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═══╝ 245 | HIGH AVAILABILITY VAULT FOR EVENTS ON NOSTR 246 | ` 247 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bitvora/haven 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | cloud.google.com/go/storage v1.52.0 7 | github.com/fiatjaf/eventstore v0.17.1 8 | github.com/fiatjaf/khatru v0.18.1 9 | github.com/joho/godotenv v1.5.1 10 | github.com/minio/minio-go/v7 v7.0.90 11 | github.com/nbd-wtf/go-nostr v0.51.12 12 | github.com/spf13/afero v1.11.0 13 | ) 14 | 15 | require ( 16 | cel.dev/expr v0.23.1 // indirect 17 | cloud.google.com/go v0.121.0 // indirect 18 | cloud.google.com/go/auth v0.16.1 // indirect 19 | cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect 20 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 21 | cloud.google.com/go/iam v1.5.2 // indirect 22 | cloud.google.com/go/monitoring v1.24.2 // indirect 23 | fiatjaf.com/lib v0.2.0 // indirect 24 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect 25 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect 26 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect 27 | github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect 28 | github.com/PowerDNS/lmdb-go v1.9.3 // indirect 29 | github.com/andybalholm/brotli v1.1.1 // indirect 30 | github.com/bep/debounce v1.2.1 // indirect 31 | github.com/btcsuite/btcd/btcec/v2 v2.3.5 // indirect 32 | github.com/btcsuite/btcd/btcutil v1.1.5 // indirect 33 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect 34 | github.com/bytedance/sonic v1.13.3 // indirect 35 | github.com/bytedance/sonic/loader v0.2.4 // indirect 36 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 37 | github.com/cloudwego/base64x v0.1.5 // indirect 38 | github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect 39 | github.com/coder/websocket v1.8.13 // indirect 40 | github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect 41 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 42 | github.com/dgraph-io/badger/v4 v4.5.0 // indirect 43 | github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect 44 | github.com/dustin/go-humanize v1.0.1 // indirect 45 | github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect 46 | github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect 47 | github.com/fasthttp/websocket v1.5.12 // indirect 48 | github.com/felixge/httpsnoop v1.0.4 // indirect 49 | github.com/go-ini/ini v1.67.0 // indirect 50 | github.com/go-jose/go-jose/v4 v4.1.0 // indirect 51 | github.com/go-logr/logr v1.4.2 // indirect 52 | github.com/go-logr/stdr v1.2.2 // indirect 53 | github.com/goccy/go-json v0.10.5 // indirect 54 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 55 | github.com/google/flatbuffers v24.12.23+incompatible // indirect 56 | github.com/google/s2a-go v0.1.9 // indirect 57 | github.com/google/uuid v1.6.0 // indirect 58 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect 59 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect 60 | github.com/josharian/intern v1.0.0 // indirect 61 | github.com/json-iterator/go v1.1.12 // indirect 62 | github.com/klauspost/compress v1.18.0 // indirect 63 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 64 | github.com/liamg/magic v0.0.1 // indirect 65 | github.com/mailru/easyjson v0.9.0 // indirect 66 | github.com/minio/crc64nvme v1.0.1 // indirect 67 | github.com/minio/md5-simd v1.1.2 // indirect 68 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 69 | github.com/modern-go/reflect2 v1.0.2 // indirect 70 | github.com/pkg/errors v0.9.1 // indirect 71 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 72 | github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect 73 | github.com/rs/cors v1.11.1 // indirect 74 | github.com/rs/xid v1.6.0 // indirect 75 | github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 // indirect 76 | github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect 77 | github.com/tidwall/gjson v1.18.0 // indirect 78 | github.com/tidwall/match v1.1.1 // indirect 79 | github.com/tidwall/pretty v1.2.1 // indirect 80 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 81 | github.com/valyala/bytebufferpool v1.0.0 // indirect 82 | github.com/valyala/fasthttp v1.62.0 // indirect 83 | github.com/zeebo/errs v1.4.0 // indirect 84 | go.opencensus.io v0.24.0 // indirect 85 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 86 | go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect 87 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect 88 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 89 | go.opentelemetry.io/otel v1.35.0 // indirect 90 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 91 | go.opentelemetry.io/otel/sdk v1.35.0 // indirect 92 | go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect 93 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 94 | golang.org/x/arch v0.18.0 // indirect 95 | golang.org/x/crypto v0.39.0 // indirect 96 | golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect 97 | golang.org/x/net v0.41.0 // indirect 98 | golang.org/x/oauth2 v0.29.0 // indirect 99 | golang.org/x/sync v0.15.0 // indirect 100 | golang.org/x/sys v0.33.0 // indirect 101 | golang.org/x/text v0.26.0 // indirect 102 | golang.org/x/time v0.11.0 // indirect 103 | google.golang.org/api v0.231.0 // indirect 104 | google.golang.org/genproto v0.0.0-20250428153025-10db94c68c34 // indirect 105 | google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect 106 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect 107 | google.golang.org/grpc v1.72.0 // indirect 108 | google.golang.org/protobuf v1.36.6 // indirect 109 | ) 110 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg= 2 | cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= 3 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg= 5 | cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q= 6 | cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= 7 | cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= 8 | cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= 9 | cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= 10 | cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= 11 | cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= 12 | cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= 13 | cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= 14 | cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= 15 | cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= 16 | cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= 17 | cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= 18 | cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= 19 | cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= 20 | cloud.google.com/go/storage v1.52.0 h1:ROpzMW/IwipKtatA69ikxibdzQSiXJrY9f6IgBa9AlA= 21 | cloud.google.com/go/storage v1.52.0/go.mod h1:4wrBAbAYUvYkbrf19ahGm4I5kDQhESSqN3CGEkMGvOY= 22 | cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= 23 | cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= 24 | fiatjaf.com/lib v0.2.0 h1:TgIJESbbND6GjOgGHxF5jsO6EMjuAxIzZHPo5DXYexs= 25 | fiatjaf.com/lib v0.2.0/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= 26 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 27 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= 28 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= 29 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= 30 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= 31 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= 32 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= 33 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= 34 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= 35 | github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg= 36 | github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA= 37 | github.com/PowerDNS/lmdb-go v1.9.3 h1:AUMY2pZT8WRpkEv39I9Id3MuoHd+NZbTVpNhruVkPTg= 38 | github.com/PowerDNS/lmdb-go v1.9.3/go.mod h1:TE0l+EZK8Z1B4dx070ZxkWTlp8RG1mjN0/+FkFRQMtU= 39 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 40 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 41 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 42 | github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= 43 | github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= 44 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= 45 | github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= 46 | github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= 47 | github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= 48 | github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= 49 | github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU= 50 | github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= 51 | github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= 52 | github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= 53 | github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= 54 | github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= 55 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 56 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 57 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= 58 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 59 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 60 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 61 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 62 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 63 | github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= 64 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 65 | github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 66 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 67 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 68 | github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= 69 | github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 70 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 71 | github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= 72 | github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 73 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 74 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 75 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 76 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 77 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 78 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 79 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 80 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 81 | github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= 82 | github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= 83 | github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= 84 | github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= 85 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 86 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 87 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 88 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 89 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 90 | github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= 91 | github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 92 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= 93 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= 94 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 95 | github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= 96 | github.com/dgraph-io/badger/v4 v4.5.0 h1:TeJE3I1pIWLBjYhIYCA1+uxrjWEoJXImFBMEBVSm16g= 97 | github.com/dgraph-io/badger/v4 v4.5.0/go.mod h1:ysgYmIeG8dS/E8kwxT7xHyc7MkmwNYLRoYnFbr7387A= 98 | github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I= 99 | github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4= 100 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= 101 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 102 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 103 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 104 | github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= 105 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 106 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 107 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 108 | github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= 109 | github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= 110 | github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= 111 | github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= 112 | github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= 113 | github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= 114 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 115 | github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= 116 | github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= 117 | github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE= 118 | github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg= 119 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 120 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 121 | github.com/fiatjaf/eventstore v0.17.1 h1:Uckf36z7BZSwUw+OwDwhtij554QqhnzmJEBKQ9Aqr1M= 122 | github.com/fiatjaf/eventstore v0.17.1/go.mod h1:u5Hc0rwHm2O/atVfujfeZ4zzRb4uj0+X8WNZQbTGW8c= 123 | github.com/fiatjaf/khatru v0.18.1 h1:3IK/pVL7D+b9+40Y87doF6utlJziOeGxDIkl0NlePaM= 124 | github.com/fiatjaf/khatru v0.18.1/go.mod h1:4KW6mom+7ajwrhj5IvLJTBKj6peV8bdZjU6XoDVrX2Q= 125 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 126 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 127 | github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= 128 | github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 129 | github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= 130 | github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= 131 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 132 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 133 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 134 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 135 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 136 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 137 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 138 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 139 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 140 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 141 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 142 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 143 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 144 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 145 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 146 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 147 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 148 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 149 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 150 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 151 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 152 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 153 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 154 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 155 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 156 | github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8= 157 | github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 158 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 159 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 160 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 161 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 162 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 163 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 164 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 165 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 166 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 167 | github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= 168 | github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= 169 | github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= 170 | github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 171 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 172 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 173 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 174 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= 175 | github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= 176 | github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= 177 | github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= 178 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 179 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 180 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 181 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 182 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 183 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 184 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 185 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 186 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 187 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 188 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 189 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 190 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 191 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 192 | github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 193 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 194 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 195 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 196 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 197 | github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM= 198 | github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk= 199 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 200 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 201 | github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= 202 | github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= 203 | github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= 204 | github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= 205 | github.com/minio/minio-go/v7 v7.0.90 h1:TmSj1083wtAD0kEYTx7a5pFsv3iRYMsOJ6A4crjA1lE= 206 | github.com/minio/minio-go/v7 v7.0.90/go.mod h1:uvMUcGrpgeSAAI6+sD3818508nUyMULw94j2Nxku/Go= 207 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 208 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 209 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 210 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 211 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 212 | github.com/nbd-wtf/go-nostr v0.51.12 h1:MRQcrShiW/cHhnYSVDQ4SIEc7DlYV7U7gg/l4H4gbbE= 213 | github.com/nbd-wtf/go-nostr v0.51.12/go.mod h1:IF30/Cm4AS90wd1GjsFJbBqq7oD1txo+2YUFYXqK3Nc= 214 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 215 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 216 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 217 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 218 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 219 | github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 220 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 221 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 222 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 223 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 224 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 225 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= 226 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 227 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 228 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 229 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 230 | github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= 231 | github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= 232 | github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= 233 | github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 234 | github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= 235 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 236 | github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 h1:qIQ0tWF9vxGtkJa24bR+2i53WBCz1nW/Pc47oVYauC4= 237 | github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= 238 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 239 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 240 | github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= 241 | github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= 242 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 243 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 244 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 245 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 246 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 247 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 248 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 249 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 250 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 251 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 252 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 253 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= 254 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 255 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 256 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 257 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 258 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 259 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 260 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 261 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 262 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 263 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 264 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 265 | github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0= 266 | github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg= 267 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 268 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 269 | github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= 270 | github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= 271 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 272 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 273 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 274 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 275 | go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= 276 | go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= 277 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= 278 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= 279 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= 280 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= 281 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 282 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 283 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= 284 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= 285 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 286 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 287 | go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= 288 | go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= 289 | go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= 290 | go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 291 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 292 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 293 | golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= 294 | golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 295 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 296 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 297 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 298 | golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= 299 | golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= 300 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 301 | golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= 302 | golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= 303 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 304 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 305 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 306 | golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 307 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 308 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 309 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 310 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 311 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 312 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 313 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 314 | golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 315 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 316 | golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= 317 | golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= 318 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 319 | golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= 320 | golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 321 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 322 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 323 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 324 | golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= 325 | golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 326 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 327 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 328 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 329 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 330 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 331 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 332 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 333 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 334 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 335 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 336 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 337 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 338 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 339 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 340 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 341 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 342 | golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= 343 | golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= 344 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 345 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 346 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 347 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 348 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 349 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 350 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 351 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 352 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 353 | google.golang.org/api v0.231.0 h1:LbUD5FUl0C4qwia2bjXhCMH65yz1MLPzA/0OYEsYY7Q= 354 | google.golang.org/api v0.231.0/go.mod h1:H52180fPI/QQlUc0F4xWfGZILdv09GCWKt2bcsn164A= 355 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 356 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 357 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 358 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 359 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 360 | google.golang.org/genproto v0.0.0-20250428153025-10db94c68c34 h1:oklGWmm0ZiCw4efmdYZo5MF9t6nRvGzM5+0klSjOmGM= 361 | google.golang.org/genproto v0.0.0-20250428153025-10db94c68c34/go.mod h1:hiH/EqX5GBdTyIpkqMqDGUHDiBniln8b4FCw+NzPxQY= 362 | google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE= 363 | google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI= 364 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs= 365 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 366 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 367 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 368 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 369 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 370 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 371 | google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= 372 | google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 373 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 374 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 375 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 376 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 377 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 378 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 379 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 380 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 381 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 382 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 383 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 384 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 385 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 386 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 387 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 388 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 389 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 390 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 391 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 392 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 393 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 394 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 395 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 396 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 397 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 398 | -------------------------------------------------------------------------------- /import.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "log/slog" 8 | "time" 9 | 10 | "github.com/fiatjaf/eventstore" 11 | "github.com/nbd-wtf/go-nostr" 12 | ) 13 | 14 | const layout = "2006-01-02" 15 | 16 | func importOwnerNotes() { 17 | ownerImportedNotes := 0 18 | nFailedImportNotes := 0 19 | wdb := eventstore.RelayWrapper{Store: outboxDB} 20 | 21 | startTime, err := time.Parse(layout, config.ImportStartDate) 22 | if err != nil { 23 | fmt.Println("Error parsing start date:", err) 24 | return 25 | } 26 | endTime := startTime.Add(240 * time.Hour) 27 | 28 | for { 29 | startTimestamp := nostr.Timestamp(startTime.Unix()) 30 | endTimestamp := nostr.Timestamp(endTime.Unix()) 31 | 32 | filter := nostr.Filter{ 33 | Authors: []string{nPubToPubkey(config.OwnerNpub)}, 34 | Since: &startTimestamp, 35 | Until: &endTimestamp, 36 | } 37 | 38 | done := make(chan int, 1) 39 | timeout := time.Duration(config.ImportOwnerNotesFetchTimeoutSeconds) * time.Second 40 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 41 | 42 | go func() { 43 | defer cancel() 44 | batchImportedNotes := 0 45 | 46 | pool.FetchManyReplaceable(ctx, config.ImportSeedRelays, filter).Range(func(_ nostr.ReplaceableKey, ev *nostr.Event) bool { 47 | if ctx.Err() != nil { 48 | return false // Stop the loop on timeout 49 | } 50 | if err := wdb.Publish(ctx, *ev); err != nil { 51 | log.Println("🚫 error importing note", ev.ID, ":", err) 52 | nFailedImportNotes++ 53 | return true 54 | } 55 | batchImportedNotes++ 56 | return true 57 | }) 58 | done <- batchImportedNotes 59 | close(done) 60 | }() 61 | 62 | select { 63 | case batchImportedNotes := <-done: 64 | ownerImportedNotes += batchImportedNotes 65 | if batchImportedNotes == 0 { 66 | log.Printf("ℹ️ No notes found for %s to %s", startTime.Format(layout), endTime.Format(layout)) 67 | } else { 68 | log.Printf("📦 Imported %d notes from %s to %s", batchImportedNotes, startTime.Format(layout), endTime.Format(layout)) 69 | } 70 | case <-ctx.Done(): 71 | log.Printf("🚫 Timeout after %v while importing notes from %s to %s", timeout, startTime.Format(layout), endTime.Format(layout)) 72 | } 73 | 74 | startTime = startTime.Add(240 * time.Hour) 75 | endTime = endTime.Add(240 * time.Hour) 76 | 77 | if startTime.After(time.Now()) { 78 | log.Println("✅ owner note import complete! Imported", ownerImportedNotes, "notes") 79 | break 80 | } 81 | if nFailedImportNotes > 0 { 82 | log.Printf("⚠️ Failed to import %d notes", nFailedImportNotes) 83 | } 84 | 85 | time.Sleep(1 * time.Second) // Avoid bombarding relays with too many requests 86 | } 87 | } 88 | 89 | func importTaggedNotes() { 90 | taggedImportedNotes := 0 91 | done := make(chan struct{}, 1) 92 | timeout := time.Duration(config.ImportTaggedNotesFetchTimeoutSeconds) * time.Second 93 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 94 | defer cancel() 95 | 96 | wdbInbox := eventstore.RelayWrapper{Store: inboxDB} 97 | wdbChat := eventstore.RelayWrapper{Store: chatDB} 98 | filter := nostr.Filter{ 99 | Tags: nostr.TagMap{ 100 | "p": {nPubToPubkey(config.OwnerNpub)}, 101 | }, 102 | } 103 | 104 | log.Println("📦 importing inbox notes, please wait up to", timeout) 105 | 106 | go func() { 107 | pool.FetchManyReplaceable(ctx, config.ImportSeedRelays, filter).Range(func(_ nostr.ReplaceableKey, ev *nostr.Event) bool { 108 | if ctx.Err() != nil { 109 | return false // Stop the loop on timeout 110 | } 111 | if !wotMap[ev.PubKey] && ev.Kind != nostr.KindGiftWrap { 112 | return true 113 | } 114 | for tag := range ev.Tags.FindAll("p") { 115 | if len(tag) < 2 { 116 | continue 117 | } 118 | if tag[1] == nPubToPubkey(config.OwnerNpub) { 119 | dbToWrite := wdbInbox 120 | if ev.Kind == nostr.KindGiftWrap { 121 | dbToWrite = wdbChat 122 | } 123 | if err := dbToWrite.Publish(ctx, *ev); err != nil { 124 | log.Println("🚫 error importing tagged note", ev.ID, ":", err) 125 | return true 126 | } 127 | taggedImportedNotes++ 128 | } 129 | } 130 | 131 | return true 132 | }) 133 | close(done) 134 | }() 135 | 136 | select { 137 | case <-done: 138 | log.Println("📦 imported", taggedImportedNotes, "tagged notes") 139 | case <-ctx.Done(): 140 | log.Println("🚫 Timeout after", timeout, "while importing tagged notes") 141 | } 142 | 143 | log.Println("✅ tagged import complete. please restart the relay") 144 | } 145 | 146 | func subscribeInboxAndChat() { 147 | ctx := context.Background() 148 | wdbInbox := eventstore.RelayWrapper{Store: inboxDB} 149 | wdbChat := eventstore.RelayWrapper{Store: chatDB} 150 | startTime := nostr.Timestamp(time.Now().Add(-time.Minute * 5).Unix()) 151 | filter := nostr.Filter{ 152 | Tags: nostr.TagMap{ 153 | "p": {nPubToPubkey(config.OwnerNpub)}, 154 | }, 155 | Since: &startTime, 156 | } 157 | 158 | log.Println("📢 subscribing to inbox") 159 | 160 | for ev := range pool.SubscribeMany(ctx, config.ImportSeedRelays, filter) { 161 | if !wotMap[ev.Event.PubKey] && ev.Event.Kind != nostr.KindGiftWrap { 162 | continue 163 | } 164 | for tag := range ev.Event.Tags.FindAll("p") { 165 | if len(tag) < 2 { 166 | continue 167 | } 168 | if tag[1] == nPubToPubkey(config.OwnerNpub) { 169 | dbToPublish := wdbInbox 170 | if ev.Event.Kind == nostr.KindGiftWrap { 171 | dbToPublish = wdbChat 172 | } 173 | 174 | slog.Debug("ℹ️ importing event", "kind", ev.Kind, "id", ev.Event.ID, "relay", ev.Relay.URL) 175 | 176 | if isDuplicate(ctx, dbToPublish, ev.Event) { 177 | slog.Debug("ℹ️ skipping duplicate event", "id", ev.Event.ID) 178 | break // Avoid re-importing duplicates 179 | } 180 | 181 | if err := dbToPublish.Publish(ctx, *ev.Event); err != nil { 182 | log.Println("🚫 error importing tagged note", ev.Event.ID, ":", "from relay", ev.Relay.URL, ":", err) 183 | break 184 | } 185 | 186 | switch ev.Event.Kind { 187 | case nostr.KindTextNote: 188 | log.Println("📰 new note in your inbox") 189 | case nostr.KindReaction: 190 | log.Println(ev.Event.Content, "new reaction in your inbox") 191 | case nostr.KindZap: 192 | log.Println("⚡️ new zap in your inbox") 193 | case nostr.KindEncryptedDirectMessage: 194 | log.Println("🔒✉️ new encrypted message in your inbox") 195 | case nostr.KindGiftWrap: 196 | log.Println("🎁🔒️✉️ new gift-wrapped message in your chat relay") 197 | case nostr.KindRepost: 198 | log.Println("🔁 new repost in your inbox") 199 | case nostr.KindFollowList: 200 | // do nothing 201 | default: 202 | log.Println("📦 new event kind", ev.Event.Kind, "event in your inbox") 203 | } 204 | } 205 | } 206 | } 207 | } 208 | 209 | func isDuplicate(ctx context.Context, db eventstore.RelayWrapper, event *nostr.Event) bool { 210 | filter := nostr.Filter{ 211 | IDs: []string{event.ID}, 212 | Since: &event.CreatedAt, 213 | Limit: 1, 214 | } 215 | 216 | events, err := db.QuerySync(ctx, filter) 217 | if err != nil { 218 | log.Println("🚫 error querying for event", event.ID, ":", err) 219 | return false 220 | } 221 | 222 | return len(events) > 0 223 | } 224 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "net/http" 8 | "text/template" 9 | "time" 10 | 11 | "github.com/fiatjaf/eventstore/badger" 12 | "github.com/fiatjaf/eventstore/lmdb" 13 | "github.com/fiatjaf/khatru" 14 | "github.com/fiatjaf/khatru/blossom" 15 | "github.com/fiatjaf/khatru/policies" 16 | "github.com/nbd-wtf/go-nostr" 17 | ) 18 | 19 | var ( 20 | privateRelay = khatru.NewRelay() 21 | privateDB = newDBBackend("db/private") 22 | ) 23 | 24 | var ( 25 | chatRelay = khatru.NewRelay() 26 | chatDB = newDBBackend("db/chat") 27 | ) 28 | 29 | var ( 30 | outboxRelay = khatru.NewRelay() 31 | outboxDB = newDBBackend("db/outbox") 32 | ) 33 | 34 | var ( 35 | inboxRelay = khatru.NewRelay() 36 | inboxDB = newDBBackend("db/inbox") 37 | ) 38 | 39 | type DBBackend interface { 40 | Init() error 41 | Close() 42 | CountEvents(ctx context.Context, filter nostr.Filter) (int64, error) 43 | DeleteEvent(ctx context.Context, evt *nostr.Event) error 44 | QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) 45 | SaveEvent(ctx context.Context, evt *nostr.Event) error 46 | ReplaceEvent(ctx context.Context, evt *nostr.Event) error 47 | Serial() []byte 48 | } 49 | 50 | func newDBBackend(path string) DBBackend { 51 | switch config.DBEngine { 52 | case "lmdb": 53 | return newLMDBBackend(path) 54 | case "badger": 55 | return &badger.BadgerBackend{ 56 | Path: path, 57 | } 58 | default: 59 | return newLMDBBackend(path) 60 | } 61 | } 62 | 63 | func newLMDBBackend(path string) *lmdb.LMDBBackend { 64 | return &lmdb.LMDBBackend{ 65 | Path: path, 66 | MapSize: config.LmdbMapSize, 67 | } 68 | } 69 | 70 | func initRelays() { 71 | if err := privateDB.Init(); err != nil { 72 | panic(err) 73 | } 74 | 75 | if err := chatDB.Init(); err != nil { 76 | panic(err) 77 | } 78 | 79 | if err := outboxDB.Init(); err != nil { 80 | panic(err) 81 | } 82 | 83 | if err := inboxDB.Init(); err != nil { 84 | panic(err) 85 | } 86 | 87 | initRelayLimits() 88 | 89 | privateRelay.Info.Name = config.PrivateRelayName 90 | privateRelay.Info.PubKey = nPubToPubkey(config.PrivateRelayNpub) 91 | privateRelay.Info.Description = config.PrivateRelayDescription 92 | privateRelay.Info.Icon = config.PrivateRelayIcon 93 | privateRelay.Info.Version = config.RelayVersion 94 | privateRelay.Info.Software = config.RelaySoftware 95 | privateRelay.ServiceURL = "https://" + config.RelayURL + "/private" 96 | 97 | if !privateRelayLimits.AllowEmptyFilters { 98 | privateRelay.RejectFilter = append(privateRelay.RejectFilter, policies.NoEmptyFilters) 99 | } 100 | 101 | if !privateRelayLimits.AllowComplexFilters { 102 | privateRelay.RejectFilter = append(privateRelay.RejectFilter, policies.NoComplexFilters) 103 | } 104 | 105 | privateRelay.RejectEvent = append(privateRelay.RejectEvent, 106 | policies.RejectEventsWithBase64Media, 107 | policies.EventIPRateLimiter( 108 | privateRelayLimits.EventIPLimiterTokensPerInterval, 109 | time.Minute*time.Duration(privateRelayLimits.EventIPLimiterInterval), 110 | privateRelayLimits.EventIPLimiterMaxTokens, 111 | ), 112 | ) 113 | 114 | privateRelay.RejectConnection = append(privateRelay.RejectConnection, 115 | policies.ConnectionRateLimiter( 116 | privateRelayLimits.ConnectionRateLimiterTokensPerInterval, 117 | time.Minute*time.Duration(privateRelayLimits.ConnectionRateLimiterInterval), 118 | privateRelayLimits.ConnectionRateLimiterMaxTokens, 119 | ), 120 | ) 121 | 122 | privateRelay.OnConnect = append(privateRelay.OnConnect, func(ctx context.Context) { 123 | khatru.RequestAuth(ctx) 124 | }) 125 | 126 | privateRelay.StoreEvent = append(privateRelay.StoreEvent, privateDB.SaveEvent) 127 | privateRelay.QueryEvents = append(privateRelay.QueryEvents, privateDB.QueryEvents) 128 | privateRelay.DeleteEvent = append(privateRelay.DeleteEvent, privateDB.DeleteEvent) 129 | privateRelay.CountEvents = append(privateRelay.CountEvents, privateDB.CountEvents) 130 | privateRelay.ReplaceEvent = append(privateRelay.ReplaceEvent, privateDB.ReplaceEvent) 131 | 132 | privateRelay.RejectFilter = append(privateRelay.RejectFilter, func(ctx context.Context, filter nostr.Filter) (bool, string) { 133 | authenticatedUser := khatru.GetAuthed(ctx) 134 | if authenticatedUser == nPubToPubkey(config.OwnerNpub) { 135 | return false, "" 136 | } 137 | 138 | return true, "auth-required: this query requires you to be authenticated" 139 | }) 140 | 141 | privateRelay.RejectEvent = append(privateRelay.RejectEvent, func(ctx context.Context, event *nostr.Event) (bool, string) { 142 | authenticatedUser := khatru.GetAuthed(ctx) 143 | 144 | if authenticatedUser == nPubToPubkey(config.OwnerNpub) { 145 | return false, "" 146 | } 147 | 148 | return true, "auth-required: publishing this event requires authentication" 149 | }) 150 | 151 | mux := privateRelay.Router() 152 | 153 | mux.HandleFunc("GET /private", func(w http.ResponseWriter, r *http.Request) { 154 | tmpl := template.Must(template.ParseFiles("templates/index.html")) 155 | data := struct { 156 | RelayName string 157 | RelayPubkey string 158 | RelayDescription string 159 | RelayURL string 160 | }{ 161 | RelayName: config.PrivateRelayName, 162 | RelayPubkey: nPubToPubkey(config.PrivateRelayNpub), 163 | RelayDescription: config.PrivateRelayDescription, 164 | RelayURL: "wss://" + config.RelayURL + "/private", 165 | } 166 | err := tmpl.Execute(w, data) 167 | if err != nil { 168 | http.Error(w, err.Error(), http.StatusInternalServerError) 169 | } 170 | }) 171 | 172 | chatRelay.Info.Name = config.ChatRelayName 173 | chatRelay.Info.PubKey = nPubToPubkey(config.ChatRelayNpub) 174 | chatRelay.Info.Description = config.ChatRelayDescription 175 | chatRelay.Info.Icon = config.ChatRelayIcon 176 | chatRelay.Info.Version = config.RelayVersion 177 | chatRelay.Info.Software = config.RelaySoftware 178 | chatRelay.ServiceURL = "https://" + config.RelayURL + "/chat" 179 | 180 | if !chatRelayLimits.AllowEmptyFilters { 181 | chatRelay.RejectFilter = append(chatRelay.RejectFilter, policies.NoEmptyFilters) 182 | } 183 | 184 | if !chatRelayLimits.AllowComplexFilters { 185 | chatRelay.RejectFilter = append(chatRelay.RejectFilter, policies.NoComplexFilters) 186 | } 187 | 188 | chatRelay.RejectEvent = append(chatRelay.RejectEvent, 189 | policies.RejectEventsWithBase64Media, 190 | policies.EventIPRateLimiter( 191 | chatRelayLimits.EventIPLimiterTokensPerInterval, 192 | time.Minute*time.Duration(chatRelayLimits.EventIPLimiterInterval), 193 | chatRelayLimits.EventIPLimiterMaxTokens, 194 | ), 195 | ) 196 | 197 | chatRelay.RejectConnection = append(chatRelay.RejectConnection, 198 | policies.ConnectionRateLimiter( 199 | chatRelayLimits.ConnectionRateLimiterTokensPerInterval, 200 | time.Minute*time.Duration(chatRelayLimits.ConnectionRateLimiterInterval), 201 | chatRelayLimits.ConnectionRateLimiterMaxTokens, 202 | ), 203 | ) 204 | 205 | chatRelay.OnConnect = append(chatRelay.OnConnect, func(ctx context.Context) { 206 | khatru.RequestAuth(ctx) 207 | }) 208 | 209 | chatRelay.StoreEvent = append(chatRelay.StoreEvent, chatDB.SaveEvent) 210 | chatRelay.QueryEvents = append(chatRelay.QueryEvents, chatDB.QueryEvents) 211 | chatRelay.DeleteEvent = append(chatRelay.DeleteEvent, chatDB.DeleteEvent) 212 | chatRelay.CountEvents = append(chatRelay.CountEvents, chatDB.CountEvents) 213 | chatRelay.ReplaceEvent = append(chatRelay.ReplaceEvent, chatDB.ReplaceEvent) 214 | 215 | chatRelay.RejectFilter = append(chatRelay.RejectFilter, func(ctx context.Context, filter nostr.Filter) (bool, string) { 216 | authenticatedUser := khatru.GetAuthed(ctx) 217 | 218 | if !wotMap[authenticatedUser] { 219 | return true, "you must be in the web of trust to chat with the relay owner" 220 | } 221 | 222 | return false, "" 223 | }) 224 | 225 | allowedKinds := []int{ 226 | // Regular kinds 227 | nostr.KindSimpleGroupChatMessage, 228 | nostr.KindSimpleGroupThreadedReply, 229 | nostr.KindSimpleGroupThread, 230 | nostr.KindSimpleGroupReply, 231 | nostr.KindChannelMessage, 232 | nostr.KindChannelHideMessage, 233 | 234 | nostr.KindGiftWrap, 235 | 236 | nostr.KindSimpleGroupPutUser, 237 | nostr.KindSimpleGroupRemoveUser, 238 | nostr.KindSimpleGroupEditMetadata, 239 | nostr.KindSimpleGroupDeleteEvent, 240 | nostr.KindSimpleGroupCreateGroup, 241 | nostr.KindSimpleGroupDeleteGroup, 242 | nostr.KindSimpleGroupCreateInvite, 243 | nostr.KindSimpleGroupJoinRequest, 244 | nostr.KindSimpleGroupLeaveRequest, 245 | 246 | // Addressable kinds 247 | nostr.KindSimpleGroupMetadata, 248 | nostr.KindSimpleGroupAdmins, 249 | nostr.KindSimpleGroupMembers, 250 | nostr.KindSimpleGroupRoles, 251 | } 252 | 253 | chatRelay.RejectEvent = append(chatRelay.RejectEvent, func(ctx context.Context, event *nostr.Event) (bool, string) { 254 | for _, kind := range allowedKinds { 255 | if event.Kind == kind { 256 | return false, "" 257 | } 258 | } 259 | 260 | return true, "only chat related events are allowed" 261 | }) 262 | 263 | mux = chatRelay.Router() 264 | 265 | mux.HandleFunc("GET /chat", func(w http.ResponseWriter, r *http.Request) { 266 | tmpl := template.Must(template.ParseFiles("templates/index.html")) 267 | data := struct { 268 | RelayName string 269 | RelayPubkey string 270 | RelayDescription string 271 | RelayURL string 272 | }{ 273 | RelayName: config.ChatRelayName, 274 | RelayPubkey: nPubToPubkey(config.ChatRelayNpub), 275 | RelayDescription: config.ChatRelayDescription, 276 | RelayURL: "wss://" + config.RelayURL + "/chat", 277 | } 278 | err := tmpl.Execute(w, data) 279 | if err != nil { 280 | http.Error(w, err.Error(), http.StatusInternalServerError) 281 | } 282 | }) 283 | 284 | outboxRelay.Info.Name = config.OutboxRelayName 285 | outboxRelay.Info.PubKey = nPubToPubkey(config.OutboxRelayNpub) 286 | outboxRelay.Info.Description = config.OutboxRelayDescription 287 | outboxRelay.Info.Icon = config.OutboxRelayIcon 288 | outboxRelay.Info.Version = config.RelayVersion 289 | outboxRelay.Info.Software = config.RelaySoftware 290 | outboxRelay.ServiceURL = "https://" + config.RelayURL 291 | 292 | if !outboxRelayLimits.AllowEmptyFilters { 293 | outboxRelay.RejectFilter = append(outboxRelay.RejectFilter, policies.NoEmptyFilters) 294 | } 295 | 296 | if !outboxRelayLimits.AllowComplexFilters { 297 | outboxRelay.RejectFilter = append(outboxRelay.RejectFilter, policies.NoComplexFilters) 298 | } 299 | 300 | outboxRelay.RejectEvent = append(outboxRelay.RejectEvent, 301 | policies.RejectEventsWithBase64Media, 302 | policies.EventIPRateLimiter( 303 | outboxRelayLimits.EventIPLimiterTokensPerInterval, 304 | time.Minute*time.Duration(outboxRelayLimits.EventIPLimiterInterval), 305 | outboxRelayLimits.EventIPLimiterMaxTokens, 306 | ), 307 | ) 308 | 309 | outboxRelay.RejectConnection = append(outboxRelay.RejectConnection, 310 | policies.ConnectionRateLimiter( 311 | outboxRelayLimits.ConnectionRateLimiterTokensPerInterval, 312 | time.Minute*time.Duration(outboxRelayLimits.ConnectionRateLimiterInterval), 313 | outboxRelayLimits.ConnectionRateLimiterMaxTokens, 314 | ), 315 | ) 316 | 317 | outboxRelay.StoreEvent = append(outboxRelay.StoreEvent, outboxDB.SaveEvent, func(ctx context.Context, event *nostr.Event) error { 318 | go blast(event) 319 | return nil 320 | }) 321 | outboxRelay.QueryEvents = append(outboxRelay.QueryEvents, outboxDB.QueryEvents) 322 | outboxRelay.DeleteEvent = append(outboxRelay.DeleteEvent, outboxDB.DeleteEvent) 323 | outboxRelay.CountEvents = append(outboxRelay.CountEvents, outboxDB.CountEvents) 324 | outboxRelay.ReplaceEvent = append(outboxRelay.ReplaceEvent, outboxDB.ReplaceEvent) 325 | 326 | outboxRelay.RejectEvent = append(outboxRelay.RejectEvent, func(ctx context.Context, event *nostr.Event) (bool, string) { 327 | if event.PubKey == nPubToPubkey(config.OwnerNpub) { 328 | return false, "" 329 | } 330 | return true, "only notes signed by the owner of this relay are allowed" 331 | }) 332 | 333 | mux = outboxRelay.Router() 334 | 335 | mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { 336 | w.Header().Set("Access-Control-Allow-Origin", "*") 337 | tmpl := template.Must(template.ParseFiles("templates/index.html")) 338 | data := struct { 339 | RelayName string 340 | RelayPubkey string 341 | RelayDescription string 342 | RelayURL string 343 | }{ 344 | RelayName: config.OutboxRelayName, 345 | RelayPubkey: nPubToPubkey(config.OutboxRelayNpub), 346 | RelayDescription: config.OutboxRelayDescription, 347 | RelayURL: "wss://" + config.RelayURL + "/outbox", 348 | } 349 | err := tmpl.Execute(w, data) 350 | if err != nil { 351 | http.Error(w, err.Error(), http.StatusInternalServerError) 352 | } 353 | }) 354 | 355 | bl := blossom.New(outboxRelay, "https://"+config.RelayURL) 356 | bl.Store = blossom.EventStoreBlobIndexWrapper{Store: outboxDB, ServiceURL: bl.ServiceURL} 357 | bl.StoreBlob = append(bl.StoreBlob, func(ctx context.Context, sha256 string, body []byte) error { 358 | 359 | file, err := fs.Create(config.BlossomPath + sha256) 360 | if err != nil { 361 | return err 362 | } 363 | if _, err := io.Copy(file, bytes.NewReader(body)); err != nil { 364 | return err 365 | } 366 | return nil 367 | }) 368 | bl.LoadBlob = append(bl.LoadBlob, func(ctx context.Context, sha256 string) (io.ReadSeeker, error) { 369 | return fs.Open(config.BlossomPath + sha256) 370 | }) 371 | bl.DeleteBlob = append(bl.DeleteBlob, func(ctx context.Context, sha256 string) error { 372 | return fs.Remove(config.BlossomPath + sha256) 373 | }) 374 | bl.RejectUpload = append(bl.RejectUpload, func(ctx context.Context, event *nostr.Event, size int, ext string) (bool, string, int) { 375 | if event.PubKey == nPubToPubkey(config.OwnerNpub) { 376 | return false, ext, size 377 | } 378 | 379 | return true, "only notes signed by the owner of this relay are allowed", 403 380 | }) 381 | 382 | inboxRelay.Info.Name = config.InboxRelayName 383 | inboxRelay.Info.PubKey = nPubToPubkey(config.InboxRelayNpub) 384 | inboxRelay.Info.Description = config.InboxRelayDescription 385 | inboxRelay.Info.Icon = config.InboxRelayIcon 386 | inboxRelay.Info.Version = config.RelayVersion 387 | inboxRelay.Info.Software = config.RelaySoftware 388 | inboxRelay.ServiceURL = "https://" + config.RelayURL + "/inbox" 389 | 390 | if !inboxRelayLimits.AllowEmptyFilters { 391 | inboxRelay.RejectFilter = append(inboxRelay.RejectFilter, policies.NoEmptyFilters) 392 | } 393 | 394 | if !inboxRelayLimits.AllowComplexFilters { 395 | inboxRelay.RejectFilter = append(inboxRelay.RejectFilter, policies.NoComplexFilters) 396 | } 397 | 398 | inboxRelay.RejectEvent = append(inboxRelay.RejectEvent, 399 | policies.RejectEventsWithBase64Media, 400 | policies.EventIPRateLimiter( 401 | inboxRelayLimits.EventIPLimiterTokensPerInterval, 402 | time.Minute*time.Duration(inboxRelayLimits.EventIPLimiterInterval), 403 | inboxRelayLimits.EventIPLimiterMaxTokens, 404 | ), 405 | ) 406 | 407 | inboxRelay.RejectConnection = append(inboxRelay.RejectConnection, 408 | policies.ConnectionRateLimiter( 409 | inboxRelayLimits.ConnectionRateLimiterTokensPerInterval, 410 | time.Minute*time.Duration(inboxRelayLimits.ConnectionRateLimiterInterval), 411 | inboxRelayLimits.ConnectionRateLimiterMaxTokens, 412 | ), 413 | ) 414 | 415 | inboxRelay.StoreEvent = append(inboxRelay.StoreEvent, inboxDB.SaveEvent) 416 | inboxRelay.QueryEvents = append(inboxRelay.QueryEvents, inboxDB.QueryEvents) 417 | inboxRelay.DeleteEvent = append(inboxRelay.DeleteEvent, inboxDB.DeleteEvent) 418 | inboxRelay.CountEvents = append(inboxRelay.CountEvents, inboxDB.CountEvents) 419 | inboxRelay.ReplaceEvent = append(inboxRelay.ReplaceEvent, inboxDB.ReplaceEvent) 420 | 421 | inboxRelay.RejectEvent = append(inboxRelay.RejectEvent, func(ctx context.Context, event *nostr.Event) (bool, string) { 422 | if !wotMap[event.PubKey] { 423 | return true, "you must be in the web of trust to post to this relay" 424 | } 425 | 426 | if event.Kind == nostr.KindEncryptedDirectMessage { 427 | return true, "only gift wrapped DMs are supported" 428 | } 429 | 430 | if event.Tags.FindWithValue("p", inboxRelay.Info.PubKey) != nil { 431 | return false, "" 432 | } 433 | 434 | return true, "you can only post notes if you've tagged the owner of this relay" 435 | }) 436 | 437 | mux = inboxRelay.Router() 438 | 439 | mux.HandleFunc("GET /inbox", func(w http.ResponseWriter, r *http.Request) { 440 | tmpl := template.Must(template.ParseFiles("templates/index.html")) 441 | data := struct { 442 | RelayName string 443 | RelayPubkey string 444 | RelayDescription string 445 | RelayURL string 446 | }{ 447 | RelayName: config.InboxRelayName, 448 | RelayPubkey: nPubToPubkey(config.InboxRelayNpub), 449 | RelayDescription: config.InboxRelayDescription, 450 | RelayURL: "wss://" + config.RelayURL + "/inbox", 451 | } 452 | err := tmpl.Execute(w, data) 453 | if err != nil { 454 | http.Error(w, err.Error(), http.StatusInternalServerError) 455 | } 456 | }) 457 | 458 | } 459 | -------------------------------------------------------------------------------- /limits.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | ) 7 | 8 | var ( 9 | privateRelayLimits PrivateRelayLimits 10 | chatRelayLimits ChatRelayLimits 11 | inboxRelayLimits InboxRelayLimits 12 | outboxRelayLimits OutboxRelayLimits 13 | ) 14 | 15 | type PrivateRelayLimits struct { 16 | EventIPLimiterTokensPerInterval int 17 | EventIPLimiterInterval int 18 | EventIPLimiterMaxTokens int 19 | AllowEmptyFilters bool 20 | AllowComplexFilters bool 21 | ConnectionRateLimiterTokensPerInterval int 22 | ConnectionRateLimiterInterval int 23 | ConnectionRateLimiterMaxTokens int 24 | } 25 | 26 | type ChatRelayLimits struct { 27 | EventIPLimiterTokensPerInterval int 28 | EventIPLimiterInterval int 29 | EventIPLimiterMaxTokens int 30 | AllowEmptyFilters bool 31 | AllowComplexFilters bool 32 | ConnectionRateLimiterTokensPerInterval int 33 | ConnectionRateLimiterInterval int 34 | ConnectionRateLimiterMaxTokens int 35 | } 36 | 37 | type InboxRelayLimits struct { 38 | EventIPLimiterTokensPerInterval int 39 | EventIPLimiterInterval int 40 | EventIPLimiterMaxTokens int 41 | AllowEmptyFilters bool 42 | AllowComplexFilters bool 43 | ConnectionRateLimiterTokensPerInterval int 44 | ConnectionRateLimiterInterval int 45 | ConnectionRateLimiterMaxTokens int 46 | } 47 | 48 | type OutboxRelayLimits struct { 49 | EventIPLimiterTokensPerInterval int 50 | EventIPLimiterInterval int 51 | EventIPLimiterMaxTokens int 52 | AllowEmptyFilters bool 53 | AllowComplexFilters bool 54 | ConnectionRateLimiterTokensPerInterval int 55 | ConnectionRateLimiterInterval int 56 | ConnectionRateLimiterMaxTokens int 57 | } 58 | 59 | func initRelayLimits() { 60 | privateRelayLimits = PrivateRelayLimits{ 61 | EventIPLimiterTokensPerInterval: getEnvInt("PRIVATE_RELAY_EVENT_IP_LIMITER_TOKENS_PER_INTERVAL", 50), 62 | EventIPLimiterInterval: getEnvInt("PRIVATE_RELAY_EVENT_IP_LIMITER_INTERVAL", 1), 63 | EventIPLimiterMaxTokens: getEnvInt("PRIVATE_RELAY_EVENT_IP_LIMITER_MAX_TOKENS", 100), 64 | AllowEmptyFilters: getEnvBool("PRIVATE_RELAY_ALLOW_EMPTY_FILTERS", true), 65 | AllowComplexFilters: getEnvBool("PRIVATE_RELAY_ALLOW_COMPLEX_FILTERS", true), 66 | ConnectionRateLimiterTokensPerInterval: getEnvInt("PRIVATE_RELAY_CONNECTION_RATE_LIMITER_TOKENS_PER_INTERVAL", 3), 67 | ConnectionRateLimiterInterval: getEnvInt("PRIVATE_RELAY_CONNECTION_RATE_LIMITER_INTERVAL", 5), 68 | ConnectionRateLimiterMaxTokens: getEnvInt("PRIVATE_RELAY_CONNECTION_RATE_LIMITER_MAX_TOKENS", 9), 69 | } 70 | 71 | chatRelayLimits = ChatRelayLimits{ 72 | EventIPLimiterTokensPerInterval: getEnvInt("CHAT_RELAY_EVENT_IP_LIMITER_TOKENS_PER_INTERVAL", 50), 73 | EventIPLimiterInterval: getEnvInt("CHAT_RELAY_EVENT_IP_LIMITER_INTERVAL", 1), 74 | EventIPLimiterMaxTokens: getEnvInt("CHAT_RELAY_EVENT_IP_LIMITER_MAX_TOKENS", 100), 75 | AllowEmptyFilters: getEnvBool("CHAT_RELAY_ALLOW_EMPTY_FILTERS", false), 76 | AllowComplexFilters: getEnvBool("CHAT_RELAY_ALLOW_COMPLEX_FILTERS", false), 77 | ConnectionRateLimiterTokensPerInterval: getEnvInt("CHAT_RELAY_CONNECTION_RATE_LIMITER_TOKENS_PER_INTERVAL", 3), 78 | ConnectionRateLimiterInterval: getEnvInt("CHAT_RELAY_CONNECTION_RATE_LIMITER_INTERVAL", 3), 79 | ConnectionRateLimiterMaxTokens: getEnvInt("CHAT_RELAY_CONNECTION_RATE_LIMITER_MAX_TOKENS", 9), 80 | } 81 | 82 | inboxRelayLimits = InboxRelayLimits{ 83 | EventIPLimiterTokensPerInterval: getEnvInt("INBOX_RELAY_EVENT_IP_LIMITER_TOKENS_PER_INTERVAL", 10), 84 | EventIPLimiterInterval: getEnvInt("INBOX_RELAY_EVENT_IP_LIMITER_INTERVAL", 1), 85 | EventIPLimiterMaxTokens: getEnvInt("INBOX_RELAY_EVENT_IP_LIMITER_MAX_TOKENS", 20), 86 | AllowEmptyFilters: getEnvBool("INBOX_RELAY_ALLOW_EMPTY_FILTERS", false), 87 | AllowComplexFilters: getEnvBool("INBOX_RELAY_ALLOW_COMPLEX_FILTERS", false), 88 | ConnectionRateLimiterTokensPerInterval: getEnvInt("INBOX_RELAY_CONNECTION_RATE_LIMITER_TOKENS_PER_INTERVAL", 3), 89 | ConnectionRateLimiterInterval: getEnvInt("INBOX_RELAY_CONNECTION_RATE_LIMITER_INTERVAL", 1), 90 | ConnectionRateLimiterMaxTokens: getEnvInt("INBOX_RELAY_CONNECTION_RATE_LIMITER_MAX_TOKENS", 9), 91 | } 92 | 93 | outboxRelayLimits = OutboxRelayLimits{ 94 | EventIPLimiterTokensPerInterval: getEnvInt("OUTBOX_RELAY_EVENT_IP_LIMITER_TOKENS_PER_INTERVAL", 10), 95 | EventIPLimiterInterval: getEnvInt("OUTBOX_RELAY_EVENT_IP_LIMITER_INTERVAL", 60), 96 | EventIPLimiterMaxTokens: getEnvInt("OUTBOX_RELAY_EVENT_IP_LIMITER_MAX_TOKENS", 100), 97 | AllowEmptyFilters: getEnvBool("OUTBOX_RELAY_ALLOW_EMPTY_FILTERS", false), 98 | AllowComplexFilters: getEnvBool("OUTBOX_RELAY_ALLOW_COMPLEX_FILTERS", false), 99 | ConnectionRateLimiterTokensPerInterval: getEnvInt("OUTBOX_RELAY_CONNECTION_RATE_LIMITER_TOKENS_PER_INTERVAL", 3), 100 | ConnectionRateLimiterInterval: getEnvInt("OUTBOX_RELAY_CONNECTION_RATE_LIMITER_INTERVAL", 1), 101 | ConnectionRateLimiterMaxTokens: getEnvInt("OUTBOX_RELAY_CONNECTION_RATE_LIMITER_MAX_TOKENS", 9), 102 | } 103 | 104 | prettyPrintLimits("Private relay limits", privateRelayLimits) 105 | prettyPrintLimits("Chat relay limits", chatRelayLimits) 106 | prettyPrintLimits("Inbox relay limits", inboxRelayLimits) 107 | prettyPrintLimits("Outbox relay limits", outboxRelayLimits) 108 | } 109 | 110 | func prettyPrintLimits(label string, value interface{}) { 111 | b, _ := json.MarshalIndent(value, "", " ") 112 | log.Printf("🚧 %s:\n%s\n", label, string(b)) 113 | } 114 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "log/slog" 10 | "net/http" 11 | 12 | "github.com/fiatjaf/khatru" 13 | "github.com/nbd-wtf/go-nostr" 14 | "github.com/spf13/afero" 15 | ) 16 | 17 | var ( 18 | pool = nostr.NewSimplePool(context.Background()) 19 | config = loadConfig() 20 | fs afero.Fs 21 | ) 22 | 23 | func main() { 24 | importFlag := flag.Bool("import", false, "Run the importNotes function after initializing relays") 25 | flag.Parse() 26 | 27 | nostr.InfoLogger = log.New(io.Discard, "", 0) 28 | slog.SetLogLoggerLevel(getLogLevelFromConfig()) 29 | green := "\033[32m" 30 | reset := "\033[0m" 31 | fmt.Println(green + art + reset) 32 | log.Println("🚀 haven is booting up") 33 | fs = afero.NewOsFs() 34 | if err := fs.MkdirAll(config.BlossomPath, 0755); err != nil { 35 | log.Fatal("🚫 error creating blossom path:", err) 36 | } 37 | 38 | initRelays() 39 | 40 | go func() { 41 | refreshTrustNetwork() 42 | 43 | if *importFlag { 44 | log.Println("📦 importing notes") 45 | importOwnerNotes() 46 | importTaggedNotes() 47 | return 48 | } 49 | 50 | go subscribeInboxAndChat() 51 | go backupDatabase() 52 | }() 53 | 54 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("templates/static")))) 55 | http.HandleFunc("/", dynamicRelayHandler) 56 | 57 | addr := fmt.Sprintf("%s:%d", config.RelayBindAddress, config.RelayPort) 58 | 59 | log.Printf("🔗 listening at %s", addr) 60 | if err := http.ListenAndServe(addr, nil); err != nil { 61 | log.Fatal("🚫 error starting server:", err) 62 | } 63 | } 64 | 65 | func dynamicRelayHandler(w http.ResponseWriter, r *http.Request) { 66 | var relay *khatru.Relay 67 | relayType := r.URL.Path 68 | 69 | if relayType == "" { 70 | relay = outboxRelay 71 | } else if relayType == "/private" { 72 | relay = privateRelay 73 | } else if relayType == "/chat" { 74 | relay = chatRelay 75 | } else if relayType == "/inbox" { 76 | relay = inboxRelay 77 | } else { 78 | relay = outboxRelay 79 | } 80 | 81 | relay.ServeHTTP(w, r) 82 | } 83 | 84 | func getLogLevelFromConfig() slog.Level { 85 | switch config.LogLevel { 86 | case "DEBUG": 87 | return slog.LevelDebug 88 | case "INFO": 89 | return slog.LevelInfo 90 | case "WARN": 91 | return slog.LevelWarn 92 | case "ERROR": 93 | return slog.LevelError 94 | default: 95 | return slog.LevelInfo // Default level 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /relays_blastr.example.json: -------------------------------------------------------------------------------- 1 | [ 2 | "relay.damus.io", 3 | "nos.lol", 4 | "relay.nostr.band", 5 | "relay.snort.social", 6 | "nostr.land", 7 | "nostr.mom", 8 | "relay.nos.social", 9 | "relay.primal.net", 10 | "relay.nostr.bg", 11 | "no.str.cr", 12 | "nostr21.com", 13 | "nostrue.com", 14 | "relay.siamstr.com", 15 | "wot.utxo.one", 16 | "nostrelites.org", 17 | "wot.nostr.party", 18 | "wot.sovbit.host", 19 | "wot.girino.org", 20 | "relay.lnau.net", 21 | "wot.siamstr.com", 22 | "relay.lexingtonbitcoin.org", 23 | "wot.azzamo.net", 24 | "wot.swarmstr.com", 25 | "zap.watch", 26 | "satsage.xyz", 27 | "wons.calva.dev" 28 | ] 29 | -------------------------------------------------------------------------------- /relays_import.example.json: -------------------------------------------------------------------------------- 1 | [ 2 | "relay.damus.io", 3 | "nos.lol", 4 | "relay.nostr.band", 5 | "relay.snort.social", 6 | "nostr.land", 7 | "nostr.mom", 8 | "relay.nos.social", 9 | "relay.primal.net", 10 | "relay.nostr.bg", 11 | "no.str.cr", 12 | "nostr21.com", 13 | "nostrue.com", 14 | "relay.siamstr.com", 15 | "wot.utxo.one", 16 | "nostrelites.org", 17 | "wot.nostr.party", 18 | "wot.sovbit.host", 19 | "wot.girino.org", 20 | "relay.lnau.net", 21 | "wot.siamstr.com", 22 | "relay.lexingtonbitcoin.org", 23 | "wot.azzamo.net", 24 | "wot.swarmstr.com", 25 | "zap.watch", 26 | "satsage.xyz", 27 | "wons.calva.dev" 28 | ] 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /templates/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvora/haven/b0d2e739f93f561d21ffc05a186baeecf860532c/templates/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /templates/static/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvora/haven/b0d2e739f93f561d21ffc05a186baeecf860532c/templates/static/android-chrome-256x256.png -------------------------------------------------------------------------------- /templates/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvora/haven/b0d2e739f93f561d21ffc05a186baeecf860532c/templates/static/apple-touch-icon.png -------------------------------------------------------------------------------- /templates/static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /templates/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvora/haven/b0d2e739f93f561d21ffc05a186baeecf860532c/templates/static/favicon-16x16.png -------------------------------------------------------------------------------- /templates/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvora/haven/b0d2e739f93f561d21ffc05a186baeecf860532c/templates/static/favicon-32x32.png -------------------------------------------------------------------------------- /templates/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvora/haven/b0d2e739f93f561d21ffc05a186baeecf860532c/templates/static/favicon.ico -------------------------------------------------------------------------------- /templates/static/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitvora/haven/b0d2e739f93f561d21ffc05a186baeecf860532c/templates/static/mstile-150x150.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nbd-wtf/go-nostr/nip19" 5 | ) 6 | 7 | func nPubToPubkey(nPub string) string { 8 | _, v, err := nip19.Decode(nPub) 9 | if err != nil { 10 | panic(err) 11 | } 12 | return v.(string) 13 | } 14 | -------------------------------------------------------------------------------- /wot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/nbd-wtf/go-nostr" 9 | ) 10 | 11 | var ( 12 | pubkeyFollowerCount = make(map[string]int) 13 | oneHopNetwork []string 14 | wot []string 15 | wotRelays []string 16 | wotMap map[string]bool 17 | ) 18 | 19 | func refreshTrustNetwork() { 20 | ctx := context.Background() 21 | timeout := time.Duration(config.WotFetchTimeoutSeconds) * time.Second 22 | timeoutCtx, cancel := context.WithTimeout(ctx, timeout) 23 | 24 | defer cancel() 25 | ownerPubkey := nPubToPubkey(config.OwnerNpub) 26 | 27 | filter := nostr.Filter{ 28 | Authors: []string{ownerPubkey}, 29 | Kinds: []int{nostr.KindFollowList}, 30 | } 31 | 32 | pool.FetchManyReplaceable(timeoutCtx, config.ImportSeedRelays, filter).Range(func(_ nostr.ReplaceableKey, ev *nostr.Event) bool { 33 | for contact := range ev.Tags.FindAll("p") { 34 | pubkeyFollowerCount[contact[1]]++ 35 | appendOneHopNetwork(contact[1]) 36 | } 37 | 38 | return true 39 | }) 40 | 41 | log.Println("🌐 building web of trust graph") 42 | nPubkeys := uint(0) 43 | for i := 0; i < len(oneHopNetwork); i += 100 { 44 | timeoutCtx, cancel = context.WithTimeout(ctx, timeout) 45 | done := make(chan struct{}) 46 | 47 | end := i + 100 48 | if end > len(oneHopNetwork) { 49 | end = len(oneHopNetwork) 50 | } 51 | 52 | filter = nostr.Filter{ 53 | Authors: oneHopNetwork[i:end], 54 | Kinds: []int{nostr.KindFollowList, nostr.KindRelayListMetadata}, 55 | } 56 | 57 | go func() { 58 | defer cancel() 59 | 60 | pool.FetchManyReplaceable(timeoutCtx, config.ImportSeedRelays, filter).Range(func(_ nostr.ReplaceableKey, ev *nostr.Event) bool { 61 | nPubkeys++ 62 | for contact := range ev.Tags.FindAll("p") { 63 | if len(contact) > 1 { 64 | pubkeyFollowerCount[contact[1]]++ 65 | } 66 | } 67 | 68 | for relay := range ev.Tags.FindAll("r") { 69 | appendRelay(relay[1]) 70 | } 71 | 72 | return true 73 | }) 74 | close(done) 75 | }() 76 | 77 | select { 78 | case <-done: 79 | log.Println("🕸️ analysed", nPubkeys, "followed pubkeys so far") 80 | case <-timeoutCtx.Done(): 81 | log.Println("🚫Timeout while fetching pubkeys, moving to the next batch") 82 | } 83 | } 84 | log.Println("🫂 total network size:", len(pubkeyFollowerCount)) 85 | log.Println("🔗 relays discovered:", len(wotRelays)) 86 | updateWoTMap() 87 | } 88 | 89 | func appendRelay(relay string) { 90 | for _, r := range wotRelays { 91 | if r == relay { 92 | return 93 | } 94 | } 95 | wotRelays = append(wotRelays, relay) 96 | } 97 | 98 | func appendPubkeyToWoT(pubkey string) { 99 | for _, pk := range wot { 100 | if pk == pubkey { 101 | return 102 | } 103 | } 104 | 105 | if len(pubkey) != 64 { 106 | return 107 | } 108 | 109 | wot = append(wot, pubkey) 110 | } 111 | 112 | func appendOneHopNetwork(pubkey string) { 113 | for _, pk := range oneHopNetwork { 114 | if pk == pubkey { 115 | return 116 | } 117 | } 118 | 119 | if len(pubkey) != 64 { 120 | return 121 | } 122 | 123 | oneHopNetwork = append(oneHopNetwork, pubkey) 124 | } 125 | 126 | func updateWoTMap() { 127 | wotMapTmp := make(map[string]bool) 128 | 129 | for pubkey, count := range pubkeyFollowerCount { 130 | if count >= config.ChatRelayMinimumFollowers { 131 | wotMapTmp[pubkey] = true 132 | appendPubkeyToWoT(pubkey) 133 | } 134 | } 135 | 136 | wotMap = wotMapTmp 137 | log.Println("🌐 pubkeys with minimum followers: ", len(wotMap), "keys") 138 | } 139 | --------------------------------------------------------------------------------