You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
335 lines
9.7 KiB
335 lines
9.7 KiB
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) |
|
longformKind int // Longform article kind (from 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, longformKind int) *WikiService { |
|
return &WikiService{ |
|
client: client, |
|
articleKinds: articleKinds, |
|
wikiKind: wikiKind, |
|
blogKind: blogKind, |
|
longformKind: longformKind, |
|
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 |
|
// Queries by kind only, then filters locally and batch-fetches profiles |
|
func (ws *WikiService) FetchWikiEvents(ctx context.Context, index *IndexEvent) ([]*WikiEvent, 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 { |
|
key := fmt.Sprintf("%d:%s:%s", item.Kind, item.Pubkey, item.DTag) |
|
expectedItems[key] = item |
|
} |
|
} |
|
|
|
if len(expectedItems) == 0 { |
|
return []*WikiEvent{}, nil |
|
} |
|
|
|
// Query ALL events of this kind - simple query by kind only |
|
filter := nostr.Filter{ |
|
Kinds: []int{ws.wikiKind}, |
|
Limit: 10000, // Reasonable limit |
|
} |
|
|
|
logger.WithFields(map[string]interface{}{ |
|
"kind": ws.wikiKind, |
|
"items": len(expectedItems), |
|
}).Debug("Querying all events of kind from relays") |
|
|
|
// Fetch all events of this kind |
|
allEvents, err := ws.client.FetchEvents(ctx, filter) |
|
if err != nil { |
|
logger.WithField("error", err).Warn("Failed to fetch events by kind") |
|
return nil, err |
|
} |
|
|
|
logger.WithFields(map[string]interface{}{ |
|
"fetched": len(allEvents), |
|
"expected": len(expectedItems), |
|
}).Debug("Fetched events, filtering locally") |
|
|
|
// Filter events locally by matching against index items |
|
eventMap := make(map[string]*nostr.Event) // Map by kind:pubkey:dtag |
|
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) |
|
if _, expected := expectedItems[key]; expected { |
|
// Keep the newest version if we have multiple |
|
existing, exists := eventMap[key] |
|
if !exists || event.CreatedAt > existing.CreatedAt { |
|
eventMap[key] = event |
|
} |
|
} |
|
} |
|
|
|
// Convert matched events to wiki events |
|
var wikiEvents []*WikiEvent |
|
for key, event := range eventMap { |
|
wiki, err := ParseWikiEvent(event, ws.wikiKind) |
|
if err != nil { |
|
logger.WithFields(map[string]interface{}{ |
|
"key": key, |
|
}).Warnf("Error parsing wiki event: %v", err) |
|
continue |
|
} |
|
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") |
|
} |
|
|
|
return wikiEvents, nil |
|
} |
|
|
|
// GetBlogKind returns the blog kind configured in this service |
|
func (ws *WikiService) GetBlogKind() int { |
|
return ws.blogKind |
|
} |
|
|
|
// GetLongformKind returns the longform kind configured in this service |
|
func (ws *WikiService) GetLongformKind() int { |
|
return ws.longformKind |
|
} |
|
|
|
// FetchLongformArticles fetches the newest longform articles (kind 30023) from a specific relay |
|
// Queries by kind only, sorted by newest first, limit 1000 |
|
func (ws *WikiService) FetchLongformArticles(ctx context.Context, relayURL string, longformKind int, limit int) ([]*nostr.Event, error) { |
|
// Connect to the specific relay |
|
relay, err := ws.client.ConnectToRelay(ctx, relayURL) |
|
if err != nil { |
|
return nil, fmt.Errorf("failed to connect to relay %s: %w", relayURL, err) |
|
} |
|
defer relay.Close() |
|
|
|
// Query ALL events of this kind, sorted by newest first |
|
filter := nostr.Filter{ |
|
Kinds: []int{longformKind}, |
|
Limit: limit, |
|
} |
|
|
|
logFilter(filter, fmt.Sprintf("longform articles (kind %d) from %s", longformKind, relayURL)) |
|
|
|
events, err := relay.QuerySync(ctx, filter) |
|
if err != nil { |
|
return nil, fmt.Errorf("failed to query events: %w", err) |
|
} |
|
|
|
// Sort by created_at descending (newest first) |
|
// Note: go-nostr may already return sorted, but we'll ensure it |
|
for i := 0; i < len(events)-1; i++ { |
|
for j := i + 1; j < len(events); j++ { |
|
if events[i].CreatedAt < events[j].CreatedAt { |
|
events[i], events[j] = events[j], events[i] |
|
} |
|
} |
|
} |
|
|
|
logger.WithFields(map[string]interface{}{ |
|
"events": len(events), |
|
"relay": relayURL, |
|
"kind": longformKind, |
|
}).Debug("Fetched longform articles") |
|
|
|
return events, nil |
|
} |
|
|
|
// FetchIndexEvents fetches all events of a specific kind referenced in an index |
|
// Only supports article kinds configured in the service |
|
// Queries by kind only, then filters locally |
|
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) |
|
} |
|
|
|
// 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 |
|
} |
|
|
|
// Query ALL events of this kind - simple query by kind only |
|
filter := nostr.Filter{ |
|
Kinds: []int{targetKind}, |
|
Limit: 10000, // Reasonable limit |
|
} |
|
|
|
logger.WithFields(map[string]interface{}{ |
|
"kind": targetKind, |
|
"items": len(expectedItems), |
|
}).Debug("Querying all events of kind from relays") |
|
|
|
// Fetch all events of this kind |
|
allEvents, err := ws.client.FetchEvents(ctx, filter) |
|
if err != nil { |
|
logger.WithField("error", err).Warn("Failed to fetch events by kind") |
|
return nil, err |
|
} |
|
|
|
logger.WithFields(map[string]interface{}{ |
|
"fetched": len(allEvents), |
|
"expected": len(expectedItems), |
|
}).Debug("Fetched events, filtering locally") |
|
|
|
// Filter events locally by matching against index items |
|
eventMap := make(map[string]*nostr.Event) // Map by kind:pubkey:dtag |
|
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) |
|
if _, expected := expectedItems[key]; expected { |
|
// 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 |
|
events := make([]*nostr.Event, 0, len(eventMap)) |
|
for _, event := range eventMap { |
|
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, |
|
"items": len(index.Items), |
|
}).Warn("No events matched from fetched events") |
|
} |
|
|
|
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) |
|
}
|
|
|