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