package main import ( "bytes" "context" "fmt" "io" "time" "github.com/fiatjaf/eventstore/badger" "github.com/fiatjaf/eventstore/lmdb" "github.com/fiatjaf/khatru" "github.com/fiatjaf/khatru/blossom" "github.com/fiatjaf/khatru/policies" "github.com/nbd-wtf/go-nostr" ) var ( privateRelay = khatru.NewRelay() privateDB = getPrivateDB() ) var ( chatRelay = khatru.NewRelay() chatDB = getChatDB() ) var ( outboxRelay = khatru.NewRelay() outboxDB = getOutboxDB() ) var ( inboxRelay = khatru.NewRelay() inboxDB = getInboxDB() ) type DBBackend interface { Init() error Close() CountEvents(ctx context.Context, filter nostr.Filter) (int64, error) DeleteEvent(ctx context.Context, evt *nostr.Event) error QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) SaveEvent(ctx context.Context, evt *nostr.Event) error Serial() []byte } func getPrivateDB() DBBackend { switch config.DBEngine { case "lmdb": return &lmdb.LMDBBackend{ Path: "db/private", } case "badger": return &badger.BadgerBackend{ Path: "db/private", } default: return &lmdb.LMDBBackend{ Path: "db/private", } } } func getChatDB() DBBackend { switch config.DBEngine { case "lmdb": return &lmdb.LMDBBackend{ Path: "db/chat", } case "badger": return &badger.BadgerBackend{ Path: "db/chat", } default: return &lmdb.LMDBBackend{ Path: "db/chat", } } } func getOutboxDB() DBBackend { switch config.DBEngine { case "lmdb": return &lmdb.LMDBBackend{ Path: "db/outbox", } case "badger": return &badger.BadgerBackend{ Path: "db/outbox", } default: return &lmdb.LMDBBackend{ Path: "db/outbox", } } } func getInboxDB() DBBackend { switch config.DBEngine { case "lmdb": return &lmdb.LMDBBackend{ Path: "db/inbox", } case "badger": return &badger.BadgerBackend{ Path: "db/inbox", } default: return &lmdb.LMDBBackend{ Path: "db/inbox", } } } func initRelays() { if err := privateDB.Init(); err != nil { panic(err) } if err := chatDB.Init(); err != nil { panic(err) } if err := outboxDB.Init(); err != nil { panic(err) } if err := inboxDB.Init(); err != nil { panic(err) } initRelayLimits() privateRelay.Info.Name = config.PrivateRelayName privateRelay.Info.PubKey = nPubToPubkey(config.PrivateRelayNpub) privateRelay.Info.Description = config.PrivateRelayDescription privateRelay.Info.Icon = config.PrivateRelayIcon privateRelay.Info.Version = config.RelayVersion privateRelay.Info.Software = config.RelaySoftware privateRelay.ServiceURL = "https://" + config.RelayURL + "/private" if !privateRelayLimits.AllowEmptyFilters { privateRelay.RejectFilter = append(privateRelay.RejectFilter, policies.NoEmptyFilters) } if !privateRelayLimits.AllowComplexFilters { privateRelay.RejectFilter = append(privateRelay.RejectFilter, policies.NoComplexFilters) } privateRelay.RejectEvent = append(privateRelay.RejectEvent, policies.RejectEventsWithBase64Media, policies.EventIPRateLimiter( privateRelayLimits.EventIPLimiterTokensPerInterval, time.Minute*time.Duration(privateRelayLimits.EventIPLimiterInterval), privateRelayLimits.EventIPLimiterMaxTokens, ), ) privateRelay.RejectConnection = append(privateRelay.RejectConnection, policies.ConnectionRateLimiter( privateRelayLimits.ConnectionRateLimiterTokensPerInterval, time.Minute*time.Duration(privateRelayLimits.ConnectionRateLimiterInterval), privateRelayLimits.ConnectionRateLimiterMaxTokens, ), ) chatRelay.Info.Name = config.ChatRelayName chatRelay.Info.PubKey = nPubToPubkey(config.ChatRelayNpub) chatRelay.Info.Description = config.ChatRelayDescription chatRelay.Info.Icon = config.ChatRelayIcon chatRelay.Info.Version = config.RelayVersion chatRelay.Info.Software = config.RelaySoftware chatRelay.ServiceURL = "https://" + config.RelayURL + "/chat" if !chatRelayLimits.AllowEmptyFilters { chatRelay.RejectFilter = append(chatRelay.RejectFilter, policies.NoEmptyFilters) } if !chatRelayLimits.AllowComplexFilters { chatRelay.RejectFilter = append(chatRelay.RejectFilter, policies.NoComplexFilters) } chatRelay.RejectEvent = append(chatRelay.RejectEvent, policies.RejectEventsWithBase64Media, policies.EventIPRateLimiter( chatRelayLimits.EventIPLimiterTokensPerInterval, time.Minute*time.Duration(chatRelayLimits.EventIPLimiterInterval), chatRelayLimits.EventIPLimiterMaxTokens, ), ) chatRelay.RejectConnection = append(chatRelay.RejectConnection, policies.ConnectionRateLimiter( chatRelayLimits.ConnectionRateLimiterTokensPerInterval, time.Minute*time.Duration(chatRelayLimits.ConnectionRateLimiterInterval), chatRelayLimits.ConnectionRateLimiterMaxTokens, ), ) outboxRelay.Info.Name = config.OutboxRelayName outboxRelay.Info.PubKey = nPubToPubkey(config.OutboxRelayNpub) outboxRelay.Info.Description = config.OutboxRelayDescription outboxRelay.Info.Icon = config.OutboxRelayIcon outboxRelay.Info.Version = config.RelayVersion outboxRelay.Info.Software = config.RelaySoftware if !outboxRelayLimits.AllowEmptyFilters { outboxRelay.RejectFilter = append(outboxRelay.RejectFilter, policies.NoEmptyFilters) } if !outboxRelayLimits.AllowComplexFilters { outboxRelay.RejectFilter = append(outboxRelay.RejectFilter, policies.NoComplexFilters) } outboxRelay.RejectEvent = append(outboxRelay.RejectEvent, policies.RejectEventsWithBase64Media, policies.EventIPRateLimiter( outboxRelayLimits.EventIPLimiterTokensPerInterval, time.Minute*time.Duration(outboxRelayLimits.EventIPLimiterInterval), outboxRelayLimits.EventIPLimiterMaxTokens, ), ) outboxRelay.RejectConnection = append(outboxRelay.RejectConnection, policies.ConnectionRateLimiter( outboxRelayLimits.ConnectionRateLimiterTokensPerInterval, time.Minute*time.Duration(outboxRelayLimits.ConnectionRateLimiterInterval), outboxRelayLimits.ConnectionRateLimiterMaxTokens, ), ) addr := fmt.Sprintf("%s:%d", config.RelayBindAddress, config.RelayPort) bl := blossom.New(outboxRelay, addr) bl.Store = blossom.EventStoreBlobIndexWrapper{Store: outboxDB, ServiceURL: bl.ServiceURL} bl.StoreBlob = append(bl.StoreBlob, func(ctx context.Context, sha256 string, body []byte) error { if khatru.GetAuthed(ctx) != nPubToPubkey(config.OwnerNpub) { return fmt.Errorf("auth-required: only the relay owner can store media") } file, err := fs.Create(config.BlossomPath + sha256) if err != nil { return err } if _, err := io.Copy(file, bytes.NewReader(body)); err != nil { return err } return nil }) bl.LoadBlob = append(bl.LoadBlob, func(ctx context.Context, sha256 string) (io.Reader, error) { return fs.Open(config.BlossomPath + sha256) }) bl.DeleteBlob = append(bl.DeleteBlob, func(ctx context.Context, sha256 string) error { return fs.Remove(config.BlossomPath + sha256) }) inboxRelay.Info.Name = config.InboxRelayName inboxRelay.Info.PubKey = nPubToPubkey(config.InboxRelayNpub) inboxRelay.Info.Description = config.InboxRelayDescription inboxRelay.Info.Icon = config.InboxRelayIcon inboxRelay.Info.Version = config.RelayVersion inboxRelay.Info.Software = config.RelaySoftware inboxRelay.ServiceURL = "https://" + config.RelayURL + "/inbox" if !inboxRelayLimits.AllowEmptyFilters { inboxRelay.RejectFilter = append(inboxRelay.RejectFilter, policies.NoEmptyFilters) } if !inboxRelayLimits.AllowComplexFilters { inboxRelay.RejectFilter = append(inboxRelay.RejectFilter, policies.NoComplexFilters) } inboxRelay.RejectEvent = append(inboxRelay.RejectEvent, policies.RejectEventsWithBase64Media, policies.EventIPRateLimiter( inboxRelayLimits.EventIPLimiterTokensPerInterval, time.Minute*time.Duration(inboxRelayLimits.EventIPLimiterInterval), inboxRelayLimits.EventIPLimiterMaxTokens, ), ) inboxRelay.RejectConnection = append(inboxRelay.RejectConnection, policies.ConnectionRateLimiter( inboxRelayLimits.ConnectionRateLimiterTokensPerInterval, time.Minute*time.Duration(inboxRelayLimits.ConnectionRateLimiterInterval), inboxRelayLimits.ConnectionRateLimiterMaxTokens, ), ) }