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.
201 lines
5.1 KiB
201 lines
5.1 KiB
package nostr |
|
|
|
import ( |
|
"context" |
|
"encoding/json" |
|
"fmt" |
|
|
|
"gitcitadel-online/internal/logger" |
|
|
|
"github.com/nbd-wtf/go-nostr" |
|
"github.com/nbd-wtf/go-nostr/nip19" |
|
) |
|
|
|
// ShortenNpub shortens an npub to npub1...xyz format (first 8 + last 4 characters) |
|
func ShortenNpub(npub string) string { |
|
if len(npub) <= 12 { |
|
return npub |
|
} |
|
return npub[:8] + "..." + npub[len(npub)-4:] |
|
} |
|
|
|
// PubkeyToNpub converts a hex pubkey to npub format |
|
func PubkeyToNpub(pubkey string) (string, error) { |
|
npub, err := nip19.EncodePublicKey(pubkey) |
|
if err != nil { |
|
return "", fmt.Errorf("failed to encode pubkey to npub: %w", err) |
|
} |
|
return npub, nil |
|
} |
|
|
|
// ShortenPubkey converts a pubkey to shortened npub format |
|
func ShortenPubkey(pubkey string) string { |
|
npub, err := PubkeyToNpub(pubkey) |
|
if err != nil { |
|
// Fallback: shorten hex pubkey |
|
if len(pubkey) > 12 { |
|
return pubkey[:8] + "..." + pubkey[len(pubkey)-4:] |
|
} |
|
return pubkey |
|
} |
|
return ShortenNpub(npub) |
|
} |
|
|
|
// Profile represents a parsed kind 0 profile event |
|
type Profile struct { |
|
Event *nostr.Event |
|
Pubkey string |
|
Name string |
|
DisplayName string |
|
About string |
|
Picture string |
|
Website string |
|
NIP05 string |
|
Lud16 string |
|
Banner string |
|
RawJSON map[string]interface{} // Store all other fields |
|
} |
|
|
|
// FetchProfile fetches a kind 0 profile event from an npub |
|
func (c *Client) FetchProfile(ctx context.Context, npub string) (*Profile, error) { |
|
// Decode npub to get pubkey |
|
prefix, value, err := nip19.Decode(npub) |
|
if err != nil { |
|
return nil, fmt.Errorf("failed to decode npub: %w", err) |
|
} |
|
|
|
if prefix != "npub" { |
|
return nil, fmt.Errorf("invalid npub prefix: expected 'npub', got '%s'", prefix) |
|
} |
|
|
|
pubkey, ok := value.(string) |
|
if !ok { |
|
return nil, fmt.Errorf("failed to parse npub: unexpected type") |
|
} |
|
|
|
profiles, err := c.FetchProfilesBatch(ctx, []string{pubkey}) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
profile, exists := profiles[pubkey] |
|
if !exists { |
|
return nil, fmt.Errorf("profile not found for pubkey") |
|
} |
|
|
|
return profile, nil |
|
} |
|
|
|
// FetchProfilesBatch fetches kind 0 profile events for multiple pubkeys in a single query |
|
// Returns a map of pubkey -> Profile |
|
func (c *Client) FetchProfilesBatch(ctx context.Context, pubkeys []string) (map[string]*Profile, error) { |
|
if len(pubkeys) == 0 { |
|
return make(map[string]*Profile), nil |
|
} |
|
|
|
// Deduplicate pubkeys |
|
pubkeySet := make(map[string]bool) |
|
uniquePubkeys := make([]string, 0, len(pubkeys)) |
|
for _, pk := range pubkeys { |
|
if pk != "" && !pubkeySet[pk] { |
|
pubkeySet[pk] = true |
|
uniquePubkeys = append(uniquePubkeys, pk) |
|
} |
|
} |
|
|
|
if len(uniquePubkeys) == 0 { |
|
return make(map[string]*Profile), nil |
|
} |
|
|
|
// Query ALL kind 0 events for these authors in one batch |
|
filter := nostr.Filter{ |
|
Kinds: []int{KindProfile}, |
|
Authors: uniquePubkeys, |
|
Limit: len(uniquePubkeys), // One profile per author |
|
} |
|
|
|
logger.WithFields(map[string]interface{}{ |
|
"pubkeys": len(uniquePubkeys), |
|
}).Debug("Batch fetching profiles") |
|
|
|
// Fetch all profile events |
|
events, err := c.FetchEvents(ctx, filter) |
|
if err != nil { |
|
return nil, fmt.Errorf("failed to fetch profile events: %w", err) |
|
} |
|
|
|
// Parse profiles and map by pubkey |
|
profiles := make(map[string]*Profile) |
|
for _, event := range events { |
|
profile, err := ParseProfile(event) |
|
if err != nil { |
|
logger.WithFields(map[string]interface{}{ |
|
"pubkey": event.PubKey, |
|
"event_id": event.ID, |
|
}).Warnf("Error parsing profile: %v", err) |
|
continue |
|
} |
|
// Keep the newest profile if we have multiple for the same pubkey |
|
existing, exists := profiles[event.PubKey] |
|
if !exists || event.CreatedAt > existing.Event.CreatedAt { |
|
profiles[event.PubKey] = profile |
|
} |
|
} |
|
|
|
logger.WithFields(map[string]interface{}{ |
|
"requested": len(uniquePubkeys), |
|
"fetched": len(profiles), |
|
}).Debug("Batch profile fetch completed") |
|
|
|
return profiles, nil |
|
} |
|
|
|
// ParseProfile parses a kind 0 profile event |
|
func ParseProfile(event *nostr.Event) (*Profile, error) { |
|
if event.Kind != KindProfile { |
|
return nil, fmt.Errorf("expected kind %d, got %d", KindProfile, event.Kind) |
|
} |
|
|
|
profile := &Profile{ |
|
Event: event, |
|
Pubkey: event.PubKey, |
|
RawJSON: make(map[string]interface{}), |
|
} |
|
|
|
// Parse JSON content |
|
var content map[string]interface{} |
|
if err := json.Unmarshal([]byte(event.Content), &content); err != nil { |
|
return nil, fmt.Errorf("failed to parse profile JSON: %w", err) |
|
} |
|
|
|
// Extract common fields |
|
if name, ok := content["name"].(string); ok { |
|
profile.Name = name |
|
} |
|
if displayName, ok := content["display_name"].(string); ok { |
|
profile.DisplayName = displayName |
|
} |
|
if about, ok := content["about"].(string); ok { |
|
profile.About = about |
|
} |
|
if picture, ok := content["picture"].(string); ok { |
|
profile.Picture = picture |
|
} |
|
if website, ok := content["website"].(string); ok { |
|
profile.Website = website |
|
} |
|
if nip05, ok := content["nip05"].(string); ok { |
|
profile.NIP05 = nip05 |
|
} |
|
if lud16, ok := content["lud16"].(string); ok { |
|
profile.Lud16 = lud16 |
|
} |
|
if banner, ok := content["banner"].(string); ok { |
|
profile.Banner = banner |
|
} |
|
|
|
// Store all fields in RawJSON for access to any other fields |
|
profile.RawJSON = content |
|
|
|
return profile, nil |
|
}
|
|
|