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

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
}