Files
nostermd-relay/cmd/nostermd/main.go
T
rgoodall 294616642f
ci / build (push) Successful in 2m3s
ci / image (push) Successful in 1m4s
initial public relay build repo
2026-07-24 13:11:06 -04:00

96 lines
3.7 KiB
Go

// Command nostermd is a minimal NIP-29 managed-group relay built on khatru,
// plus the Nosterm capability handshake (a `features` array in its NIP-11 doc).
//
// It is intentionally small: it enforces group membership for chat writes,
// auto-creates open channels on first join, and advertises itself as a
// "nostermd" so a Nosterm client can feature-gate its UI. The relay is
// assembled from focused internal packages (group, federation, retention,
// relayinfo); this file is the composition root that wires them to env config
// and the network.
package main
import (
"log"
"net/http"
"os"
"path/filepath"
"time"
"fiatjaf.com/nostr/eventstore/boltdb"
"github.com/nosterm/relay/internal/federation"
"github.com/nosterm/relay/internal/retention"
)
// defaultMOTD is the built-in message-of-the-day: a "nerdworks.io" ascii banner
// (figlet, standard font) the client shows when RELAY_MOTD is unset.
const defaultMOTD = " _ _ _\n" +
" _ __ ___ _ __ __| |_ _____ _ __| | _____ (_) ___\n" +
"| '_ \\ / _ \\ '__/ _` \\ \\ /\\ / / _ \\| '__| |/ / __| | |/ _ \\\n" +
"| | | | __/ | | (_| |\\ V V / (_) | | | <\\__ \\_| | (_) |\n" +
"|_| |_|\\___|_| \\__,_| \\_/\\_/ \\___/|_| |_|\\_\\___(_)_|\\___/\n"
func main() {
addr := envOr("RELAY_ADDR", ":3334")
dataDir := envOr("RELAY_DATA_DIR", "./data")
// Embedded, pure-Go storage (no cgo → clean static Docker builds).
if err := os.MkdirAll(dataDir, 0o755); err != nil {
log.Fatalf("cannot create data dir: %v", err)
}
// Relay identity: signs the 39000-series group metadata/roster events. A
// stable key is required so those signatures survive restarts (see below).
sk := loadOrGenerateKey(dataDir)
store := &boltdb.BoltBackend{Path: filepath.Join(dataDir, "events.bolt")}
if err := store.Init(); err != nil {
log.Fatalf("cannot open eventstore: %v", err)
}
// Parse optional federation config up front so warnings surface at boot.
fedCfg, fedWarnings := federation.ParseConfig(os.Getenv("RELAY_FEDERATION_PEERS"))
for _, w := range fedWarnings {
log.Printf("federation config: %s", w)
}
app := buildRelay(relayConfig{
secretKey: sk,
store: store,
name: envOr("RELAY_NAME", "Nosterm Home Relay"),
description: envOr("RELAY_DESCRIPTION", "A Nosterm NIP-29 managed-group relay."),
// NIP-11 contact: a NIP-05-style identifier / address for the relay operator.
contact: envOr("RELAY_CONTACT", "relay@nosterm.com"),
// Optional message-of-the-day shown in the client's relay server window;
// defaults to the nerdworks.io banner when unset.
motd: envOr("RELAY_MOTD", defaultMOTD),
fedCfg: fedCfg,
})
// Reconstruct membership/metadata from persisted events so groups survive
// restarts (without this, every restart silently drops all group state).
if replayed := app.groups.Rebuild(store); replayed > 0 {
log.Printf("replayed %d group management event(s) from storage", replayed)
}
// Retention: prune old chat messages so the store doesn't grow forever.
stopRetention := retention.Start(store, retention.Config{
MaxAge: time.Duration(envIntOr("RELAY_RETENTION_DAYS", 0)) * 24 * time.Hour,
MaxMessages: envIntOr("RELAY_RETENTION_MAX_MESSAGES", 0),
Interval: time.Duration(envIntOr("RELAY_RETENTION_INTERVAL_MINUTES", 60)) * time.Minute,
})
defer stopRetention()
// Start federation last, once the relay pipeline is fully wired.
if app.federator != nil {
app.federator.Start()
defer app.federator.Stop()
}
log.Printf("nostermd pubkey: %s", app.pubKey.Hex())
log.Printf("listening on %s (%s; %s)", addr, app.groups.Summary(), fedCfg.Summary())
if err := http.ListenAndServe(addr, app.handler); err != nil {
log.Fatal(err)
}
}