package nostr import ( "fmt" "strings" "github.com/nbd-wtf/go-nostr" ) // 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, } // Extract d tag var dTag string for _, tag := range event.Tags { if len(tag) > 0 && tag[0] == "d" && len(tag) > 1 { dTag = tag[1] 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) 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) } } } // 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 } // WikiEvent represents a kind 30818 wiki event (NIP-54) type WikiEvent struct { Event *nostr.Event DTag string Title string Summary string Content string } // ParseWikiEvent parses a wiki event according to NIP-54 func ParseWikiEvent(event *nostr.Event, expectedKind int) (*WikiEvent, error) { if event.Kind != expectedKind { return nil, fmt.Errorf("expected kind %d, got %d", expectedKind, event.Kind) } 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 } } return wiki, nil } // BlogEvent represents a kind 30041 blog event type BlogEvent struct { Event *nostr.Event DTag string Title string Summary string Content string } // ParseBlogEvent parses a blog event func ParseBlogEvent(event *nostr.Event, expectedKind int) (*BlogEvent, error) { if event.Kind != expectedKind { return nil, fmt.Errorf("expected kind %d, got %d", expectedKind, event.Kind) } 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 } // Extract summary tag (optional) for _, tag := range event.Tags { if len(tag) > 0 && tag[0] == "summary" && len(tag) > 1 { blog.Summary = tag[1] break } } return blog, 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 }