Initial commit: Standard Nostr relay with NIP-86 management API
Some checks failed
Build and Push Docker Image / build (push) Failing after 7s
Some checks failed
Build and Push Docker Image / build (push) Failing after 7s
Features: - NIP-86 management API with NIP-98 authentication - Dual mode operation: blacklist (default) or allowlist mode - BoltDB storage for events and management data - Negentropy sync support - Docker deployment ready
This commit is contained in:
commit
f623e7c969
11 changed files with 1120 additions and 0 deletions
6
.dockerignore
Normal file
6
.dockerignore
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.git
|
||||
.env
|
||||
data/
|
||||
*.db
|
||||
*.md
|
||||
!README.md
|
||||
18
.env.example
Normal file
18
.env.example
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Relay operator pubkey (hex format) - becomes the primary admin
|
||||
PUBKEY=
|
||||
|
||||
# Additional admin pubkeys (comma-separated hex format)
|
||||
ADMIN_PUBKEYS=
|
||||
|
||||
# Relay info (NIP-11)
|
||||
NAME=Standard Relay
|
||||
DESCRIPTION=A standard Nostr relay with NIP-86 management API
|
||||
ICON=
|
||||
|
||||
# Server configuration
|
||||
PORT=3334
|
||||
DB_PATH=./data/relay.db
|
||||
|
||||
# Allowlist mode (optional)
|
||||
# When enabled, only pubkeys explicitly added to the allowlist can publish events
|
||||
# ALLOWLIST_MODE=true
|
||||
51
.forgejo/workflows/build.yml
Normal file
51
.forgejo/workflows/build.yml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
name: Build and Push Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
REGISTRY: git.edufeed.org
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker-builder
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Forgejo Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,prefix=
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
|
||||
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Binaries
|
||||
standard-86-relay
|
||||
relay
|
||||
|
||||
# Data
|
||||
data/
|
||||
*.db
|
||||
|
||||
# Environment
|
||||
.env
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
32
Dockerfile
Normal file
32
Dockerfile
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go mod and sum files
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# Download dependencies
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/standard-86-relay .
|
||||
|
||||
# Start a new stage from scratch
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
# Copy the binary from the builder stage
|
||||
COPY --from=builder /app/standard-86-relay .
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p /data
|
||||
|
||||
# Expose port 3334
|
||||
EXPOSE 3334
|
||||
|
||||
# Command to run the executable
|
||||
CMD ["./standard-86-relay"]
|
||||
123
README.md
Normal file
123
README.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# Standard-86-Relay
|
||||
|
||||
A standard Nostr relay with NIP-86 Management API support built on [khatru](https://github.com/fiatjaf/khatru).
|
||||
|
||||
## Features
|
||||
|
||||
- **NIP-86 Management API**: Full support for relay management via HTTP/JSON-RPC
|
||||
- **NIP-98 Authentication**: Secure admin authentication using signed Nostr events
|
||||
- **BoltDB Storage**: Embedded key-value storage for events and management data
|
||||
- **Negentropy Sync**: Efficient event synchronization with other relays
|
||||
- **Dual Mode Operation**: Choose between blacklist (ban) or allowlist (whitelist) modes
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PUBKEY` | Relay operator pubkey (hex format) - becomes the primary admin | Required |
|
||||
| `ADMIN_PUBKEYS` | Additional admin pubkeys (comma-separated hex) | Optional |
|
||||
| `NAME` | Relay name (NIP-11) | `Standard Relay` |
|
||||
| `DESCRIPTION` | Relay description (NIP-11) | Empty |
|
||||
| `ICON` | Relay icon URL (NIP-11) | Empty |
|
||||
| `PORT` | HTTP server port | `3334` |
|
||||
| `DB_PATH` | Path to BoltDB database file | `./data/relay.db` |
|
||||
| `ALLOWLIST_MODE` | Enable allowlist mode (`true`/`false`) | `false` |
|
||||
|
||||
## Operating Modes
|
||||
|
||||
### Blacklist Mode (Default)
|
||||
|
||||
In blacklist mode, all pubkeys can publish events unless explicitly banned:
|
||||
|
||||
- Use `banpubkey` to block a pubkey from publishing
|
||||
- Use `allowpubkey` to unban a previously banned pubkey
|
||||
- `listbannedpubkeys` returns all banned pubkeys
|
||||
|
||||
### Allowlist Mode
|
||||
|
||||
When `ALLOWLIST_MODE=true`, only explicitly allowed pubkeys can publish:
|
||||
|
||||
- On startup, operator and admin pubkeys are automatically added to the allowlist
|
||||
- Use `allowpubkey` to add a pubkey to the allowlist
|
||||
- Use `listallowedpubkeys` to view all allowed pubkeys
|
||||
- Pubkeys not in the allowlist receive "pubkey not in allowlist" rejection
|
||||
|
||||
## NIP-86 API Methods
|
||||
|
||||
All methods require NIP-98 authentication with an admin pubkey.
|
||||
|
||||
### Pubkey Management
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `banpubkey` | Ban a pubkey from publishing (params: `[pubkey, reason?]`) |
|
||||
| `allowpubkey` | Add to allowlist AND remove from ban list (params: `[pubkey, reason?]`) |
|
||||
| `listbannedpubkeys` | List all banned pubkeys with reasons |
|
||||
| `listallowedpubkeys` | List all allowed pubkeys with reasons |
|
||||
|
||||
### Event Management
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `banevent` | Ban an event by ID (params: `[id, reason?]`) |
|
||||
| `allowevent` | Remove event from ban list (params: `[id, reason?]`) |
|
||||
| `listbannedevents` | List all banned event IDs with reasons |
|
||||
|
||||
### Relay Configuration
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `changerelayname` | Update relay name (params: `[name]`) |
|
||||
| `changerelaydescription` | Update relay description (params: `[description]`) |
|
||||
| `changerelayicon` | Update relay icon URL (params: `[url]`) |
|
||||
|
||||
### Admin Management
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `grantadmin` | Grant admin access to a pubkey (params: `[pubkey, methods?]`) |
|
||||
| `revokeadmin` | Revoke admin access (params: `[pubkey, methods?]`) |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Using Docker
|
||||
|
||||
```bash
|
||||
docker build -t standard-86-relay .
|
||||
docker run -d \
|
||||
-e PUBKEY=your_hex_pubkey \
|
||||
-e NAME="My Relay" \
|
||||
-p 3334:3334 \
|
||||
-v relay-data:/app/data \
|
||||
standard-86-relay
|
||||
```
|
||||
|
||||
### Using Docker Compose
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your pubkey and settings
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
go build -o relay .
|
||||
PUBKEY=your_hex_pubkey ./relay
|
||||
```
|
||||
|
||||
## Example API Call
|
||||
|
||||
```bash
|
||||
# Create NIP-98 auth event and call API
|
||||
curl -X POST http://localhost:3334 \
|
||||
-H "Content-Type: application/nostr+json+rpc" \
|
||||
-H "Authorization: Nostr <base64-encoded-signed-event>" \
|
||||
-d '{"method":"listbannedpubkeys","params":[]}'
|
||||
```
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [relay-manager](../relay-manager) - Web UI for managing this relay
|
||||
- [khatru](https://github.com/fiatjaf/khatru) - The underlying relay framework
|
||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
services:
|
||||
standard-86-relay:
|
||||
build:
|
||||
context: .
|
||||
restart: on-failure
|
||||
ports:
|
||||
- "${PORT:-3334}:${PORT:-3334}"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
environment:
|
||||
- NAME=${NAME:-Standard Relay}
|
||||
- DESCRIPTION=${DESCRIPTION:-A standard Nostr relay with NIP-86 management API}
|
||||
- ICON=${ICON}
|
||||
- PUBKEY=${PUBKEY}
|
||||
- ADMIN_PUBKEYS=${ADMIN_PUBKEYS}
|
||||
- PORT=${PORT:-3334}
|
||||
- DB_PATH=/data/relay.db
|
||||
54
go.mod
Normal file
54
go.mod
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
module git.edufeed.org/edufeed/standard-86-relay
|
||||
|
||||
go 1.25.5
|
||||
|
||||
replace fiatjaf.com/nostr => git.edufeed.org/edufeed/nostrlib v0.0.0-20260210102229-b6e185a36e88
|
||||
|
||||
require (
|
||||
fiatjaf.com/nostr v0.0.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
go.etcd.io/bbolt v1.4.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/FastFilter/xorfilter v0.2.1 // indirect
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/btcsuite/btcd v0.24.2 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coder/websocket v1.8.13 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/elnosh/gonuts v0.4.2 // indirect
|
||||
github.com/fasthttp/websocket v1.5.12 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/rs/cors v1.11.1 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
||||
github.com/templexxx/cpu v0.0.1 // indirect
|
||||
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.59.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
209
go.sum
Normal file
209
go.sum
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
git.edufeed.org/edufeed/nostrlib v0.0.0-20260210102229-b6e185a36e88 h1:KkwsihRPz6UEMyjBqeINew5sDBwlS2jV45Ng8tyRGwg=
|
||||
git.edufeed.org/edufeed/nostrlib v0.0.0-20260210102229-b6e185a36e88/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
||||
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
||||
github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I=
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
|
||||
github.com/PowerDNS/lmdb-go v1.9.3 h1:AUMY2pZT8WRpkEv39I9Id3MuoHd+NZbTVpNhruVkPTg=
|
||||
github.com/PowerDNS/lmdb-go v1.9.3/go.mod h1:TE0l+EZK8Z1B4dx070ZxkWTlp8RG1mjN0/+FkFRQMtU=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
|
||||
github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY=
|
||||
github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
|
||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk=
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/elnosh/gonuts v0.4.2 h1:/WubPAWGxTE+okJ0WPvmtEzTzpi04RGxiTHAF1FYU+M=
|
||||
github.com/elnosh/gonuts v0.4.2/go.mod h1:vgZomh4YQk7R3w4ltZc0sHwCmndfHkuX6V4sga/8oNs=
|
||||
github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=
|
||||
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
github.com/templexxx/cpu v0.0.1 h1:hY4WdLOgKdc8y13EYklu9OUTXik80BkxHoWvTO6MQQY=
|
||||
github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
|
||||
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b h1:XeDLE6c9mzHpdv3Wb1+pWBaWv/BlHK0ZYIu/KaL6eHg=
|
||||
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b/go.mod h1:7rwmCH0wC2fQvNEvPZ3sKXukhyCTyiaZ5VTZMQYpZKQ=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
|
||||
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
|
||||
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
|
||||
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
290
main.go
Normal file
290
main.go
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/eventstore/boltdb"
|
||||
"fiatjaf.com/nostr/khatru"
|
||||
"fiatjaf.com/nostr/khatru/policies"
|
||||
"fiatjaf.com/nostr/nip11"
|
||||
"fiatjaf.com/nostr/nip86"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load .env file (optional — Docker passes env vars directly)
|
||||
if err := godotenv.Load(); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
fmt.Printf("Error loading .env file: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// BoltDB backend (event persistence)
|
||||
dbPath := os.Getenv("DB_PATH")
|
||||
if dbPath == "" {
|
||||
dbPath = "./data/relay.db"
|
||||
}
|
||||
if dir := filepath.Dir(dbPath); dir != "." {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
panic(fmt.Sprintf("failed to create data directory %s: %v", dir, err))
|
||||
}
|
||||
}
|
||||
boltDB := boltdb.BoltBackend{Path: dbPath}
|
||||
if err := boltDB.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer boltDB.Close()
|
||||
|
||||
// Management store (bans + admins) — shares the same bbolt database
|
||||
mgmt := ManagementStore{}
|
||||
if err := mgmt.Init(boltDB.DB); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create relay
|
||||
relay := khatru.NewRelay()
|
||||
|
||||
// Relay info from environment
|
||||
relay.Info.Name = getEnvOrDefault("NAME", "Standard Relay")
|
||||
relay.Info.Description = os.Getenv("DESCRIPTION")
|
||||
relay.Info.Icon = os.Getenv("ICON")
|
||||
|
||||
// Override with persisted values if set
|
||||
if name := mgmt.GetRelayInfo("name"); name != "" {
|
||||
relay.Info.Name = name
|
||||
}
|
||||
if desc := mgmt.GetRelayInfo("description"); desc != "" {
|
||||
relay.Info.Description = desc
|
||||
}
|
||||
if icon := mgmt.GetRelayInfo("icon"); icon != "" {
|
||||
relay.Info.Icon = icon
|
||||
}
|
||||
|
||||
// NIP-11: Software identification
|
||||
relay.Info.Software = "https://git.edufeed.org/edufeed/standard-86-relay"
|
||||
relay.Info.Version = getVersion()
|
||||
|
||||
// NIP-11: Supported NIPs
|
||||
relay.Info.AddSupportedNIPs([]int{9, 45, 86}) // deletion, count, management
|
||||
|
||||
// NIP-11: Limitations
|
||||
relay.Info.Limitation = &nip11.RelayLimitationDocument{
|
||||
MaxLimit: 500,
|
||||
RestrictedWrites: false,
|
||||
AuthRequired: false,
|
||||
}
|
||||
|
||||
// Parse relay operator pubkey
|
||||
var operatorPK nostr.PubKey
|
||||
if pkHex := os.Getenv("PUBKEY"); pkHex != "" {
|
||||
pk, err := nostr.PubKeyFromHex(pkHex)
|
||||
if err != nil {
|
||||
fmt.Printf("Error parsing PUBKEY: %v\n", err)
|
||||
} else {
|
||||
operatorPK = pk
|
||||
relay.Info.PubKey = &pk
|
||||
// Ensure operator is always admin with full access
|
||||
mgmt.GrantAdmin(pk, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Build admin pubkey set from environment
|
||||
var adminPubkeys []nostr.PubKey
|
||||
if adminList := os.Getenv("ADMIN_PUBKEYS"); adminList != "" {
|
||||
for _, hex := range strings.Split(adminList, ",") {
|
||||
hex = strings.TrimSpace(hex)
|
||||
if pk, err := nostr.PubKeyFromHex(hex); err == nil {
|
||||
mgmt.GrantAdmin(pk, nil) // full access
|
||||
adminPubkeys = append(adminPubkeys, pk)
|
||||
} else {
|
||||
fmt.Printf("Error parsing admin pubkey %q: %v\n", hex, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure allowlist mode from environment
|
||||
if os.Getenv("ALLOWLIST_MODE") == "true" {
|
||||
mgmt.SetAllowlistMode(true)
|
||||
fmt.Println("Relay running in ALLOWLIST mode")
|
||||
// Auto-add operator and admin pubkeys to allowlist
|
||||
if operatorPK != (nostr.PubKey{}) {
|
||||
mgmt.AddToAllowlist(operatorPK, "operator")
|
||||
}
|
||||
for _, pk := range adminPubkeys {
|
||||
mgmt.AddToAllowlist(pk, "admin")
|
||||
}
|
||||
}
|
||||
|
||||
// Use the event store
|
||||
relay.UseEventstore(&boltDB, 500)
|
||||
|
||||
// Enable negentropy sync
|
||||
relay.Negentropy = true
|
||||
|
||||
// Set up policies
|
||||
relay.OnEvent = policies.SeqEvent(
|
||||
policies.ValidateKind,
|
||||
policies.PreventTimestampsInTheFuture(time.Minute*10),
|
||||
policies.PreventLargeContent(100000),
|
||||
func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
|
||||
if mgmt.IsAllowlistMode() {
|
||||
// Allowlist mode: reject if NOT in allowlist
|
||||
if !mgmt.IsAllowedPubKey(event.PubKey) {
|
||||
return true, "pubkey not in allowlist"
|
||||
}
|
||||
} else {
|
||||
// Blacklist mode: reject if banned
|
||||
if mgmt.IsPubKeyBanned(event.PubKey) {
|
||||
return true, "pubkey is banned"
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
},
|
||||
func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
|
||||
if mgmt.IsEventBanned(event.ID) {
|
||||
return true, "event is banned"
|
||||
}
|
||||
return false, ""
|
||||
},
|
||||
)
|
||||
|
||||
relay.OnRequest = policies.SeqRequest(
|
||||
policies.NoEmptyFilters,
|
||||
policies.NoComplexFilters,
|
||||
)
|
||||
|
||||
// NIP-86 Management API
|
||||
relay.ManagementAPI.OnAPICall = func(ctx context.Context, mp nip86.MethodParams) (reject bool, msg string) {
|
||||
authed, ok := khatru.GetAuthed(ctx)
|
||||
if !ok {
|
||||
return true, "authentication required"
|
||||
}
|
||||
|
||||
// Check if pubkey can call this method
|
||||
if !mgmt.CanCallMethod(authed, mp.MethodName()) {
|
||||
return true, "not authorized for this method"
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Pubkey management
|
||||
relay.ManagementAPI.BanPubKey = func(ctx context.Context, pubkey nostr.PubKey, reason string) error {
|
||||
// Remove from allowlist when banning
|
||||
mgmt.RemoveFromAllowlist(pubkey)
|
||||
return mgmt.BanPubKey(pubkey, reason)
|
||||
}
|
||||
relay.ManagementAPI.ListBannedPubKeys = func(ctx context.Context) ([]nip86.PubKeyReason, error) {
|
||||
return mgmt.ListBannedPubKeys()
|
||||
}
|
||||
relay.ManagementAPI.AllowPubKey = func(ctx context.Context, pubkey nostr.PubKey, reason string) error {
|
||||
// Always add to allowlist
|
||||
if err := mgmt.AddToAllowlist(pubkey, reason); err != nil {
|
||||
return err
|
||||
}
|
||||
// Also remove from ban list (unban)
|
||||
return mgmt.AllowPubKey(pubkey)
|
||||
}
|
||||
relay.ManagementAPI.ListAllowedPubKeys = func(ctx context.Context) ([]nip86.PubKeyReason, error) {
|
||||
return mgmt.ListAllowedPubKeys()
|
||||
}
|
||||
|
||||
// Event management
|
||||
relay.ManagementAPI.BanEvent = func(ctx context.Context, id nostr.ID, reason string) error {
|
||||
// Also delete the event from storage
|
||||
if err := boltDB.DeleteEvent(id); err != nil {
|
||||
fmt.Printf("Warning: could not delete event %s: %v\n", id, err)
|
||||
}
|
||||
return mgmt.BanEvent(id, reason)
|
||||
}
|
||||
relay.ManagementAPI.ListBannedEvents = func(ctx context.Context) ([]nip86.IDReason, error) {
|
||||
return mgmt.ListBannedEvents()
|
||||
}
|
||||
relay.ManagementAPI.AllowEvent = func(ctx context.Context, id nostr.ID, reason string) error {
|
||||
return mgmt.AllowEvent(id)
|
||||
}
|
||||
|
||||
// Relay info management (persisted)
|
||||
relay.ManagementAPI.ChangeRelayName = func(ctx context.Context, name string) error {
|
||||
relay.Info.Name = name
|
||||
return mgmt.SaveRelayInfo("name", name)
|
||||
}
|
||||
relay.ManagementAPI.ChangeRelayDescription = func(ctx context.Context, desc string) error {
|
||||
relay.Info.Description = desc
|
||||
return mgmt.SaveRelayInfo("description", desc)
|
||||
}
|
||||
relay.ManagementAPI.ChangeRelayIcon = func(ctx context.Context, icon string) error {
|
||||
relay.Info.Icon = icon
|
||||
return mgmt.SaveRelayInfo("icon", icon)
|
||||
}
|
||||
|
||||
// Admin management
|
||||
relay.ManagementAPI.GrantAdmin = func(ctx context.Context, pubkey nostr.PubKey, methods []string) error {
|
||||
// Don't allow granting to yourself or modifying the operator
|
||||
if pubkey == operatorPK {
|
||||
return nil // silently ignore
|
||||
}
|
||||
return mgmt.GrantAdmin(pubkey, methods)
|
||||
}
|
||||
relay.ManagementAPI.RevokeAdmin = func(ctx context.Context, pubkey nostr.PubKey, methods []string) error {
|
||||
// Don't allow revoking the operator
|
||||
if pubkey == operatorPK {
|
||||
return nil // silently ignore
|
||||
}
|
||||
return mgmt.RevokeAdmin(pubkey, methods)
|
||||
}
|
||||
|
||||
// Stats
|
||||
relay.ManagementAPI.Stats = func(ctx context.Context) (nip86.Response, error) {
|
||||
count, _ := boltDB.CountEvents(nostr.Filter{})
|
||||
return nip86.Response{
|
||||
Result: map[string]any{
|
||||
"event_count": count,
|
||||
"uptime": time.Since(startTime).String(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "3334"
|
||||
}
|
||||
fmt.Printf("Standard relay running on :%s\n", port)
|
||||
if operatorPK != (nostr.PubKey{}) {
|
||||
fmt.Printf("Operator pubkey: %s\n", operatorPK.Hex())
|
||||
}
|
||||
http.ListenAndServe(":"+port, relay)
|
||||
}
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
// getVersion returns the git commit hash from build info, or "dev" if unavailable.
|
||||
func getVersion() string {
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
for _, setting := range info.Settings {
|
||||
if setting.Key == "vcs.revision" {
|
||||
if len(setting.Value) > 7 {
|
||||
return setting.Value[:7]
|
||||
}
|
||||
return setting.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
return "dev"
|
||||
}
|
||||
|
||||
// getEnvOrDefault returns the environment variable value or a default.
|
||||
func getEnvOrDefault(key, defaultValue string) string {
|
||||
if val := os.Getenv(key); val != "" {
|
||||
return val
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
306
management.go
Normal file
306
management.go
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip86"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var (
|
||||
bucketBannedPubKeys = []byte("banned_pubkeys")
|
||||
bucketBannedEvents = []byte("banned_events")
|
||||
bucketAllowedPubKeys = []byte("allowed_pubkeys")
|
||||
bucketAdmins = []byte("admins")
|
||||
bucketRelayInfo = []byte("relay_info")
|
||||
)
|
||||
|
||||
const (
|
||||
relayInfoKeyAllowlistMode = "allowlist_mode"
|
||||
)
|
||||
|
||||
type ManagementStore struct {
|
||||
DB *bbolt.DB
|
||||
}
|
||||
|
||||
func (m *ManagementStore) Init(db *bbolt.DB) error {
|
||||
m.DB = db
|
||||
return db.Update(func(tx *bbolt.Tx) error {
|
||||
for _, bucket := range [][]byte{bucketBannedPubKeys, bucketBannedEvents, bucketAllowedPubKeys, bucketAdmins, bucketRelayInfo} {
|
||||
if _, err := tx.CreateBucketIfNotExists(bucket); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type reasonEntry struct {
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type adminEntry struct {
|
||||
Methods []string `json:"methods,omitempty"` // empty = full access
|
||||
}
|
||||
|
||||
// BanPubKey adds a pubkey to the ban list.
|
||||
func (m *ManagementStore) BanPubKey(pubkey nostr.PubKey, reason string) error {
|
||||
val, _ := json.Marshal(reasonEntry{Reason: reason})
|
||||
return m.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.Bucket(bucketBannedPubKeys).Put([]byte(pubkey.Hex()), val)
|
||||
})
|
||||
}
|
||||
|
||||
// AllowPubKey removes a pubkey from the ban list.
|
||||
func (m *ManagementStore) AllowPubKey(pubkey nostr.PubKey) error {
|
||||
return m.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.Bucket(bucketBannedPubKeys).Delete([]byte(pubkey.Hex()))
|
||||
})
|
||||
}
|
||||
|
||||
// ListBannedPubKeys returns all banned pubkeys.
|
||||
func (m *ManagementStore) ListBannedPubKeys() ([]nip86.PubKeyReason, error) {
|
||||
result := []nip86.PubKeyReason{} // Initialize as empty slice, not nil
|
||||
err := m.DB.View(func(tx *bbolt.Tx) error {
|
||||
return tx.Bucket(bucketBannedPubKeys).ForEach(func(k, v []byte) error {
|
||||
pk, err := nostr.PubKeyFromHex(string(k))
|
||||
if err != nil {
|
||||
return nil // skip invalid entries
|
||||
}
|
||||
var entry reasonEntry
|
||||
json.Unmarshal(v, &entry)
|
||||
result = append(result, nip86.PubKeyReason{PubKey: pk, Reason: entry.Reason})
|
||||
return nil
|
||||
})
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// IsPubKeyBanned checks if a pubkey is banned.
|
||||
func (m *ManagementStore) IsPubKeyBanned(pubkey nostr.PubKey) bool {
|
||||
var banned bool
|
||||
m.DB.View(func(tx *bbolt.Tx) error {
|
||||
if tx.Bucket(bucketBannedPubKeys).Get([]byte(pubkey.Hex())) != nil {
|
||||
banned = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return banned
|
||||
}
|
||||
|
||||
// AddToAllowlist adds a pubkey to the allowlist.
|
||||
func (m *ManagementStore) AddToAllowlist(pubkey nostr.PubKey, reason string) error {
|
||||
val, _ := json.Marshal(reasonEntry{Reason: reason})
|
||||
return m.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.Bucket(bucketAllowedPubKeys).Put([]byte(pubkey.Hex()), val)
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveFromAllowlist removes a pubkey from the allowlist.
|
||||
func (m *ManagementStore) RemoveFromAllowlist(pubkey nostr.PubKey) error {
|
||||
return m.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.Bucket(bucketAllowedPubKeys).Delete([]byte(pubkey.Hex()))
|
||||
})
|
||||
}
|
||||
|
||||
// ListAllowedPubKeys returns all allowed pubkeys.
|
||||
func (m *ManagementStore) ListAllowedPubKeys() ([]nip86.PubKeyReason, error) {
|
||||
result := []nip86.PubKeyReason{} // Initialize as empty slice, not nil
|
||||
err := m.DB.View(func(tx *bbolt.Tx) error {
|
||||
return tx.Bucket(bucketAllowedPubKeys).ForEach(func(k, v []byte) error {
|
||||
pk, err := nostr.PubKeyFromHex(string(k))
|
||||
if err != nil {
|
||||
return nil // skip invalid entries
|
||||
}
|
||||
var entry reasonEntry
|
||||
json.Unmarshal(v, &entry)
|
||||
result = append(result, nip86.PubKeyReason{PubKey: pk, Reason: entry.Reason})
|
||||
return nil
|
||||
})
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// IsAllowedPubKey checks if a pubkey is in the allowlist.
|
||||
func (m *ManagementStore) IsAllowedPubKey(pubkey nostr.PubKey) bool {
|
||||
var allowed bool
|
||||
m.DB.View(func(tx *bbolt.Tx) error {
|
||||
if tx.Bucket(bucketAllowedPubKeys).Get([]byte(pubkey.Hex())) != nil {
|
||||
allowed = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return allowed
|
||||
}
|
||||
|
||||
// SetAllowlistMode enables or disables allowlist mode.
|
||||
func (m *ManagementStore) SetAllowlistMode(enabled bool) error {
|
||||
value := "false"
|
||||
if enabled {
|
||||
value = "true"
|
||||
}
|
||||
return m.SaveRelayInfo(relayInfoKeyAllowlistMode, value)
|
||||
}
|
||||
|
||||
// IsAllowlistMode returns true if the relay is in allowlist mode.
|
||||
func (m *ManagementStore) IsAllowlistMode() bool {
|
||||
return m.GetRelayInfo(relayInfoKeyAllowlistMode) == "true"
|
||||
}
|
||||
|
||||
// BanEvent adds an event ID to the ban list.
|
||||
func (m *ManagementStore) BanEvent(id nostr.ID, reason string) error {
|
||||
val, _ := json.Marshal(reasonEntry{Reason: reason})
|
||||
return m.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.Bucket(bucketBannedEvents).Put([]byte(id.Hex()), val)
|
||||
})
|
||||
}
|
||||
|
||||
// AllowEvent removes an event ID from the ban list.
|
||||
func (m *ManagementStore) AllowEvent(id nostr.ID) error {
|
||||
return m.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.Bucket(bucketBannedEvents).Delete([]byte(id.Hex()))
|
||||
})
|
||||
}
|
||||
|
||||
// ListBannedEvents returns all banned event IDs.
|
||||
func (m *ManagementStore) ListBannedEvents() ([]nip86.IDReason, error) {
|
||||
result := []nip86.IDReason{} // Initialize as empty slice, not nil
|
||||
err := m.DB.View(func(tx *bbolt.Tx) error {
|
||||
return tx.Bucket(bucketBannedEvents).ForEach(func(k, v []byte) error {
|
||||
id, err := nostr.IDFromHex(string(k))
|
||||
if err != nil {
|
||||
return nil // skip invalid entries
|
||||
}
|
||||
var entry reasonEntry
|
||||
json.Unmarshal(v, &entry)
|
||||
result = append(result, nip86.IDReason{ID: id, Reason: entry.Reason})
|
||||
return nil
|
||||
})
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// IsEventBanned checks if an event is banned.
|
||||
func (m *ManagementStore) IsEventBanned(id nostr.ID) bool {
|
||||
var banned bool
|
||||
m.DB.View(func(tx *bbolt.Tx) error {
|
||||
if tx.Bucket(bucketBannedEvents).Get([]byte(id.Hex())) != nil {
|
||||
banned = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return banned
|
||||
}
|
||||
|
||||
// GrantAdmin adds or updates admin permissions for a pubkey.
|
||||
func (m *ManagementStore) GrantAdmin(pubkey nostr.PubKey, methods []string) error {
|
||||
return m.DB.Update(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket(bucketAdmins)
|
||||
key := []byte(pubkey.Hex())
|
||||
|
||||
// Get existing entry
|
||||
var entry adminEntry
|
||||
if existing := bucket.Get(key); existing != nil {
|
||||
json.Unmarshal(existing, &entry)
|
||||
}
|
||||
|
||||
if len(methods) == 0 {
|
||||
// Grant full access
|
||||
entry.Methods = nil
|
||||
} else {
|
||||
// Add new methods to existing
|
||||
for _, m := range methods {
|
||||
if !slices.Contains(entry.Methods, m) {
|
||||
entry.Methods = append(entry.Methods, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val, _ := json.Marshal(entry)
|
||||
return bucket.Put(key, val)
|
||||
})
|
||||
}
|
||||
|
||||
// RevokeAdmin removes admin permissions for a pubkey.
|
||||
func (m *ManagementStore) RevokeAdmin(pubkey nostr.PubKey, methods []string) error {
|
||||
return m.DB.Update(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket(bucketAdmins)
|
||||
key := []byte(pubkey.Hex())
|
||||
|
||||
if len(methods) == 0 {
|
||||
// Revoke all access
|
||||
return bucket.Delete(key)
|
||||
}
|
||||
|
||||
// Get existing entry
|
||||
existing := bucket.Get(key)
|
||||
if existing == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var entry adminEntry
|
||||
json.Unmarshal(existing, &entry)
|
||||
|
||||
// Remove specific methods
|
||||
entry.Methods = slices.DeleteFunc(entry.Methods, func(m string) bool {
|
||||
return slices.Contains(methods, m)
|
||||
})
|
||||
|
||||
val, _ := json.Marshal(entry)
|
||||
return bucket.Put(key, val)
|
||||
})
|
||||
}
|
||||
|
||||
// IsAdmin checks if a pubkey is an admin.
|
||||
func (m *ManagementStore) IsAdmin(pubkey nostr.PubKey) bool {
|
||||
var isAdmin bool
|
||||
m.DB.View(func(tx *bbolt.Tx) error {
|
||||
if tx.Bucket(bucketAdmins).Get([]byte(pubkey.Hex())) != nil {
|
||||
isAdmin = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return isAdmin
|
||||
}
|
||||
|
||||
// CanCallMethod checks if a pubkey can call a specific NIP-86 method.
|
||||
func (m *ManagementStore) CanCallMethod(pubkey nostr.PubKey, method string) bool {
|
||||
var canCall bool
|
||||
m.DB.View(func(tx *bbolt.Tx) error {
|
||||
data := tx.Bucket(bucketAdmins).Get([]byte(pubkey.Hex()))
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
var entry adminEntry
|
||||
json.Unmarshal(data, &entry)
|
||||
// Empty methods = full access
|
||||
if len(entry.Methods) == 0 {
|
||||
canCall = true
|
||||
return nil
|
||||
}
|
||||
canCall = slices.Contains(entry.Methods, method)
|
||||
return nil
|
||||
})
|
||||
return canCall
|
||||
}
|
||||
|
||||
// SaveRelayInfo stores relay info (name, description, icon) persistently.
|
||||
func (m *ManagementStore) SaveRelayInfo(key, value string) error {
|
||||
return m.DB.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.Bucket(bucketRelayInfo).Put([]byte(key), []byte(value))
|
||||
})
|
||||
}
|
||||
|
||||
// GetRelayInfo retrieves relay info.
|
||||
func (m *ManagementStore) GetRelayInfo(key string) string {
|
||||
var value string
|
||||
m.DB.View(func(tx *bbolt.Tx) error {
|
||||
if data := tx.Bucket(bucketRelayInfo).Get([]byte(key)); data != nil {
|
||||
value = string(data)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return value
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue