Browse Source

refactor

master
Silberengel 4 weeks ago
parent
commit
07dbb55b55
  1. 28
      internal/config/config.go
  2. 26
      internal/nostr/client.go
  3. 53
      internal/nostr/ebooks.go
  4. 325
      internal/nostr/events.go
  5. 23
      internal/nostr/feed.go
  6. 27
      internal/nostr/issues.go
  7. 133
      internal/nostr/wiki.go
  8. 46
      internal/server/handlers.go

28
internal/config/config.go

@ -96,12 +96,12 @@ func LoadConfig(path string) (*Config, error) {
return &config, nil return &config, nil
} }
// GetProfilesRelays parses the comma-separated profiles relay string into a slice // parseRelayList parses a comma-separated relay string into a slice
func (c *Config) GetProfilesRelays() []string { func parseRelayList(relayStr string) []string {
if c.Relays.Profiles == "" { if relayStr == "" {
return []string{} return []string{}
} }
relays := strings.Split(c.Relays.Profiles, ",") relays := strings.Split(relayStr, ",")
result := make([]string, 0, len(relays)) result := make([]string, 0, len(relays))
for _, relay := range relays { for _, relay := range relays {
relay = strings.TrimSpace(relay) relay = strings.TrimSpace(relay)
@ -114,22 +114,14 @@ func (c *Config) GetProfilesRelays() []string {
return result 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 // GetContactFormRelays parses the comma-separated contact form relay string into a slice
func (c *Config) GetContactFormRelays() []string { func (c *Config) GetContactFormRelays() []string {
if c.Relays.ContactForm == "" { return parseRelayList(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
} }
// Validate validates the configuration // Validate validates the configuration

26
internal/nostr/client.go

@ -3,6 +3,7 @@ package nostr
import ( import (
"context" "context"
"fmt" "fmt"
"sort"
"time" "time"
"gitcitadel-online/internal/logger" "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 // 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) deletedEventIDs := make(map[string]*nostr.Event)
for _, deletionEvent := range deletionEvents { for _, deletionEvent := range deletionEvents {
// Kind 5 events have "e" tags with the event IDs they're deleting // Kind 5 events have "e" tags with the event IDs they're deleting
for _, tag := range deletionEvent.Tags { for _, eventID := range getETagValues(deletionEvent.Tags) {
if len(tag) > 0 && tag[0] == "e" && len(tag) > 1 { // Keep the newest deletion event if multiple deletions exist
eventID := tag[1] existing, exists := deletedEventIDs[eventID]
// Keep the newest deletion event if multiple deletions exist if !exists || deletionEvent.CreatedAt > existing.CreatedAt {
existing, exists := deletedEventIDs[eventID] deletedEventIDs[eventID] = deletionEvent
if !exists || deletionEvent.CreatedAt > existing.CreatedAt {
deletedEventIDs[eventID] = deletionEvent
}
} }
} }
} }
@ -558,13 +558,9 @@ func (c *Client) ProcessEventsWithCache(
allEvents = FilterDeletedEvents(allEvents, deletedEventIDs) allEvents = FilterDeletedEvents(allEvents, deletedEventIDs)
// Step 6: Sort newest-first (by created_at descending) // Step 6: Sort newest-first (by created_at descending)
for i := 0; i < len(allEvents)-1; i++ { sort.Slice(allEvents, func(i, j int) bool {
for j := i + 1; j < len(allEvents); j++ { return allEvents[i].CreatedAt > allEvents[j].CreatedAt
if allEvents[i].CreatedAt < allEvents[j].CreatedAt { })
allEvents[i], allEvents[j] = allEvents[j], allEvents[i]
}
}
}
logger.WithFields(map[string]interface{}{ logger.WithFields(map[string]interface{}{
"after_deletion": len(allEvents), "after_deletion": len(allEvents),

53
internal/nostr/ebooks.go

@ -108,57 +108,18 @@ func (es *EBooksService) FetchTopLevelIndexEvents(ctx context.Context) ([]EBookI
CreatedAt: int64(event.CreatedAt), CreatedAt: int64(event.CreatedAt),
} }
// Extract d tag // Extract common tags using utilities
for _, tag := range event.Tags { ebook.DTag = getDTag(event.Tags)
if len(tag) > 0 && tag[0] == "d" && len(tag) > 1 { ebook.Title = getTitle(event.Tags)
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
}
}
if ebook.Title == "" { if ebook.Title == "" {
ebook.Title = ebook.DTag // Fallback to d tag 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 // Extract relay hints from event tags if available
var relays []string relays := getAllTagValues(event.Tags, "relays")
for _, tag := range event.Tags {
if len(tag) > 0 && tag[0] == "relays" {
relays = append(relays, tag[1:]...)
}
}
// If no relays in tags, use the relay we fetched from // If no relays in tags, use the relay we fetched from
if len(relays) == 0 { if len(relays) == 0 {
relays = []string{es.relayURL} relays = []string{es.relayURL}

325
internal/nostr/events.go

@ -7,6 +7,97 @@ import (
"github.com/nbd-wtf/go-nostr" "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) // IndexEvent represents a kind 30040 publication index event (NKBIP-01)
type IndexEvent struct { type IndexEvent struct {
Event *nostr.Event Event *nostr.Event
@ -37,26 +128,15 @@ func ParseIndexEvent(event *nostr.Event, expectedKind int) (*IndexEvent, error)
} }
index := &IndexEvent{ index := &IndexEvent{
Event: event, Event: event,
Author: event.PubKey, Author: event.PubKey,
} DTag: getDTag(event.Tags),
Title: getTitle(event.Tags),
// Extract d tag Summary: getSummary(event.Tags),
var dTag string Image: getImage(event.Tags),
for _, tag := range event.Tags { Type: getTagValue(event.Tags, "type"),
if len(tag) > 0 && tag[0] == "d" && len(tag) > 1 { Version: getTagValue(event.Tags, "version"),
dTag = tag[1] AutoUpdate: getTagValue(event.Tags, "auto-update"),
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
}
} }
// Extract 'a' tags (index items) // 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 return index, nil
} }
// WikiEvent represents a kind 30818 wiki event (NIP-54) // WikiEvent represents a kind 30818 wiki event (NIP-54)
type WikiEvent struct { type WikiEvent struct {
Event *nostr.Event *ContentEvent
DTag string
Title string
Summary string
Content string
Image string
} }
// ParseWikiEvent parses a wiki event according to NIP-54 // ParseWikiEvent parses a wiki event according to NIP-54
func ParseWikiEvent(event *nostr.Event, expectedKind int) (*WikiEvent, error) { func ParseWikiEvent(event *nostr.Event, expectedKind int) (*WikiEvent, error) {
if event.Kind != expectedKind { ce, err := parseContentEvent(event, expectedKind)
return nil, fmt.Errorf("expected kind %d, got %d", expectedKind, event.Kind) if err != nil {
return nil, err
} }
return &WikiEvent{ContentEvent: ce}, nil
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
} }
// BlogEvent represents a kind 30041 blog event // BlogEvent represents a kind 30041 blog event
type BlogEvent struct { type BlogEvent struct {
Event *nostr.Event *ContentEvent
DTag string
Title string
Summary string
Content string
Image string
} }
// ParseBlogEvent parses a blog event // ParseBlogEvent parses a blog event
func ParseBlogEvent(event *nostr.Event, expectedKind int) (*BlogEvent, error) { func ParseBlogEvent(event *nostr.Event, expectedKind int) (*BlogEvent, error) {
if event.Kind != expectedKind { ce, err := parseContentEvent(event, expectedKind)
return nil, fmt.Errorf("expected kind %d, got %d", expectedKind, event.Kind) if err != nil {
} return nil, err
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
} }
return &BlogEvent{ContentEvent: ce}, nil
// 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
} }
// LongformEvent represents a kind 30023 longform article event // LongformEvent represents a kind 30023 longform article event
type LongformEvent struct { type LongformEvent struct {
Event *nostr.Event *ContentEvent
DTag string
Title string
Summary string
Content string
Image string
} }
// ParseLongformEvent parses a longform article event // ParseLongformEvent parses a longform article event
func ParseLongformEvent(event *nostr.Event, expectedKind int) (*LongformEvent, error) { func ParseLongformEvent(event *nostr.Event, expectedKind int) (*LongformEvent, error) {
if event.Kind != expectedKind { ce, err := parseContentEvent(event, expectedKind)
return nil, fmt.Errorf("expected kind %d, got %d", expectedKind, event.Kind) if err != nil {
return nil, err
} }
return &LongformEvent{ContentEvent: ce}, nil
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
} }
// NormalizeDTag normalizes a d tag according to NIP-54 rules // NormalizeDTag normalizes a d tag according to NIP-54 rules

23
internal/nostr/feed.go

@ -40,29 +40,20 @@ func (fs *FeedService) FetchFeedItems(ctx context.Context, feedRelay string, max
// Convert events to feed items // Convert events to feed items
items := make([]FeedItem, 0, len(result.Events)) items := make([]FeedItem, 0, len(result.Events))
for _, event := range result.Events { for _, event := range result.Events {
item := FeedItem{ item := FeedItem{
EventID: event.ID, EventID: event.ID,
Author: event.PubKey, Author: event.PubKey,
Content: event.Content, Content: event.Content,
Time: time.Unix(int64(event.CreatedAt), 0), Time: time.Unix(int64(event.CreatedAt), 0),
Link: fmt.Sprintf("https://alexandria.gitcitadel.eu/events?id=nevent1%s", event.ID), Link: fmt.Sprintf("https://alexandria.gitcitadel.eu/events?id=nevent1%s", event.ID),
} }
// Extract title, summary, and image tags // Extract title, summary, and image tags
for _, tag := range event.Tags { item.Title = getTitle(event.Tags)
if len(tag) > 0 && len(tag) > 1 { item.Summary = getSummary(event.Tags)
switch tag[0] { item.Image = getImage(event.Tags)
case "title":
item.Title = tag[1]
case "summary":
item.Summary = tag[1]
case "image":
item.Image = tag[1]
}
}
}
items = append(items, item) items = append(items, item)
} }
logger.WithFields(map[string]interface{}{ logger.WithFields(map[string]interface{}{

27
internal/nostr/issues.go

@ -51,31 +51,16 @@ func ParseRepoAnnouncement(event *nostr.Event, expectedKind int) (*RepoAnnouncem
} }
// Extract d tag // Extract d tag
for _, tag := range event.Tags { repo.DTag = getDTag(event.Tags)
if len(tag) > 0 && tag[0] == "d" && len(tag) > 1 {
repo.DTag = tag[1]
break
}
}
// Validate that DTag is set // Validate that DTag is set
if repo.DTag == "" { if repo.DTag == "" {
return nil, fmt.Errorf("repository announcement event missing d tag") return nil, fmt.Errorf("repository announcement event missing d tag")
} }
// Extract relays tag // Extract relays and maintainers tags
for _, tag := range event.Tags { repo.Relays = getAllTagValues(event.Tags, "relays")
if len(tag) > 0 && tag[0] == "relays" && len(tag) > 1 { repo.Maintainers = getAllTagValues(event.Tags, "maintainers")
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:]...)
}
}
return repo, nil return repo, nil
} }
@ -195,6 +180,8 @@ func (s *IssueService) PublishIssue(ctx context.Context, repoAnnouncement *RepoA
} }
err = relay.Publish(ctx, *event) 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() relay.Close()
if err != nil { if err != nil {
lastErr = err lastErr = err
@ -253,6 +240,8 @@ func (s *IssueService) PublishSignedIssue(ctx context.Context, signedEvent *nost
} }
err = relay.Publish(ctx, *signedEvent) 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() relay.Close()
if err != nil { if err != nil {
lastErr = err lastErr = err

133
internal/nostr/wiki.go

@ -76,25 +76,24 @@ func (ws *WikiService) FetchWikiIndex(ctx context.Context, naddrStr string) (*In
return index, nil return index, nil
} }
// FetchWikiEvents fetches all wiki events referenced in an index // fetchIndexEventsByKind is a helper that fetches events of a specific kind from an index
// Uses ProcessEventsWithCache for the initial fetch, then filters by index items // Returns a map of kind:pubkey:dtag -> event for matching
func (ws *WikiService) FetchWikiEvents(ctx context.Context, index *IndexEvent) ([]*WikiEvent, error) { 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 // Build a map of expected items (kind:pubkey:dtag) for fast lookup
expectedItems := make(map[string]IndexItem) expectedItems := make(map[string]IndexItem)
for _, item := range index.Items { 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) key := fmt.Sprintf("%d:%s:%s", item.Kind, item.Pubkey, item.DTag)
expectedItems[key] = item expectedItems[key] = item
} }
} }
if len(expectedItems) == 0 { if len(expectedItems) == 0 {
return []*WikiEvent{}, nil return make(map[string]*nostr.Event), nil
} }
// Use ProcessEventsWithCache to fetch events of this kind // Use ProcessEventsWithCache to fetch events of this kind
// Use a high display limit (1000) to ensure we get all events referenced in the index // 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 displayLimit := 1000
primaryRelay := ws.client.GetPrimaryRelay() primaryRelay := ws.client.GetPrimaryRelay()
if primaryRelay == "" { if primaryRelay == "" {
@ -102,37 +101,21 @@ func (ws *WikiService) FetchWikiEvents(ctx context.Context, index *IndexEvent) (
} }
logger.WithFields(map[string]interface{}{ logger.WithFields(map[string]interface{}{
"kind": ws.wikiKind, "kind": targetKind,
"items": len(expectedItems), "items": len(expectedItems),
"index_event_id": index.Event.ID, "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 // Use standard process with index event ID
result, err := ws.client.ProcessEventsWithCache(ctx, ws.wikiKind, displayLimit, make(map[string]*nostr.Event), primaryRelay, index.Event.ID, ws.indexKind) result, err := ws.client.ProcessEventsWithCache(ctx, targetKind, displayLimit, make(map[string]*nostr.Event), primaryRelay, index.Event.ID, ws.indexKind)
if err != nil { if err != nil {
logger.WithField("error", err).Warn("Failed to fetch wiki events using ProcessEventsWithCache") return nil, fmt.Errorf("failed to fetch events using ProcessEventsWithCache: %w", err)
return nil, 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 // Build event map by kind:pubkey:dtag for matching
eventMap := make(map[string]*nostr.Event) eventMap := make(map[string]*nostr.Event)
for _, event := range allEvents { for _, event := range result.Events {
// Extract d-tag from event dTag := getDTag(event.Tags)
var dTag string
for _, tag := range event.Tags {
if len(tag) > 0 && tag[0] == "d" && len(tag) > 1 {
dTag = tag[1]
break
}
}
if dTag == "" { if dTag == "" {
continue // Skip events without d-tag 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 // Convert matched events to wiki events, preserving order from index.Items
var wikiEvents []*WikiEvent var wikiEvents []*WikiEvent
for _, item := range index.Items { for _, item := range index.Items {
@ -166,11 +167,6 @@ func (ws *WikiService) FetchWikiEvents(ctx context.Context, index *IndexEvent) (
wikiEvents = append(wikiEvents, wiki) 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 { if len(wikiEvents) == 0 && len(index.Items) > 0 {
logger.WithField("items", len(index.Items)).Warn("No wiki events matched from fetched events") 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) 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 eventMap, err := ws.fetchIndexEventsByKind(ctx, index, targetKind)
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)
if err != nil { if err != nil {
logger.WithField("error", err).Warn("Failed to fetch events using ProcessEventsWithCache")
return nil, err 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 // Convert to result slice, preserving order from index.Items
events := make([]*nostr.Event, 0, len(eventMap)) events := make([]*nostr.Event, 0, len(eventMap))
for _, item := range index.Items { for _, item := range index.Items {
@ -298,12 +233,6 @@ func (ws *WikiService) FetchIndexEvents(ctx context.Context, index *IndexEvent,
events = append(events, event) 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 { if len(events) == 0 && len(index.Items) > 0 {
logger.WithFields(map[string]interface{}{ logger.WithFields(map[string]interface{}{
"kind": targetKind, "kind": targetKind,

46
internal/server/handlers.go

@ -90,48 +90,36 @@ func (s *Server) handleWiki(w http.ResponseWriter, r *http.Request) {
s.servePage(w, r, page) s.servePage(w, r, page)
} }
// handleBlog handles the blog page // handleCachedPage handles pages that are served from cache
func (s *Server) handleBlog(w http.ResponseWriter, r *http.Request) { func (s *Server) handleCachedPage(path string) http.HandlerFunc {
page, exists := s.cache.Get("/blog") return func(w http.ResponseWriter, r *http.Request) {
if !exists { page, exists := s.cache.Get(path)
http.Error(w, "Page not ready", http.StatusServiceUnavailable) if !exists {
return 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 // handleArticles handles the articles page
func (s *Server) handleArticles(w http.ResponseWriter, r *http.Request) { func (s *Server) handleArticles(w http.ResponseWriter, r *http.Request) {
page, exists := s.cache.Get("/articles") s.handleCachedPage("/articles")(w, r)
if !exists {
http.Error(w, "Page not ready", http.StatusServiceUnavailable)
return
}
s.servePage(w, r, page)
} }
// handleEBooks handles the e-books listing page // handleEBooks handles the e-books listing page
func (s *Server) handleEBooks(w http.ResponseWriter, r *http.Request) { func (s *Server) handleEBooks(w http.ResponseWriter, r *http.Request) {
page, exists := s.cache.Get("/ebooks") s.handleCachedPage("/ebooks")(w, r)
if !exists {
http.Error(w, "Page not ready", http.StatusServiceUnavailable)
return
}
s.servePage(w, r, page)
} }
// handleFeed handles the Feed page // handleFeed handles the Feed page
func (s *Server) handleFeed(w http.ResponseWriter, r *http.Request) { func (s *Server) handleFeed(w http.ResponseWriter, r *http.Request) {
page, exists := s.cache.Get("/feed") s.handleCachedPage("/feed")(w, r)
if !exists {
http.Error(w, "Page not ready", http.StatusServiceUnavailable)
return
}
s.servePage(w, r, page)
} }
// handleContact handles the contact form (GET and POST) // 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) err = relay.Publish(ctx, *req.Event)
// Note: SimplePool manages connections, but we close here for explicit cleanup
relay.Close() relay.Close()
if err != nil { if err != nil {
logger.WithFields(map[string]interface{}{ logger.WithFields(map[string]interface{}{
"relay": relayURL, "relay": relayURL,

Loading…
Cancel
Save