package nostr import ( "fmt" "strings" "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 Title string DTag string Author string Items []IndexItem AutoUpdate string Type string Version string Summary string Image string } // IndexItem represents an item in a publication index (from 'a' tags) type IndexItem struct { Kind int Pubkey string DTag string RelayHint string EventID string // Optional event ID for version tracking } // ParseIndexEvent parses an index event according to NKBIP-01 func ParseIndexEvent(event *nostr.Event, expectedKind int) (*IndexEvent, error) { if event.Kind != expectedKind { return nil, fmt.Errorf("expected kind %d, got %d", expectedKind, event.Kind) } index := &IndexEvent{ 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) for _, tag := range event.Tags { if len(tag) > 0 && tag[0] == "a" && len(tag) > 1 { // Format: ["a", "", "", ""] ref := tag[1] parts := strings.Split(ref, ":") if len(parts) >= 3 { var kind int fmt.Sscanf(parts[0], "%d", &kind) item := IndexItem{ Kind: kind, Pubkey: parts[1], DTag: parts[2], RelayHint: "", EventID: "", } if len(tag) > 2 { item.RelayHint = tag[2] } if len(tag) > 3 { item.EventID = tag[3] } index.Items = append(index.Items, item) } } } return index, nil } // WikiEvent represents a kind 30818 wiki event (NIP-54) type WikiEvent struct { *ContentEvent } // ParseWikiEvent parses a wiki event according to NIP-54 func ParseWikiEvent(event *nostr.Event, expectedKind int) (*WikiEvent, error) { ce, err := parseContentEvent(event, expectedKind) if err != nil { return nil, err } return &WikiEvent{ContentEvent: ce}, nil } // BlogEvent represents a kind 30041 blog event type BlogEvent struct { *ContentEvent } // ParseBlogEvent parses a blog event func ParseBlogEvent(event *nostr.Event, expectedKind int) (*BlogEvent, error) { ce, err := parseContentEvent(event, expectedKind) if err != nil { return nil, err } return &BlogEvent{ContentEvent: ce}, nil } // LongformEvent represents a kind 30023 longform article event type LongformEvent struct { *ContentEvent } // ParseLongformEvent parses a longform article event func ParseLongformEvent(event *nostr.Event, expectedKind int) (*LongformEvent, error) { ce, err := parseContentEvent(event, expectedKind) if err != nil { return nil, err } return &LongformEvent{ContentEvent: ce}, nil } // NormalizeDTag normalizes a d tag according to NIP-54 rules func NormalizeDTag(dTag string) string { // Convert to lowercase dTag = strings.ToLower(dTag) // Convert whitespace to hyphens dTag = strings.ReplaceAll(dTag, " ", "-") dTag = strings.ReplaceAll(dTag, "\t", "-") dTag = strings.ReplaceAll(dTag, "\n", "-") // Remove punctuation and symbols (keep alphanumeric, hyphens, and non-ASCII) var result strings.Builder for _, r := range dTag { if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r > 127 { result.WriteRune(r) } } dTag = result.String() // Collapse multiple consecutive hyphens for strings.Contains(dTag, "--") { dTag = strings.ReplaceAll(dTag, "--", "-") } // Remove leading and trailing hyphens dTag = strings.Trim(dTag, "-") return dTag }