05-users-profiles.ts raw

   1  /**
   2   * NDK User and Profile Handling
   3   * 
   4   * Examples from: src/queries/profiles.tsx, src/components/Profile.tsx
   5   */
   6  
   7  import NDK, { NDKUser, NDKUserProfile } from '@nostr-dev-kit/ndk'
   8  import { nip19 } from 'nostr-tools'
   9  
  10  // ============================================================
  11  // FETCH PROFILE BY NPUB
  12  // ============================================================
  13  
  14  const fetchProfileByNpub = async (ndk: NDK, npub: string): Promise<NDKUserProfile | null> => {
  15    try {
  16      // Get user object from npub
  17      const user = ndk.getUser({ npub })
  18      
  19      // Fetch profile from relays
  20      const profile = await user.fetchProfile()
  21      
  22      return profile
  23    } catch (error) {
  24      console.error('Failed to fetch profile:', error)
  25      return null
  26    }
  27  }
  28  
  29  // ============================================================
  30  // FETCH PROFILE BY HEX PUBKEY
  31  // ============================================================
  32  
  33  const fetchProfileByPubkey = async (ndk: NDK, pubkey: string): Promise<NDKUserProfile | null> => {
  34    try {
  35      const user = ndk.getUser({ hexpubkey: pubkey })
  36      const profile = await user.fetchProfile()
  37      
  38      return profile
  39    } catch (error) {
  40      console.error('Failed to fetch profile:', error)
  41      return null
  42    }
  43  }
  44  
  45  // ============================================================
  46  // FETCH PROFILE BY NIP-05
  47  // ============================================================
  48  
  49  const fetchProfileByNip05 = async (ndk: NDK, nip05: string): Promise<NDKUserProfile | null> => {
  50    try {
  51      // Resolve NIP-05 identifier to user
  52      const user = await ndk.getUserFromNip05(nip05)
  53      
  54      if (!user) {
  55        console.log('User not found for NIP-05:', nip05)
  56        return null
  57      }
  58      
  59      // Fetch profile
  60      const profile = await user.fetchProfile()
  61      
  62      return profile
  63    } catch (error) {
  64      console.error('Failed to fetch profile by NIP-05:', error)
  65      return null
  66    }
  67  }
  68  
  69  // ============================================================
  70  // FETCH PROFILE BY ANY IDENTIFIER
  71  // ============================================================
  72  
  73  const fetchProfileByIdentifier = async (
  74    ndk: NDK,
  75    identifier: string
  76  ): Promise<{ profile: NDKUserProfile | null; user: NDKUser | null }> => {
  77    try {
  78      // Check if it's a NIP-05 (contains @)
  79      if (identifier.includes('@')) {
  80        const user = await ndk.getUserFromNip05(identifier)
  81        if (!user) return { profile: null, user: null }
  82        
  83        const profile = await user.fetchProfile()
  84        return { profile, user }
  85      }
  86      
  87      // Check if it's an npub
  88      if (identifier.startsWith('npub')) {
  89        const user = ndk.getUser({ npub: identifier })
  90        const profile = await user.fetchProfile()
  91        return { profile, user }
  92      }
  93      
  94      // Assume it's a hex pubkey
  95      const user = ndk.getUser({ hexpubkey: identifier })
  96      const profile = await user.fetchProfile()
  97      return { profile, user }
  98    } catch (error) {
  99      console.error('Failed to fetch profile:', error)
 100      return { profile: null, user: null }
 101    }
 102  }
 103  
 104  // ============================================================
 105  // GET CURRENT USER
 106  // ============================================================
 107  
 108  const getCurrentUser = async (ndk: NDK): Promise<NDKUser | null> => {
 109    if (!ndk.signer) {
 110      console.log('No signer set')
 111      return null
 112    }
 113    
 114    try {
 115      const user = await ndk.signer.user()
 116      return user
 117    } catch (error) {
 118      console.error('Failed to get current user:', error)
 119      return null
 120    }
 121  }
 122  
 123  // ============================================================
 124  // PROFILE DATA STRUCTURE
 125  // ============================================================
 126  
 127  interface ProfileData {
 128    // Standard fields
 129    name?: string
 130    displayName?: string
 131    display_name?: string
 132    picture?: string
 133    image?: string
 134    banner?: string
 135    about?: string
 136    
 137    // Contact
 138    nip05?: string
 139    lud06?: string  // LNURL
 140    lud16?: string  // Lightning address
 141    
 142    // Social
 143    website?: string
 144    
 145    // Raw data
 146    [key: string]: any
 147  }
 148  
 149  // ============================================================
 150  // EXTRACT PROFILE INFO
 151  // ============================================================
 152  
 153  const extractProfileInfo = (profile: NDKUserProfile | null) => {
 154    if (!profile) {
 155      return {
 156        displayName: 'Anonymous',
 157        avatar: null,
 158        bio: null,
 159        lightningAddress: null,
 160        nip05: null
 161      }
 162    }
 163    
 164    return {
 165      displayName: profile.displayName || profile.display_name || profile.name || 'Anonymous',
 166      avatar: profile.picture || profile.image || null,
 167      banner: profile.banner || null,
 168      bio: profile.about || null,
 169      lightningAddress: profile.lud16 || profile.lud06 || null,
 170      nip05: profile.nip05 || null,
 171      website: profile.website || null
 172    }
 173  }
 174  
 175  // ============================================================
 176  // UPDATE PROFILE
 177  // ============================================================
 178  
 179  import { NDKEvent } from '@nostr-dev-kit/ndk'
 180  
 181  const updateProfile = async (ndk: NDK, profileData: Partial<ProfileData>) => {
 182    if (!ndk.signer) {
 183      throw new Error('No signer available')
 184    }
 185    
 186    // Get current profile
 187    const currentUser = await ndk.signer.user()
 188    const currentProfile = await currentUser.fetchProfile()
 189    
 190    // Merge with new data
 191    const updatedProfile = {
 192      ...currentProfile,
 193      ...profileData
 194    }
 195    
 196    // Create kind 0 (metadata) event
 197    const event = new NDKEvent(ndk)
 198    event.kind = 0
 199    event.content = JSON.stringify(updatedProfile)
 200    event.tags = []
 201    
 202    await event.sign()
 203    await event.publish()
 204    
 205    console.log('✅ Profile updated')
 206    return event.id
 207  }
 208  
 209  // ============================================================
 210  // BATCH FETCH PROFILES
 211  // ============================================================
 212  
 213  const fetchMultipleProfiles = async (
 214    ndk: NDK,
 215    pubkeys: string[]
 216  ): Promise<Map<string, NDKUserProfile | null>> => {
 217    const profiles = new Map<string, NDKUserProfile | null>()
 218    
 219    // Fetch all profiles in parallel
 220    await Promise.all(
 221      pubkeys.map(async (pubkey) => {
 222        try {
 223          const user = ndk.getUser({ hexpubkey: pubkey })
 224          const profile = await user.fetchProfile()
 225          profiles.set(pubkey, profile)
 226        } catch (error) {
 227          console.error(`Failed to fetch profile for ${pubkey}:`, error)
 228          profiles.set(pubkey, null)
 229        }
 230      })
 231    )
 232    
 233    return profiles
 234  }
 235  
 236  // ============================================================
 237  // CONVERT BETWEEN FORMATS
 238  // ============================================================
 239  
 240  const convertPubkeyFormats = (identifier: string) => {
 241    try {
 242      // If it's npub, convert to hex
 243      if (identifier.startsWith('npub')) {
 244        const decoded = nip19.decode(identifier)
 245        if (decoded.type === 'npub') {
 246          return {
 247            hex: decoded.data as string,
 248            npub: identifier
 249          }
 250        }
 251      }
 252      
 253      // If it's hex, convert to npub
 254      if (/^[0-9a-f]{64}$/.test(identifier)) {
 255        return {
 256          hex: identifier,
 257          npub: nip19.npubEncode(identifier)
 258        }
 259      }
 260      
 261      throw new Error('Invalid pubkey format')
 262    } catch (error) {
 263      console.error('Format conversion failed:', error)
 264      return null
 265    }
 266  }
 267  
 268  // ============================================================
 269  // REACT HOOK FOR PROFILE
 270  // ============================================================
 271  
 272  import { useQuery } from '@tanstack/react-query'
 273  import { useEffect, useState } from 'react'
 274  
 275  function useProfile(ndk: NDK | null, npub: string | undefined) {
 276    return useQuery({
 277      queryKey: ['profile', npub],
 278      queryFn: async () => {
 279        if (!ndk || !npub) throw new Error('NDK or npub missing')
 280        return await fetchProfileByNpub(ndk, npub)
 281      },
 282      enabled: !!ndk && !!npub,
 283      staleTime: 5 * 60 * 1000,  // 5 minutes
 284      cacheTime: 30 * 60 * 1000  // 30 minutes
 285    })
 286  }
 287  
 288  // ============================================================
 289  // REACT COMPONENT EXAMPLE
 290  // ============================================================
 291  
 292  interface ProfileDisplayProps {
 293    ndk: NDK
 294    pubkey: string
 295  }
 296  
 297  function ProfileDisplay({ ndk, pubkey }: ProfileDisplayProps) {
 298    const [profile, setProfile] = useState<NDKUserProfile | null>(null)
 299    const [loading, setLoading] = useState(true)
 300    
 301    useEffect(() => {
 302      const loadProfile = async () => {
 303        setLoading(true)
 304        try {
 305          const user = ndk.getUser({ hexpubkey: pubkey })
 306          const fetchedProfile = await user.fetchProfile()
 307          setProfile(fetchedProfile)
 308        } catch (error) {
 309          console.error('Failed to load profile:', error)
 310        } finally {
 311          setLoading(false)
 312        }
 313      }
 314      
 315      loadProfile()
 316    }, [ndk, pubkey])
 317    
 318    if (loading) {
 319      return <div>Loading profile...</div>
 320    }
 321    
 322    const info = extractProfileInfo(profile)
 323    
 324    return (
 325      <div className="profile">
 326        {info.avatar && <img src={info.avatar} alt={info.displayName} />}
 327        <h2>{info.displayName}</h2>
 328        {info.bio && <p>{info.bio}</p>}
 329        {info.nip05 && <span>✓ {info.nip05}</span>}
 330        {info.lightningAddress && <span>⚡ {info.lightningAddress}</span>}
 331      </div>
 332    )
 333  }
 334  
 335  // ============================================================
 336  // FOLLOW/UNFOLLOW USER
 337  // ============================================================
 338  
 339  const followUser = async (ndk: NDK, pubkeyToFollow: string) => {
 340    if (!ndk.signer) {
 341      throw new Error('No signer available')
 342    }
 343    
 344    // Fetch current contact list (kind 3)
 345    const currentUser = await ndk.signer.user()
 346    const contactListFilter = {
 347      kinds: [3],
 348      authors: [currentUser.pubkey]
 349    }
 350    
 351    const existingEvents = await ndk.fetchEvents(contactListFilter)
 352    const existingContactList = existingEvents.size > 0
 353      ? Array.from(existingEvents)[0]
 354      : null
 355    
 356    // Get existing p tags
 357    const existingPTags = existingContactList
 358      ? existingContactList.tags.filter(tag => tag[0] === 'p')
 359      : []
 360    
 361    // Check if already following
 362    const alreadyFollowing = existingPTags.some(tag => tag[1] === pubkeyToFollow)
 363    if (alreadyFollowing) {
 364      console.log('Already following this user')
 365      return
 366    }
 367    
 368    // Create new contact list with added user
 369    const event = new NDKEvent(ndk)
 370    event.kind = 3
 371    event.content = existingContactList?.content || ''
 372    event.tags = [
 373      ...existingPTags,
 374      ['p', pubkeyToFollow]
 375    ]
 376    
 377    await event.sign()
 378    await event.publish()
 379    
 380    console.log('✅ Now following user')
 381  }
 382  
 383  // ============================================================
 384  // USAGE EXAMPLE
 385  // ============================================================
 386  
 387  async function profileExample(ndk: NDK) {
 388    // Fetch by different identifiers
 389    const profile1 = await fetchProfileByNpub(ndk, 'npub1...')
 390    const profile2 = await fetchProfileByNip05(ndk, 'user@domain.com')
 391    const profile3 = await fetchProfileByPubkey(ndk, 'hex pubkey...')
 392    
 393    // Extract display info
 394    const info = extractProfileInfo(profile1)
 395    console.log('Display name:', info.displayName)
 396    console.log('Avatar:', info.avatar)
 397    
 398    // Update own profile
 399    await updateProfile(ndk, {
 400      name: 'My Name',
 401      about: 'My bio',
 402      picture: 'https://example.com/avatar.jpg',
 403      lud16: 'me@getalby.com'
 404    })
 405    
 406    // Follow someone
 407    await followUser(ndk, 'pubkey to follow')
 408  }
 409  
 410  export {
 411    fetchProfileByNpub,
 412    fetchProfileByPubkey,
 413    fetchProfileByNip05,
 414    fetchProfileByIdentifier,
 415    getCurrentUser,
 416    extractProfileInfo,
 417    updateProfile,
 418    fetchMultipleProfiles,
 419    convertPubkeyFormats,
 420    useProfile,
 421    followUser
 422  }
 423  
 424