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.
242 lines
6.2 KiB
242 lines
6.2 KiB
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", "<kind:pubkey:dtag>", "<relay hint>", "<event id>"] |
|
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 |
|
}
|
|
|