mirror of
https://github.com/aljazceru/haven.git
synced 2025-12-17 05:44:20 +01:00
320 lines
9.4 KiB
Go
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
|
|
}
|
|
}
|