├── .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 |
46 |
47 | Built with Love by
48 | Bitvora
54 | and Proudly powered by
55 | Khatru
61 | |
62 | Haven Relay on Github
68 |
69 |
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 |
--------------------------------------------------------------------------------