package nostr import ( "context" "fmt" "gitcitadel-online/internal/logger" "github.com/nbd-wtf/go-nostr" ) // EBooksService handles fetching top-level 30040 events type EBooksService struct { client *Client indexKind int relayURL string } // NewEBooksService creates a new e-books service func NewEBooksService(client *Client, indexKind int, relayURL string) *EBooksService { return &EBooksService{ client: client, indexKind: indexKind, relayURL: relayURL, } } // EBookInfo represents information about a top-level index event type EBookInfo struct { EventID string Title string DTag string Author string Summary string Type string CreatedAt int64 Naddr string Image string } // FetchTopLevelIndexEvents fetches all top-level 30040 events from the specified relay // Top-level means the event is not referenced in any other 30040 event's 'a' tags // Uses the standard ProcessEventsWithCache process, then filters for top-level events func (es *EBooksService) FetchTopLevelIndexEvents(ctx context.Context) ([]EBookInfo, error) { // For e-books, we want to fetch a large number to ensure we get all top-level events // Use a high display limit (1000) to get all events, then filter for top-level displayLimit := 1000 // Use standard process: fetch 2x limit, merge cache, deduplicate, filter deletions, sort, limit, fetch profiles // For e-books, use a high display limit (1000) to get all events, then filter for top-level // This means we'll fetch 2000 events, which should be enough for most cases result, err := es.client.ProcessEventsWithCache(ctx, es.indexKind, displayLimit, make(map[string]*nostr.Event), es.relayURL, "", 0) if err != nil { return nil, fmt.Errorf("failed to process index events: %w", err) } events := result.Events logger.WithFields(map[string]interface{}{ "events": len(events), "relay": es.relayURL, }).Debug("Processed index events using standard process") // Build a set of all referenced kind:pubkey:dtag from 'a' tags referencedSet := make(map[string]bool) for _, event := range events { for _, tag := range event.Tags { if len(tag) > 0 && tag[0] == "a" && len(tag) > 1 { // tag[1] is the kind:pubkey:dtag reference referencedSet[tag[1]] = true } } } logger.WithField("count", len(referencedSet)).Debug("Found referenced index events") // Filter to only top-level events (not referenced in any other event) var topLevelEvents []*nostr.Event for _, event := range events { // Build the kind:pubkey:dtag identifier for this 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 } // Check if this event is referenced by any other event eventRef := fmt.Sprintf("%d:%s:%s", es.indexKind, event.PubKey, dTag) if !referencedSet[eventRef] { topLevelEvents = append(topLevelEvents, event) } } logger.WithField("count", len(topLevelEvents)).Info("Found top-level index events") // Convert to EBookInfo ebooks := make([]EBookInfo, 0, len(topLevelEvents)) for _, event := range topLevelEvents { ebook := EBookInfo{ EventID: event.ID, Author: event.PubKey, 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 } } if ebook.Title == "" { ebook.Title = ebook.DTag // Fallback to d tag } // 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:]...) } } // If no relays in tags, use the relay we fetched from if len(relays) == 0 { relays = []string{es.relayURL} } naddr, err := CreateNaddr(es.indexKind, ebook.Author, ebook.DTag, relays) if err != nil { logger.WithFields(map[string]interface{}{ "ebook": ebook.DTag, "error": err, }).Warn("Failed to create naddr for ebook, using fallback format") // Fallback to kind:pubkey:dtag format ebook.Naddr = fmt.Sprintf("%d:%s:%s", es.indexKind, ebook.Author, ebook.DTag) } else { ebook.Naddr = naddr } ebooks = append(ebooks, ebook) } return ebooks, nil }