From 3a4bdd9b0af982d4eba62412fc328abd3f34f27d Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 15 Feb 2026 17:11:39 +0100 Subject: [PATCH] bug-fixes --- cmd/server/main.go | 2 +- internal/generator/html.go | 17 ++++- internal/nostr/profile.go | 117 +++++++++++++++++++++++++++++++ internal/server/handlers.go | 23 ++++-- internal/server/server.go | 6 +- static/GitCitadel_Icon_Black.svg | 3 + templates/base.html | 2 +- templates/contact.html | 47 +++++++++++++ 8 files changed, 206 insertions(+), 11 deletions(-) create mode 100644 internal/nostr/profile.go create mode 100644 static/GitCitadel_Icon_Black.svg diff --git a/cmd/server/main.go b/cmd/server/main.go index 64635ae..5fc42a0 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -87,7 +87,7 @@ func main() { rewarmer.Start(ctx) // Initialize HTTP server - httpServer := server.NewServer(cfg.Server.Port, pageCache, feedCache, issueService, cfg.RepoAnnouncement, htmlGenerator) + httpServer := server.NewServer(cfg.Server.Port, pageCache, feedCache, issueService, cfg.RepoAnnouncement, htmlGenerator, nostrClient) // Start server in goroutine go func() { diff --git a/internal/generator/html.go b/internal/generator/html.go index 03598a4..65bf8a6 100644 --- a/internal/generator/html.go +++ b/internal/generator/html.go @@ -342,7 +342,7 @@ func (g *HTMLGenerator) GenerateEBooksPage(ebooks []EBookInfo, feedItems []FeedI } // GenerateContactPage generates the contact form page -func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, eventID string, formData map[string]string, repoAnnouncement *nostr.RepoAnnouncement, feedItems []FeedItemInfo) (string, error) { +func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, eventID string, formData map[string]string, repoAnnouncement *nostr.RepoAnnouncement, feedItems []FeedItemInfo, profile *nostr.Profile) (string, error) { // Prepare form data with defaults subject := "" content := "" @@ -447,6 +447,21 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event } } + // Add profile data + if profile != nil { + templateData["Profile"] = map[string]interface{}{ + "Pubkey": profile.Pubkey, + "Name": profile.Name, + "DisplayName": profile.DisplayName, + "About": profile.About, + "Picture": profile.Picture, + "Website": profile.Website, + "NIP05": profile.NIP05, + "Lud16": profile.Lud16, + "Banner": profile.Banner, + } + } + // Execute base.html which will use the blocks from contact.html var buf bytes.Buffer if err := renderTmpl.ExecuteTemplate(&buf, "base.html", templateData); err != nil { diff --git a/internal/nostr/profile.go b/internal/nostr/profile.go new file mode 100644 index 0000000..4c44d23 --- /dev/null +++ b/internal/nostr/profile.go @@ -0,0 +1,117 @@ +package nostr + +import ( + "context" + "encoding/json" + "fmt" + "log" + + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip19" +) + +// Profile represents a parsed kind 0 profile event +type Profile struct { + Event *nostr.Event + Pubkey string + Name string + DisplayName string + About string + Picture string + Website string + NIP05 string + Lud16 string + Banner string + RawJSON map[string]interface{} // Store all other fields +} + +// FetchProfile fetches a kind 0 profile event from an npub +func (c *Client) FetchProfile(ctx context.Context, npub string) (*Profile, error) { + // Decode npub to get pubkey + prefix, value, err := nip19.Decode(npub) + if err != nil { + return nil, fmt.Errorf("failed to decode npub: %w", err) + } + + if prefix != "npub" { + return nil, fmt.Errorf("invalid npub prefix: expected 'npub', got '%s'", prefix) + } + + pubkey, ok := value.(string) + if !ok { + return nil, fmt.Errorf("failed to parse npub: unexpected type") + } + + // Create filter for kind 0 profile event + filter := nostr.Filter{ + Kinds: []int{0}, + Authors: []string{pubkey}, + Limit: 1, + } + + log.Printf("Fetching profile for npub %s (pubkey: %s)", npub, pubkey) + + // Fetch the event + event, err := c.FetchEvent(ctx, filter) + if err != nil { + return nil, fmt.Errorf("failed to fetch profile event: %w", err) + } + + // Parse the profile + profile, err := ParseProfile(event) + if err != nil { + return nil, fmt.Errorf("failed to parse profile: %w", err) + } + + return profile, nil +} + +// ParseProfile parses a kind 0 profile event +func ParseProfile(event *nostr.Event) (*Profile, error) { + if event.Kind != 0 { + return nil, fmt.Errorf("expected kind 0, got %d", event.Kind) + } + + profile := &Profile{ + Event: event, + Pubkey: event.PubKey, + RawJSON: make(map[string]interface{}), + } + + // Parse JSON content + var content map[string]interface{} + if err := json.Unmarshal([]byte(event.Content), &content); err != nil { + return nil, fmt.Errorf("failed to parse profile JSON: %w", err) + } + + // Extract common fields + if name, ok := content["name"].(string); ok { + profile.Name = name + } + if displayName, ok := content["display_name"].(string); ok { + profile.DisplayName = displayName + } + if about, ok := content["about"].(string); ok { + profile.About = about + } + if picture, ok := content["picture"].(string); ok { + profile.Picture = picture + } + if website, ok := content["website"].(string); ok { + profile.Website = website + } + if nip05, ok := content["nip05"].(string); ok { + profile.NIP05 = nip05 + } + if lud16, ok := content["lud16"].(string); ok { + profile.Lud16 = lud16 + } + if banner, ok := content["banner"].(string); ok { + profile.Banner = banner + } + + // Store all fields in RawJSON for access to any other fields + profile.RawJSON = content + + return profile, nil +} diff --git a/internal/server/handlers.go b/internal/server/handlers.go index c08a916..069a3a1 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -120,8 +120,19 @@ func (s *Server) handleContact(w http.ResponseWriter, r *http.Request) { } } + // Fetch profile for npub + var profile *nostr.Profile + npub := "npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz" + if s.nostrClient != nil { + profile, err = s.nostrClient.FetchProfile(ctx, npub) + if err != nil { + log.Printf("Failed to fetch profile for contact page: %v", err) + // Continue without profile - not critical + } + } + // Render the contact form (feed items not needed - only on landing page) - html, err := s.htmlGenerator.GenerateContactPage(false, "", "", nil, repoAnnouncement, []generator.FeedItemInfo{}) + html, err := s.htmlGenerator.GenerateContactPage(false, "", "", nil, repoAnnouncement, []generator.FeedItemInfo{}, profile) if err != nil { http.Error(w, "Failed to generate contact page", http.StatusInternalServerError) return @@ -138,7 +149,7 @@ func (s *Server) handleContact(w http.ResponseWriter, r *http.Request) { // Parse form data if err := r.ParseForm(); err != nil { - html, _ := s.htmlGenerator.GenerateContactPage(false, "Failed to parse form data", "", nil, nil, []generator.FeedItemInfo{}) + html, _ := s.htmlGenerator.GenerateContactPage(false, "Failed to parse form data", "", nil, nil, []generator.FeedItemInfo{}, nil) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(html)) return @@ -155,7 +166,7 @@ func (s *Server) handleContact(w http.ResponseWriter, r *http.Request) { "content": content, "labels": labelsStr, } - html, _ := s.htmlGenerator.GenerateContactPage(false, "Subject and message are required", "", formData, nil, []generator.FeedItemInfo{}) + html, _ := s.htmlGenerator.GenerateContactPage(false, "Subject and message are required", "", formData, nil, []generator.FeedItemInfo{}, nil) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(html)) return @@ -185,7 +196,7 @@ func (s *Server) handleContact(w http.ResponseWriter, r *http.Request) { "content": content, "labels": labelsStr, } - html, _ := s.htmlGenerator.GenerateContactPage(false, "Failed to connect to repository. Please try again later.", "", formData, nil, []generator.FeedItemInfo{}) + html, _ := s.htmlGenerator.GenerateContactPage(false, "Failed to connect to repository. Please try again later.", "", formData, nil, []generator.FeedItemInfo{}, nil) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(html)) return @@ -207,14 +218,14 @@ func (s *Server) handleContact(w http.ResponseWriter, r *http.Request) { "content": content, "labels": labelsStr, } - html, _ := s.htmlGenerator.GenerateContactPage(false, "Failed to submit your message. Please try again later.", "", formData, nil, []generator.FeedItemInfo{}) + html, _ := s.htmlGenerator.GenerateContactPage(false, "Failed to submit your message. Please try again later.", "", formData, nil, []generator.FeedItemInfo{}, nil) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(html)) return } // Success - render success page - html, err := s.htmlGenerator.GenerateContactPage(true, "", eventID, nil, repoAnnouncement, []generator.FeedItemInfo{}) + html, err := s.htmlGenerator.GenerateContactPage(true, "", eventID, nil, repoAnnouncement, []generator.FeedItemInfo{}, nil) if err != nil { http.Error(w, "Failed to generate success page", http.StatusInternalServerError) return diff --git a/internal/server/server.go b/internal/server/server.go index 0d4a90d..dca1ffe 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -26,6 +26,7 @@ type Server struct { issueService IssueServiceInterface repoAnnouncement string htmlGenerator HTMLGeneratorInterface + nostrClient *nostr.Client } // IssueServiceInterface defines the interface for issue service @@ -37,12 +38,12 @@ type IssueServiceInterface interface { // HTMLGeneratorInterface defines the interface for HTML generator type HTMLGeneratorInterface interface { - GenerateContactPage(success bool, errorMsg string, eventID string, formData map[string]string, repoAnnouncement *nostr.RepoAnnouncement, feedItems []generator.FeedItemInfo) (string, error) + GenerateContactPage(success bool, errorMsg string, eventID string, formData map[string]string, repoAnnouncement *nostr.RepoAnnouncement, feedItems []generator.FeedItemInfo, profile *nostr.Profile) (string, error) GenerateErrorPage(statusCode int, feedItems []generator.FeedItemInfo) (string, error) } // NewServer creates a new HTTP server -func NewServer(port int, pageCache *cache.Cache, feedCache *cache.FeedCache, issueService IssueServiceInterface, repoAnnouncement string, htmlGenerator HTMLGeneratorInterface) *Server { +func NewServer(port int, pageCache *cache.Cache, feedCache *cache.FeedCache, issueService IssueServiceInterface, repoAnnouncement string, htmlGenerator HTMLGeneratorInterface, nostrClient *nostr.Client) *Server { s := &Server{ cache: pageCache, feedCache: feedCache, @@ -50,6 +51,7 @@ func NewServer(port int, pageCache *cache.Cache, feedCache *cache.FeedCache, iss issueService: issueService, repoAnnouncement: repoAnnouncement, htmlGenerator: htmlGenerator, + nostrClient: nostrClient, } mux := http.NewServeMux() diff --git a/static/GitCitadel_Icon_Black.svg b/static/GitCitadel_Icon_Black.svg new file mode 100644 index 0000000..115c93b --- /dev/null +++ b/static/GitCitadel_Icon_Black.svg @@ -0,0 +1,3 @@ + + + diff --git a/templates/base.html b/templates/base.html index b400480..a6e2c18 100644 --- a/templates/base.html +++ b/templates/base.html @@ -22,7 +22,7 @@ - + diff --git a/templates/contact.html b/templates/contact.html index f16501f..6bf9030 100644 --- a/templates/contact.html +++ b/templates/contact.html @@ -1,6 +1,53 @@ {{define "content"}}

Contact Us

+ + + + {{if .Profile}} +
+

Nostr Profile

+
+ {{if .Profile.Picture}} + Profile picture + {{end}} +
+

+ {{if .Profile.DisplayName}}{{.Profile.DisplayName}}{{else if .Profile.Name}}{{.Profile.Name}}{{else}}Anonymous{{end}} +

+ {{if .Profile.Name}} +

@{{.Profile.Name}}

+ {{end}} + {{if .Profile.About}} +

{{.Profile.About}}

+ {{end}} + {{if .Profile.Website}} +

+ 🌐 Website +

+ {{end}} + {{if .Profile.NIP05}} +

NIP-05: {{.Profile.NIP05}}

+ {{end}} +
+
+
+ {{end}} +

Have a question, suggestion, or want to report an issue? Fill out the form below and we'll get back to you.

{{if .Success}}