package nostr import ( "context" "fmt" "gitcitadel-online/internal/logger" "github.com/nbd-wtf/go-nostr" ) // logFilter logs the exact filter being used for debugging func logFilter(filter nostr.Filter, context string) { logger.WithFields(map[string]interface{}{ "context": context, "kinds": filter.Kinds, "authors": filter.Authors, "ids": filter.IDs, "tags": filter.Tags, "limit": filter.Limit, }).Debug("Nostr filter") } // WikiService handles wiki-specific operations type WikiService struct { client *Client articleKinds []int // Allowed article kinds (from config) wikiKind int // Primary wiki kind constant (first from wiki_kinds config) blogKind int // Primary blog kind constant (first from blog_kinds config) additionalFallback string // Additional fallback relay URL (from config) indexKind int // Index event kind (from config) } // NewWikiService creates a new wiki service func NewWikiService(client *Client, articleKinds []int, wikiKind int, additionalFallback string, indexKind int, blogKind int) *WikiService { return &WikiService{ client: client, articleKinds: articleKinds, wikiKind: wikiKind, blogKind: blogKind, additionalFallback: additionalFallback, indexKind: indexKind, } } // FetchWikiIndex fetches a wiki index by naddr func (ws *WikiService) FetchWikiIndex(ctx context.Context, naddrStr string) (*IndexEvent, error) { // Parse naddr naddr, err := ParseNaddr(naddrStr) if err != nil { return nil, fmt.Errorf("failed to parse naddr: %w", err) } // Create filter for the index event filter := naddr.ToFilter() logFilter(filter, fmt.Sprintf("wiki index (kind %d)", ws.indexKind)) // Fetch the event event, err := ws.client.FetchEvent(ctx, filter) if err != nil { return nil, fmt.Errorf("failed to fetch index event: %w", err) } // Parse the index event index, err := ParseIndexEvent(event, ws.indexKind) if err != nil { return nil, fmt.Errorf("failed to parse index event: %w", err) } return index, nil } // FetchWikiEvents fetches all wiki events referenced in an index func (ws *WikiService) FetchWikiEvents(ctx context.Context, index *IndexEvent) ([]*WikiEvent, error) { var wikiEvents []*WikiEvent for _, item := range index.Items { if item.Kind != ws.wikiKind { continue // Skip non-wiki items } // Create filter for this wiki event filter := nostr.Filter{ Kinds: []int{ws.wikiKind}, Authors: []string{item.Pubkey}, Tags: map[string][]string{ "d": {item.DTag}, }, } // If event ID is specified, use it for more reliable fetching if item.EventID != "" { filter.IDs = []string{item.EventID} logger.WithFields(map[string]interface{}{ "event_id": item.EventID[:16] + "...", "dtag": item.DTag, }).Debug("Using event ID for wiki event") } logFilter(filter, fmt.Sprintf("wiki event %s", item.DTag)) // Use relay hint if available, otherwise use default client relays var event *nostr.Event var err error if item.RelayHint != "" { // Connect to the relay hint logger.WithFields(map[string]interface{}{ "relay_hint": item.RelayHint, "dtag": item.DTag, }).Debug("Trying relay hint") relay, relayErr := ws.client.ConnectToRelay(ctx, item.RelayHint) if relayErr == nil { events, queryErr := relay.QuerySync(ctx, filter) relay.Close() if queryErr == nil { if len(events) > 0 { event = events[0] logger.WithFields(map[string]interface{}{ "dtag": item.DTag, "relay_hint": item.RelayHint, }).Debug("Successfully fetched from relay hint") } else { logger.WithFields(map[string]interface{}{ "relay_hint": item.RelayHint, "dtag": item.DTag, "has_event_id": item.EventID != "", }).Debug("Relay hint returned 0 events") } } else { logger.WithFields(map[string]interface{}{ "relay_hint": item.RelayHint, "dtag": item.DTag, }).Debugf("Error querying relay hint: %v", queryErr) } } else { logger.WithFields(map[string]interface{}{ "relay_hint": item.RelayHint, "dtag": item.DTag, }).Debugf("Error connecting to relay hint: %v", relayErr) } } // Fallback to default client relays if relay hint failed or wasn't provided // Client now queries all three relays automatically if event == nil { logger.WithField("dtag", item.DTag).Debug("Querying all relays") event, err = ws.client.FetchEvent(ctx, filter) if err != nil { // If still not found and event ID was specified, try without event ID to get latest replaceable event if item.EventID != "" { logger.WithField("dtag", item.DTag).Debug("Trying without event ID to get latest replaceable event") filterWithoutID := nostr.Filter{ Kinds: []int{ws.wikiKind}, Authors: []string{item.Pubkey}, Tags: map[string][]string{ "d": {item.DTag}, }, } logFilter(filterWithoutID, fmt.Sprintf("wiki event %s (without event ID)", item.DTag)) event, err = ws.client.FetchEvent(ctx, filterWithoutID) if err == nil { logger.WithField("dtag", item.DTag).Debug("Successfully fetched latest from all relays") } } } else { logger.WithField("dtag", item.DTag).Debug("Successfully fetched from all relays") } if event == nil { // Log error but continue with other events logger.WithFields(map[string]interface{}{ "dtag": item.DTag, "pubkey": item.Pubkey, "has_event_id": item.EventID != "", }).Warnf("Error fetching wiki event: %v", err) continue } } // Parse the wiki event wiki, err := ParseWikiEvent(event, ws.wikiKind) if err != nil { logger.WithFields(map[string]interface{}{ "dtag": item.DTag, }).Warnf("Error parsing wiki event: %v", err) continue } wikiEvents = append(wikiEvents, wiki) } if len(wikiEvents) == 0 && len(index.Items) > 0 { logger.WithField("items", len(index.Items)).Warn("No wiki events could be fetched from index items") // Return empty slice instead of error to allow landing page generation } return wikiEvents, nil } // GetBlogKind returns the blog kind configured in this service func (ws *WikiService) GetBlogKind() int { return ws.blogKind } // FetchIndexEvents fetches all events of a specific kind referenced in an index // Only supports article kinds configured in the service func (ws *WikiService) FetchIndexEvents(ctx context.Context, index *IndexEvent, targetKind int) ([]*nostr.Event, error) { // Check if the target kind is in the allowed article kinds allowed := false for _, kind := range ws.articleKinds { if kind == targetKind { allowed = true break } } if !allowed { return nil, fmt.Errorf("unsupported event kind: %d (only %v are supported)", targetKind, ws.articleKinds) } var events []*nostr.Event for _, item := range index.Items { // Only process items of the target kind if item.Kind != targetKind { continue // Skip items that don't match the target kind } // Create filter for this event filter := nostr.Filter{ Kinds: []int{targetKind}, Authors: []string{item.Pubkey}, Tags: map[string][]string{ "d": {item.DTag}, }, } // If event ID is specified, use it for more reliable fetching if item.EventID != "" { filter.IDs = []string{item.EventID} } logFilter(filter, fmt.Sprintf("index event kind %d %s", targetKind, item.DTag)) // Use relay hint if available, otherwise use default client relays var event *nostr.Event var err error if item.RelayHint != "" { // Connect to the relay hint relay, relayErr := ws.client.ConnectToRelay(ctx, item.RelayHint) if relayErr == nil { relayEvents, queryErr := relay.QuerySync(ctx, filter) relay.Close() if queryErr == nil { if len(relayEvents) > 0 { event = relayEvents[0] } else { logger.WithFields(map[string]interface{}{ "relay_hint": item.RelayHint, "dtag": item.DTag, "kind": targetKind, "has_event_id": item.EventID != "", }).Debug("Relay hint returned 0 events") } } else { logger.WithFields(map[string]interface{}{ "relay_hint": item.RelayHint, "dtag": item.DTag, "kind": targetKind, }).Debugf("Error querying relay hint: %v", queryErr) } } else { logger.WithFields(map[string]interface{}{ "relay_hint": item.RelayHint, "dtag": item.DTag, "kind": targetKind, }).Debugf("Error connecting to relay hint: %v", relayErr) } } // Fallback to default client relays if relay hint failed or wasn't provided // Client now queries all three relays automatically if event == nil { logger.WithFields(map[string]interface{}{ "dtag": item.DTag, "kind": targetKind, }).Debug("Querying all relays") event, err = ws.client.FetchEvent(ctx, filter) if err != nil { // If still not found and event ID was specified, try without event ID to get latest replaceable event if item.EventID != "" { logger.WithFields(map[string]interface{}{ "dtag": item.DTag, "kind": targetKind, }).Debug("Trying without event ID to get latest replaceable event") filterWithoutID := nostr.Filter{ Kinds: []int{targetKind}, Authors: []string{item.Pubkey}, Tags: map[string][]string{ "d": {item.DTag}, }, } logFilter(filterWithoutID, fmt.Sprintf("index event kind %d %s (without event ID)", targetKind, item.DTag)) event, err = ws.client.FetchEvent(ctx, filterWithoutID) if err == nil { logger.WithFields(map[string]interface{}{ "dtag": item.DTag, "kind": targetKind, }).Debug("Successfully fetched latest from all relays") } } } else { logger.WithFields(map[string]interface{}{ "dtag": item.DTag, "kind": targetKind, }).Debug("Successfully fetched from all relays") } if event == nil { // Log error but continue with other events logger.WithFields(map[string]interface{}{ "dtag": item.DTag, "kind": targetKind, "pubkey": item.Pubkey, "has_event_id": item.EventID != "", }).Warnf("Error fetching event: %v", err) continue } } events = append(events, event) } if len(events) == 0 && len(index.Items) > 0 { logger.WithFields(map[string]interface{}{ "kind": targetKind, "items": len(index.Items), }).Warn("No events could be fetched from index items") } return events, nil } // FetchWikiEventByDTag fetches a single wiki event by d tag func (ws *WikiService) FetchWikiEventByDTag(ctx context.Context, pubkey, dTag string) (*WikiEvent, error) { filter := nostr.Filter{ Kinds: []int{ws.wikiKind}, Authors: []string{pubkey}, Tags: map[string][]string{ "d": {dTag}, }, Limit: 1, } logFilter(filter, fmt.Sprintf("wiki by d-tag %s", dTag)) event, err := ws.client.FetchEvent(ctx, filter) if err != nil { return nil, fmt.Errorf("failed to fetch wiki event: %w", err) } return ParseWikiEvent(event, ws.wikiKind) }