From 07dbb55b5531e9362d35afb1d549e4c157f506fb Mon Sep 17 00:00:00 2001 From: Silberengel Date: Mon, 16 Feb 2026 16:00:21 +0100 Subject: [PATCH] refactor --- internal/config/config.go | 28 ++-- internal/nostr/client.go | 26 ++- internal/nostr/ebooks.go | 53 +----- internal/nostr/events.go | 325 +++++++++++++----------------------- internal/nostr/feed.go | 23 +-- internal/nostr/issues.go | 27 +-- internal/nostr/wiki.go | 133 ++++----------- internal/server/handlers.go | 46 ++--- 8 files changed, 206 insertions(+), 455 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 37fc969..94c460d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -96,12 +96,12 @@ func LoadConfig(path string) (*Config, error) { return &config, nil } -// GetProfilesRelays parses the comma-separated profiles relay string into a slice -func (c *Config) GetProfilesRelays() []string { - if c.Relays.Profiles == "" { +// parseRelayList parses a comma-separated relay string into a slice +func parseRelayList(relayStr string) []string { + if relayStr == "" { return []string{} } - relays := strings.Split(c.Relays.Profiles, ",") + relays := strings.Split(relayStr, ",") result := make([]string, 0, len(relays)) for _, relay := range relays { relay = strings.TrimSpace(relay) @@ -114,22 +114,14 @@ func (c *Config) GetProfilesRelays() []string { return result } +// GetProfilesRelays parses the comma-separated profiles relay string into a slice +func (c *Config) GetProfilesRelays() []string { + return parseRelayList(c.Relays.Profiles) +} + // GetContactFormRelays parses the comma-separated contact form relay string into a slice func (c *Config) GetContactFormRelays() []string { - if c.Relays.ContactForm == "" { - return []string{} - } - relays := strings.Split(c.Relays.ContactForm, ",") - result := make([]string, 0, len(relays)) - for _, relay := range relays { - relay = strings.TrimSpace(relay) - // Remove quotes if present - relay = strings.Trim(relay, "\"'") - if relay != "" { - result = append(result, relay) - } - } - return result + return parseRelayList(c.Relays.ContactForm) } // Validate validates the configuration diff --git a/internal/nostr/client.go b/internal/nostr/client.go index bec4a83..f1d6186 100644 --- a/internal/nostr/client.go +++ b/internal/nostr/client.go @@ -3,6 +3,7 @@ package nostr import ( "context" "fmt" + "sort" "time" "gitcitadel-online/internal/logger" @@ -284,17 +285,16 @@ func (c *Client) FetchDeletionEvents(ctx context.Context, authors []string) (map } // Parse deletion events - extract event IDs from "e" tags + // According to NIP-09, "e" tags have format ["e", "event_id", ...] + // Only the first element (event_id) should be used, not additional elements deletedEventIDs := make(map[string]*nostr.Event) for _, deletionEvent := range deletionEvents { // Kind 5 events have "e" tags with the event IDs they're deleting - for _, tag := range deletionEvent.Tags { - if len(tag) > 0 && tag[0] == "e" && len(tag) > 1 { - eventID := tag[1] - // Keep the newest deletion event if multiple deletions exist - existing, exists := deletedEventIDs[eventID] - if !exists || deletionEvent.CreatedAt > existing.CreatedAt { - deletedEventIDs[eventID] = deletionEvent - } + for _, eventID := range getETagValues(deletionEvent.Tags) { + // Keep the newest deletion event if multiple deletions exist + existing, exists := deletedEventIDs[eventID] + if !exists || deletionEvent.CreatedAt > existing.CreatedAt { + deletedEventIDs[eventID] = deletionEvent } } } @@ -558,13 +558,9 @@ func (c *Client) ProcessEventsWithCache( allEvents = FilterDeletedEvents(allEvents, deletedEventIDs) // Step 6: Sort newest-first (by created_at descending) - for i := 0; i < len(allEvents)-1; i++ { - for j := i + 1; j < len(allEvents); j++ { - if allEvents[i].CreatedAt < allEvents[j].CreatedAt { - allEvents[i], allEvents[j] = allEvents[j], allEvents[i] - } - } - } + sort.Slice(allEvents, func(i, j int) bool { + return allEvents[i].CreatedAt > allEvents[j].CreatedAt + }) logger.WithFields(map[string]interface{}{ "after_deletion": len(allEvents), diff --git a/internal/nostr/ebooks.go b/internal/nostr/ebooks.go index 25c1ca0..7fc5c88 100644 --- a/internal/nostr/ebooks.go +++ b/internal/nostr/ebooks.go @@ -108,57 +108,18 @@ func (es *EBooksService) FetchTopLevelIndexEvents(ctx context.Context) ([]EBookI CreatedAt: int64(event.CreatedAt), } - // Extract d tag - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "d" && len(tag) > 1 { - ebook.DTag = tag[1] - break - } - } - - // Extract title - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "title" && len(tag) > 1 { - ebook.Title = tag[1] - break - } - } + // Extract common tags using utilities + ebook.DTag = getDTag(event.Tags) + ebook.Title = getTitle(event.Tags) if ebook.Title == "" { ebook.Title = ebook.DTag // Fallback to d tag } + ebook.Summary = getSummary(event.Tags) + ebook.Type = getTagValue(event.Tags, "type") + ebook.Image = getImage(event.Tags) - // Extract summary - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "summary" && len(tag) > 1 { - ebook.Summary = tag[1] - break - } - } - - // Extract type - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "type" && len(tag) > 1 { - ebook.Type = tag[1] - break - } - } - - // Extract image - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "image" && len(tag) > 1 { - ebook.Image = tag[1] - break - } - } - - // Build naddr - create proper bech32-encoded naddr // Extract relay hints from event tags if available - var relays []string - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "relays" { - relays = append(relays, tag[1:]...) - } - } + relays := getAllTagValues(event.Tags, "relays") // If no relays in tags, use the relay we fetched from if len(relays) == 0 { relays = []string{es.relayURL} diff --git a/internal/nostr/events.go b/internal/nostr/events.go index 67ffe68..f028f4a 100644 --- a/internal/nostr/events.go +++ b/internal/nostr/events.go @@ -7,6 +7,97 @@ import ( "github.com/nbd-wtf/go-nostr" ) +// Tag utilities for extracting common tags from events + +// getTagValue extracts the first value for a given tag name +func getTagValue(tags nostr.Tags, tagName string) string { + for _, tag := range tags { + if len(tag) > 0 && tag[0] == tagName && len(tag) > 1 { + return tag[1] + } + } + return "" +} + +// getAllTagValues extracts all values for a given tag name +// For tags that may have multiple values (like "relays", "maintainers") +func getAllTagValues(tags nostr.Tags, tagName string) []string { + var values []string + for _, tag := range tags { + if len(tag) > 0 && tag[0] == tagName && len(tag) > 1 { + values = append(values, tag[1:]...) + } + } + return values +} + +// getETagValues extracts event IDs from "e" tags +// According to NIP-09, "e" tags have format ["e", "event_id", ...] +// Only the first element (event_id) should be extracted, not additional elements like relay URLs or markers +func getETagValues(tags nostr.Tags) []string { + var eventIDs []string + for _, tag := range tags { + if len(tag) > 0 && tag[0] == "e" && len(tag) > 1 { + // Only take the first element (event_id), ignore additional elements + eventIDs = append(eventIDs, tag[1]) + } + } + return eventIDs +} + +// getDTag extracts the d tag from an event +func getDTag(tags nostr.Tags) string { + return getTagValue(tags, "d") +} + +// getTitle extracts the title tag from an event +func getTitle(tags nostr.Tags) string { + return getTagValue(tags, "title") +} + +// getSummary extracts the summary tag from an event +func getSummary(tags nostr.Tags) string { + return getTagValue(tags, "summary") +} + +// getImage extracts the image tag from an event +func getImage(tags nostr.Tags) string { + return getTagValue(tags, "image") +} + +// ContentEvent represents a generic content event with common fields +type ContentEvent struct { + Event *nostr.Event + DTag string + Title string + Summary string + Content string + Image string +} + +// parseContentEvent parses common content event fields (used by wiki, blog, longform) +func parseContentEvent(event *nostr.Event, expectedKind int) (*ContentEvent, error) { + if event.Kind != expectedKind { + return nil, fmt.Errorf("expected kind %d, got %d", expectedKind, event.Kind) + } + + ce := &ContentEvent{ + Event: event, + Content: event.Content, + DTag: getDTag(event.Tags), + Title: getTitle(event.Tags), + Summary: getSummary(event.Tags), + Image: getImage(event.Tags), + } + + // Fallback title to d tag if not set + if ce.Title == "" { + ce.Title = ce.DTag + } + + return ce, nil +} + // IndexEvent represents a kind 30040 publication index event (NKBIP-01) type IndexEvent struct { Event *nostr.Event @@ -37,26 +128,15 @@ func ParseIndexEvent(event *nostr.Event, expectedKind int) (*IndexEvent, error) } index := &IndexEvent{ - Event: event, - Author: event.PubKey, - } - - // Extract d tag - var dTag string - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "d" && len(tag) > 1 { - dTag = tag[1] - break - } - } - index.DTag = dTag - - // Extract title - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "title" && len(tag) > 1 { - index.Title = tag[1] - break - } + Event: event, + Author: event.PubKey, + DTag: getDTag(event.Tags), + Title: getTitle(event.Tags), + Summary: getSummary(event.Tags), + Image: getImage(event.Tags), + Type: getTagValue(event.Tags, "type"), + Version: getTagValue(event.Tags, "version"), + AutoUpdate: getTagValue(event.Tags, "auto-update"), } // Extract 'a' tags (index items) @@ -86,224 +166,49 @@ func ParseIndexEvent(event *nostr.Event, expectedKind int) (*IndexEvent, error) } } - // Extract auto-update tag - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "auto-update" && len(tag) > 1 { - index.AutoUpdate = tag[1] - break - } - } - - // Extract type tag - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "type" && len(tag) > 1 { - index.Type = tag[1] - break - } - } - - // Extract version tag - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "version" && len(tag) > 1 { - index.Version = tag[1] - break - } - } - - // Extract summary tag - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "summary" && len(tag) > 1 { - index.Summary = tag[1] - break - } - } - - // Extract image tag - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "image" && len(tag) > 1 { - index.Image = tag[1] - break - } - } - return index, nil } // WikiEvent represents a kind 30818 wiki event (NIP-54) type WikiEvent struct { - Event *nostr.Event - DTag string - Title string - Summary string - Content string - Image string + *ContentEvent } // ParseWikiEvent parses a wiki event according to NIP-54 func ParseWikiEvent(event *nostr.Event, expectedKind int) (*WikiEvent, error) { - if event.Kind != expectedKind { - return nil, fmt.Errorf("expected kind %d, got %d", expectedKind, event.Kind) + ce, err := parseContentEvent(event, expectedKind) + if err != nil { + return nil, err } - - wiki := &WikiEvent{ - Event: event, - Content: event.Content, - } - - // Extract d tag (normalized identifier) - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "d" && len(tag) > 1 { - wiki.DTag = tag[1] - break - } - } - - // Extract title tag (optional, falls back to d tag) - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "title" && len(tag) > 1 { - wiki.Title = tag[1] - break - } - } - if wiki.Title == "" { - wiki.Title = wiki.DTag - } - - // Extract summary tag (optional) - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "summary" && len(tag) > 1 { - wiki.Summary = tag[1] - break - } - } - - // Extract image tag (optional) - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "image" && len(tag) > 1 { - wiki.Image = tag[1] - break - } - } - - return wiki, nil + return &WikiEvent{ContentEvent: ce}, nil } // BlogEvent represents a kind 30041 blog event type BlogEvent struct { - Event *nostr.Event - DTag string - Title string - Summary string - Content string - Image string + *ContentEvent } // ParseBlogEvent parses a blog event func ParseBlogEvent(event *nostr.Event, expectedKind int) (*BlogEvent, error) { - if event.Kind != expectedKind { - return nil, fmt.Errorf("expected kind %d, got %d", expectedKind, event.Kind) - } - - blog := &BlogEvent{ - Event: event, - Content: event.Content, - } - - // Extract d tag (normalized identifier) - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "d" && len(tag) > 1 { - blog.DTag = tag[1] - break - } - } - - // Extract title tag (optional, falls back to d tag) - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "title" && len(tag) > 1 { - blog.Title = tag[1] - break - } - } - if blog.Title == "" { - blog.Title = blog.DTag + ce, err := parseContentEvent(event, expectedKind) + if err != nil { + return nil, err } - - // Extract summary tag (optional) - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "summary" && len(tag) > 1 { - blog.Summary = tag[1] - break - } - } - - // Extract image tag (optional) - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "image" && len(tag) > 1 { - blog.Image = tag[1] - break - } - } - - return blog, nil + return &BlogEvent{ContentEvent: ce}, nil } // LongformEvent represents a kind 30023 longform article event type LongformEvent struct { - Event *nostr.Event - DTag string - Title string - Summary string - Content string - Image string + *ContentEvent } // ParseLongformEvent parses a longform article event func ParseLongformEvent(event *nostr.Event, expectedKind int) (*LongformEvent, error) { - if event.Kind != expectedKind { - return nil, fmt.Errorf("expected kind %d, got %d", expectedKind, event.Kind) + ce, err := parseContentEvent(event, expectedKind) + if err != nil { + return nil, err } - - longform := &LongformEvent{ - Event: event, - Content: event.Content, - } - - // Extract d tag (normalized identifier) - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "d" && len(tag) > 1 { - longform.DTag = tag[1] - break - } - } - - // Extract title tag (optional, falls back to d tag) - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "title" && len(tag) > 1 { - longform.Title = tag[1] - break - } - } - if longform.Title == "" { - longform.Title = longform.DTag - } - - // Extract summary tag (optional) - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "summary" && len(tag) > 1 { - longform.Summary = tag[1] - break - } - } - - // Extract image tag (optional) - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "image" && len(tag) > 1 { - longform.Image = tag[1] - break - } - } - - return longform, nil + return &LongformEvent{ContentEvent: ce}, nil } // NormalizeDTag normalizes a d tag according to NIP-54 rules diff --git a/internal/nostr/feed.go b/internal/nostr/feed.go index 653c32f..57db998 100644 --- a/internal/nostr/feed.go +++ b/internal/nostr/feed.go @@ -40,29 +40,20 @@ func (fs *FeedService) FetchFeedItems(ctx context.Context, feedRelay string, max // Convert events to feed items items := make([]FeedItem, 0, len(result.Events)) for _, event := range result.Events { - item := FeedItem{ + item := FeedItem{ EventID: event.ID, Author: event.PubKey, Content: event.Content, Time: time.Unix(int64(event.CreatedAt), 0), Link: fmt.Sprintf("https://alexandria.gitcitadel.eu/events?id=nevent1%s", event.ID), - } + } - // Extract title, summary, and image tags - for _, tag := range event.Tags { - if len(tag) > 0 && len(tag) > 1 { - switch tag[0] { - case "title": - item.Title = tag[1] - case "summary": - item.Summary = tag[1] - case "image": - item.Image = tag[1] - } - } - } + // Extract title, summary, and image tags + item.Title = getTitle(event.Tags) + item.Summary = getSummary(event.Tags) + item.Image = getImage(event.Tags) - items = append(items, item) + items = append(items, item) } logger.WithFields(map[string]interface{}{ diff --git a/internal/nostr/issues.go b/internal/nostr/issues.go index d7dcd58..8d4bd47 100644 --- a/internal/nostr/issues.go +++ b/internal/nostr/issues.go @@ -51,31 +51,16 @@ func ParseRepoAnnouncement(event *nostr.Event, expectedKind int) (*RepoAnnouncem } // Extract d tag - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "d" && len(tag) > 1 { - repo.DTag = tag[1] - break - } - } + repo.DTag = getDTag(event.Tags) // Validate that DTag is set if repo.DTag == "" { return nil, fmt.Errorf("repository announcement event missing d tag") } - // Extract relays tag - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "relays" && len(tag) > 1 { - repo.Relays = append(repo.Relays, tag[1:]...) - } - } - - // Extract maintainers tag - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "maintainers" && len(tag) > 1 { - repo.Maintainers = append(repo.Maintainers, tag[1:]...) - } - } + // Extract relays and maintainers tags + repo.Relays = getAllTagValues(event.Tags, "relays") + repo.Maintainers = getAllTagValues(event.Tags, "maintainers") return repo, nil } @@ -195,6 +180,8 @@ func (s *IssueService) PublishIssue(ctx context.Context, repoAnnouncement *RepoA } err = relay.Publish(ctx, *event) + // Note: SimplePool manages connections, but we close here for explicit cleanup + // Closing after publish attempt ensures cleanup regardless of success/failure relay.Close() if err != nil { lastErr = err @@ -253,6 +240,8 @@ func (s *IssueService) PublishSignedIssue(ctx context.Context, signedEvent *nost } err = relay.Publish(ctx, *signedEvent) + // Note: SimplePool manages connections, but we close here for explicit cleanup + // Closing after publish attempt ensures cleanup regardless of success/failure relay.Close() if err != nil { lastErr = err diff --git a/internal/nostr/wiki.go b/internal/nostr/wiki.go index d5183a5..14d511c 100644 --- a/internal/nostr/wiki.go +++ b/internal/nostr/wiki.go @@ -76,25 +76,24 @@ func (ws *WikiService) FetchWikiIndex(ctx context.Context, naddrStr string) (*In return index, nil } -// FetchWikiEvents fetches all wiki events referenced in an index -// Uses ProcessEventsWithCache for the initial fetch, then filters by index items -func (ws *WikiService) FetchWikiEvents(ctx context.Context, index *IndexEvent) ([]*WikiEvent, error) { +// fetchIndexEventsByKind is a helper that fetches events of a specific kind from an index +// Returns a map of kind:pubkey:dtag -> event for matching +func (ws *WikiService) fetchIndexEventsByKind(ctx context.Context, index *IndexEvent, targetKind int) (map[string]*nostr.Event, error) { // Build a map of expected items (kind:pubkey:dtag) for fast lookup expectedItems := make(map[string]IndexItem) for _, item := range index.Items { - if item.Kind == ws.wikiKind { + if item.Kind == targetKind { key := fmt.Sprintf("%d:%s:%s", item.Kind, item.Pubkey, item.DTag) expectedItems[key] = item } } if len(expectedItems) == 0 { - return []*WikiEvent{}, nil + return make(map[string]*nostr.Event), nil } // Use ProcessEventsWithCache to fetch events of this kind // Use a high display limit (1000) to ensure we get all events referenced in the index - // This means we'll fetch 2000 events, which should be enough for most cases displayLimit := 1000 primaryRelay := ws.client.GetPrimaryRelay() if primaryRelay == "" { @@ -102,37 +101,21 @@ func (ws *WikiService) FetchWikiEvents(ctx context.Context, index *IndexEvent) ( } logger.WithFields(map[string]interface{}{ - "kind": ws.wikiKind, + "kind": targetKind, "items": len(expectedItems), "index_event_id": index.Event.ID, - }).Debug("Fetching wiki events using ProcessEventsWithCache with index") + }).Debug("Fetching events using ProcessEventsWithCache with index") - // Use standard process with index event ID: fetch index, query only referenced events, merge cache, deduplicate, filter deletions, sort, limit, fetch profiles - result, err := ws.client.ProcessEventsWithCache(ctx, ws.wikiKind, displayLimit, make(map[string]*nostr.Event), primaryRelay, index.Event.ID, ws.indexKind) + // Use standard process with index event ID + result, err := ws.client.ProcessEventsWithCache(ctx, targetKind, displayLimit, make(map[string]*nostr.Event), primaryRelay, index.Event.ID, ws.indexKind) if err != nil { - logger.WithField("error", err).Warn("Failed to fetch wiki events using ProcessEventsWithCache") - return nil, err + return nil, fmt.Errorf("failed to fetch events using ProcessEventsWithCache: %w", err) } - allEvents := result.Events - - logger.WithFields(map[string]interface{}{ - "fetched": len(allEvents), - "expected": len(expectedItems), - }).Debug("Fetched wiki events using ProcessEventsWithCache with index") - // Build event map by kind:pubkey:dtag for matching eventMap := make(map[string]*nostr.Event) - for _, event := range allEvents { - // Extract d-tag from event - var dTag string - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "d" && len(tag) > 1 { - dTag = tag[1] - break - } - } - + for _, event := range result.Events { + dTag := getDTag(event.Tags) if dTag == "" { continue // Skip events without d-tag } @@ -145,6 +128,24 @@ func (ws *WikiService) FetchWikiEvents(ctx context.Context, index *IndexEvent) ( } } + logger.WithFields(map[string]interface{}{ + "fetched": len(result.Events), + "matched": len(eventMap), + "expected": len(expectedItems), + "kind": targetKind, + }).Debug("Fetched and matched events from index") + + return eventMap, nil +} + +// FetchWikiEvents fetches all wiki events referenced in an index +// Uses ProcessEventsWithCache for the initial fetch, then filters by index items +func (ws *WikiService) FetchWikiEvents(ctx context.Context, index *IndexEvent) ([]*WikiEvent, error) { + eventMap, err := ws.fetchIndexEventsByKind(ctx, index, ws.wikiKind) + if err != nil { + return nil, err + } + // Convert matched events to wiki events, preserving order from index.Items var wikiEvents []*WikiEvent for _, item := range index.Items { @@ -166,11 +167,6 @@ func (ws *WikiService) FetchWikiEvents(ctx context.Context, index *IndexEvent) ( wikiEvents = append(wikiEvents, wiki) } - logger.WithFields(map[string]interface{}{ - "matched": len(wikiEvents), - "expected": len(expectedItems), - }).Debug("Matched wiki events") - if len(wikiEvents) == 0 && len(index.Items) > 0 { logger.WithField("items", len(index.Items)).Warn("No wiki events matched from fetched events") } @@ -218,72 +214,11 @@ func (ws *WikiService) FetchIndexEvents(ctx context.Context, index *IndexEvent, return nil, fmt.Errorf("unsupported event kind: %d (only %v are supported)", targetKind, ws.articleKinds) } - // Build a map of expected items (kind:pubkey:dtag) for fast lookup - expectedItems := make(map[string]IndexItem) - for _, item := range index.Items { - if item.Kind == targetKind { - key := fmt.Sprintf("%d:%s:%s", item.Kind, item.Pubkey, item.DTag) - expectedItems[key] = item - } - } - - if len(expectedItems) == 0 { - return []*nostr.Event{}, nil - } - - // Use ProcessEventsWithCache to fetch events of this kind - // Use a high display limit (1000) to ensure we get all events referenced in the index - // This means we'll fetch 2000 events, which should be enough for most cases - displayLimit := 1000 - primaryRelay := ws.client.GetPrimaryRelay() - if primaryRelay == "" { - return nil, fmt.Errorf("primary relay not configured") - } - - logger.WithFields(map[string]interface{}{ - "kind": targetKind, - "items": len(expectedItems), - "index_event_id": index.Event.ID, - }).Debug("Fetching events using ProcessEventsWithCache with index") - - // Use standard process with index event ID: fetch index, query only referenced events, merge cache, deduplicate, filter deletions, sort, limit, fetch profiles - result, err := ws.client.ProcessEventsWithCache(ctx, targetKind, displayLimit, make(map[string]*nostr.Event), primaryRelay, index.Event.ID, ws.indexKind) + eventMap, err := ws.fetchIndexEventsByKind(ctx, index, targetKind) if err != nil { - logger.WithField("error", err).Warn("Failed to fetch events using ProcessEventsWithCache") return nil, err } - allEvents := result.Events - - logger.WithFields(map[string]interface{}{ - "fetched": len(allEvents), - "expected": len(expectedItems), - }).Debug("Fetched events using ProcessEventsWithCache with index") - - // Build event map by kind:pubkey:dtag for matching - eventMap := make(map[string]*nostr.Event) - for _, event := range allEvents { - // Extract d-tag from event - var dTag string - for _, tag := range event.Tags { - if len(tag) > 0 && tag[0] == "d" && len(tag) > 1 { - dTag = tag[1] - break - } - } - - if dTag == "" { - continue // Skip events without d-tag - } - - key := fmt.Sprintf("%d:%s:%s", event.Kind, event.PubKey, dTag) - // Keep the newest version if we have multiple - existing, exists := eventMap[key] - if !exists || event.CreatedAt > existing.CreatedAt { - eventMap[key] = event - } - } - // Convert to result slice, preserving order from index.Items events := make([]*nostr.Event, 0, len(eventMap)) for _, item := range index.Items { @@ -298,12 +233,6 @@ func (ws *WikiService) FetchIndexEvents(ctx context.Context, index *IndexEvent, events = append(events, event) } - logger.WithFields(map[string]interface{}{ - "matched": len(events), - "expected": len(expectedItems), - "kind": targetKind, - }).Debug("Matched index events") - if len(events) == 0 && len(index.Items) > 0 { logger.WithFields(map[string]interface{}{ "kind": targetKind, diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 3b70a7a..828fcc9 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -90,48 +90,36 @@ func (s *Server) handleWiki(w http.ResponseWriter, r *http.Request) { s.servePage(w, r, page) } -// handleBlog handles the blog page -func (s *Server) handleBlog(w http.ResponseWriter, r *http.Request) { - page, exists := s.cache.Get("/blog") - if !exists { - http.Error(w, "Page not ready", http.StatusServiceUnavailable) - return +// handleCachedPage handles pages that are served from cache +func (s *Server) handleCachedPage(path string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + page, exists := s.cache.Get(path) + if !exists { + http.Error(w, "Page not ready", http.StatusServiceUnavailable) + return + } + s.servePage(w, r, page) } +} - s.servePage(w, r, page) +// handleBlog handles the blog page +func (s *Server) handleBlog(w http.ResponseWriter, r *http.Request) { + s.handleCachedPage("/blog")(w, r) } // handleArticles handles the articles page func (s *Server) handleArticles(w http.ResponseWriter, r *http.Request) { - page, exists := s.cache.Get("/articles") - if !exists { - http.Error(w, "Page not ready", http.StatusServiceUnavailable) - return - } - - s.servePage(w, r, page) + s.handleCachedPage("/articles")(w, r) } // handleEBooks handles the e-books listing page func (s *Server) handleEBooks(w http.ResponseWriter, r *http.Request) { - page, exists := s.cache.Get("/ebooks") - if !exists { - http.Error(w, "Page not ready", http.StatusServiceUnavailable) - return - } - - s.servePage(w, r, page) + s.handleCachedPage("/ebooks")(w, r) } // handleFeed handles the Feed page func (s *Server) handleFeed(w http.ResponseWriter, r *http.Request) { - page, exists := s.cache.Get("/feed") - if !exists { - http.Error(w, "Page not ready", http.StatusServiceUnavailable) - return - } - - s.servePage(w, r, page) + s.handleCachedPage("/feed")(w, r) } // handleContact handles the contact form (GET and POST) @@ -345,8 +333,8 @@ func (s *Server) handleContactAPI(w http.ResponseWriter, r *http.Request) { } err = relay.Publish(ctx, *req.Event) + // Note: SimplePool manages connections, but we close here for explicit cleanup relay.Close() - if err != nil { logger.WithFields(map[string]interface{}{ "relay": relayURL,