Files
haven/main.go
2024-10-29 18:50:36 -04:00

320 lines
9.4 KiB
Go

package main
import (
"bytes"
"context"
"flag"
"fmt"
"io"
"log"
"net/http"
"text/template"
"github.com/fiatjaf/khatru"
"github.com/fiatjaf/khatru/blossom"
"github.com/nbd-wtf/go-nostr"
"github.com/puzpuzpuz/xsync/v3"
"github.com/spf13/afero"
)
var (
mainRelay = khatru.NewRelay()
subRelays = xsync.NewMapOf[string, *khatru.Relay]()
pool = nostr.NewSimplePool(context.Background())
config = loadConfig()
fs afero.Fs
)
func main() {
importFlag := flag.Bool("import", false, "Run the importNotes function after initializing relays")
flag.Parse()
nostr.InfoLogger = log.New(io.Discard, "", 0)
green := "\033[32m"
reset := "\033[0m"
fmt.Println(green + art + reset)
log.Println("🚀 haven is booting up")
fs = afero.NewOsFs()
fs.MkdirAll(config.BlossomPath, 0755)
initRelays()
go func() {
refreshTrustNetwork()
if *importFlag {
log.Println("📦 importing notes")
importOwnerNotes()
importTaggedNotes()
return
}
go subscribeInbox()
go backupDatabase()
}()
http.HandleFunc("/", dynamicRelayHandler)
addr := fmt.Sprintf("%s:%d", config.RelayBindAddress, config.RelayPort)
log.Printf("🔗 listening at %s", addr)
http.ListenAndServe(addr, nil)
}
func dynamicRelayHandler(w http.ResponseWriter, r *http.Request) {
var relay *khatru.Relay
relayType := r.URL.Path
if relayType == "" {
relay = mainRelay
} else {
relay, _ = subRelays.LoadOrCompute(relayType, func() *khatru.Relay {
return makeNewRelay(relayType, w, r)
})
}
relay.ServeHTTP(w, r)
}
func makeNewRelay(relayType string, w http.ResponseWriter, r *http.Request) *khatru.Relay {
switch relayType {
case "/private":
privateRelay.OnConnect = append(privateRelay.OnConnect, func(ctx context.Context) {
khatru.RequestAuth(ctx)
})
privateRelay.StoreEvent = append(privateRelay.StoreEvent, privateDB.SaveEvent)
privateRelay.QueryEvents = append(privateRelay.QueryEvents, privateDB.QueryEvents)
privateRelay.DeleteEvent = append(privateRelay.DeleteEvent, privateDB.DeleteEvent)
privateRelay.RejectFilter = append(privateRelay.RejectFilter, func(ctx context.Context, filter nostr.Filter) (bool, string) {
authenticatedUser := khatru.GetAuthed(ctx)
if authenticatedUser == nPubToPubkey(config.OwnerNpub) {
return false, ""
}
return true, "auth-required: this query requires you to be authenticated"
})
privateRelay.RejectEvent = append(privateRelay.RejectEvent, func(ctx context.Context, event *nostr.Event) (bool, string) {
authenticatedUser := khatru.GetAuthed(ctx)
if authenticatedUser == nPubToPubkey(config.OwnerNpub) {
return false, ""
}
return true, "auth-required: publishing this event requires authentication"
})
mux := privateRelay.Router()
mux.HandleFunc("/private", func(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("templates/index.html"))
data := struct {
RelayName string
RelayPubkey string
RelayDescription string
RelayURL string
}{
RelayName: config.PrivateRelayName,
RelayPubkey: nPubToPubkey(config.PrivateRelayNpub),
RelayDescription: config.PrivateRelayDescription,
RelayURL: "wss://" + config.RelayURL + "/private",
}
err := tmpl.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
return privateRelay
case "/chat":
chatRelay.OnConnect = append(chatRelay.OnConnect, func(ctx context.Context) {
khatru.RequestAuth(ctx)
})
chatRelay.StoreEvent = append(chatRelay.StoreEvent, chatDB.SaveEvent)
chatRelay.QueryEvents = append(chatRelay.QueryEvents, chatDB.QueryEvents)
chatRelay.DeleteEvent = append(chatRelay.DeleteEvent, chatDB.DeleteEvent)
chatRelay.RejectFilter = append(chatRelay.RejectFilter, func(ctx context.Context, filter nostr.Filter) (bool, string) {
authenticatedUser := khatru.GetAuthed(ctx)
if !wotMap[authenticatedUser] {
return true, "you must be in the web of trust to chat with the relay owner"
}
return false, ""
})
allowedKinds := []int{
nostr.KindSimpleGroupAddPermission,
nostr.KindSimpleGroupAddUser,
nostr.KindSimpleGroupAdmins,
nostr.KindSimpleGroupChatMessage,
nostr.KindSimpleGroupCreateGroup,
nostr.KindSimpleGroupDeleteEvent,
nostr.KindSimpleGroupDeleteGroup,
nostr.KindSimpleGroupEditGroupStatus,
nostr.KindSimpleGroupEditMetadata,
nostr.KindSimpleGroupJoinRequest,
nostr.KindSimpleGroupLeaveRequest,
nostr.KindSimpleGroupMembers,
nostr.KindSimpleGroupMetadata,
nostr.KindSimpleGroupRemovePermission,
nostr.KindSimpleGroupRemoveUser,
nostr.KindSimpleGroupReply,
nostr.KindSimpleGroupThread,
nostr.KindChannelHideMessage,
nostr.KindChannelMessage,
nostr.KindGiftWrap,
}
chatRelay.RejectEvent = append(chatRelay.RejectEvent, func(ctx context.Context, event *nostr.Event) (bool, string) {
for _, kind := range allowedKinds {
if event.Kind == kind {
return false, ""
}
}
return true, "only gift wrapped DMs are allowed"
})
mux := chatRelay.Router()
mux.HandleFunc("/chat", func(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("templates/index.html"))
data := struct {
RelayName string
RelayPubkey string
RelayDescription string
RelayURL string
}{
RelayName: config.ChatRelayName,
RelayPubkey: nPubToPubkey(config.ChatRelayNpub),
RelayDescription: config.ChatRelayDescription,
RelayURL: "wss://" + config.RelayURL + "/chat",
}
err := tmpl.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
return chatRelay
case "/inbox":
inboxRelay.StoreEvent = append(inboxRelay.StoreEvent, inboxDB.SaveEvent)
inboxRelay.QueryEvents = append(inboxRelay.QueryEvents, inboxDB.QueryEvents)
inboxRelay.DeleteEvent = append(inboxRelay.DeleteEvent, inboxDB.DeleteEvent)
inboxRelay.RejectEvent = append(inboxRelay.RejectEvent, func(ctx context.Context, event *nostr.Event) (bool, string) {
if !wotMap[event.PubKey] {
return true, "you must be in the web of trust to post to this relay"
}
if event.Kind == nostr.KindEncryptedDirectMessage {
return true, "only gift wrapped DMs are supported"
}
for _, tag := range event.Tags.GetAll([]string{"p"}) {
if tag[1] == inboxRelay.Info.PubKey {
return false, ""
}
}
return true, "you can only post notes if you've tagged the owner of this relay"
})
mux := inboxRelay.Router()
mux.HandleFunc("/inbox", func(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("templates/index.html"))
data := struct {
RelayName string
RelayPubkey string
RelayDescription string
RelayURL string
}{
RelayName: config.InboxRelayName,
RelayPubkey: nPubToPubkey(config.InboxRelayNpub),
RelayDescription: config.InboxRelayDescription,
RelayURL: "wss://" + config.RelayURL + "/inbox",
}
err := tmpl.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
return inboxRelay
default: // default to outbox
outboxRelay.QueryEvents = append(outboxRelay.QueryEvents, outboxDB.QueryEvents)
outboxRelay.DeleteEvent = append(outboxRelay.DeleteEvent, outboxDB.DeleteEvent)
outboxRelay.RejectEvent = append(outboxRelay.RejectEvent, func(ctx context.Context, event *nostr.Event) (bool, string) {
if event.PubKey == nPubToPubkey(config.OwnerNpub) {
return false, ""
}
return true, "only notes signed by the owner of this relay are allowed"
})
outboxRelay.StoreEvent = append(outboxRelay.StoreEvent, outboxDB.SaveEvent, func(ctx context.Context, event *nostr.Event) error {
go blast(event)
return nil
})
mux := outboxRelay.Router()
mux.HandleFunc(relayType, func(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("templates/index.html"))
data := struct {
RelayName string
RelayPubkey string
RelayDescription string
RelayURL string
}{
RelayName: config.OutboxRelayName,
RelayPubkey: nPubToPubkey(config.OutboxRelayNpub),
RelayDescription: config.OutboxRelayDescription,
RelayURL: "wss://" + config.RelayURL + "/outbox",
}
err := tmpl.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
bl := blossom.New(outboxRelay, "https://"+config.RelayURL)
bl.Store = blossom.EventStoreBlobIndexWrapper{Store: outboxDB, ServiceURL: bl.ServiceURL}
bl.StoreBlob = append(bl.StoreBlob, func(ctx context.Context, sha256 string, body []byte) error {
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)
})
bl.RejectUpload = append(bl.RejectUpload, func(ctx context.Context, event *nostr.Event, size int, ext string) (bool, string, int) {
if event.PubKey == nPubToPubkey(config.OwnerNpub) {
return false, ext, size
}
return true, "only notes signed by the owner of this relay are allowed", 0
})
return outboxRelay
}
}