96 lines
3.7 KiB
Go
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)
|
|
}
|
|
}
|