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

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
}