Compare commits
2 commits
26a661891a
...
531de18ad7
| Author | SHA1 | Date | |
|---|---|---|---|
| 531de18ad7 | |||
| 414b79565c |
6 changed files with 17 additions and 300 deletions
|
|
@ -144,9 +144,6 @@ export const SERVICE_TOKENS = {
|
||||||
SUBMISSION_SERVICE: Symbol('submissionService'),
|
SUBMISSION_SERVICE: Symbol('submissionService'),
|
||||||
LINK_PREVIEW_SERVICE: Symbol('linkPreviewService'),
|
LINK_PREVIEW_SERVICE: Symbol('linkPreviewService'),
|
||||||
|
|
||||||
// Nostr metadata services
|
|
||||||
NOSTR_METADATA_SERVICE: Symbol('nostrMetadataService'),
|
|
||||||
|
|
||||||
// Nostr transport (kind-21000 RPC over relays — LNbits backend)
|
// Nostr transport (kind-21000 RPC over relays — LNbits backend)
|
||||||
NOSTR_TRANSPORT_SERVICE: Symbol('nostrTransportService'),
|
NOSTR_TRANSPORT_SERVICE: Symbol('nostrTransportService'),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,7 @@
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { BaseService } from '@/core/base/BaseService'
|
import { BaseService } from '@/core/base/BaseService'
|
||||||
import { eventBus } from '@/core/event-bus'
|
import { eventBus } from '@/core/event-bus'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
|
||||||
import type { LoginCredentials, RegisterData, User } from '@/lib/api/lnbits'
|
import type { LoginCredentials, RegisterData, User } from '@/lib/api/lnbits'
|
||||||
import type { NostrMetadataService } from '../nostr/nostr-metadata-service'
|
|
||||||
import { getPendingAuthToken, removePendingAuthToken } from '@/lib/config/lnbits'
|
import { getPendingAuthToken, removePendingAuthToken } from '@/lib/config/lnbits'
|
||||||
|
|
||||||
export class AuthService extends BaseService {
|
export class AuthService extends BaseService {
|
||||||
|
|
@ -114,9 +112,6 @@ export class AuthService extends BaseService {
|
||||||
|
|
||||||
eventBus.emit('auth:login', { user: userData }, 'auth-service')
|
eventBus.emit('auth:login', { user: userData }, 'auth-service')
|
||||||
|
|
||||||
// Auto-broadcast Nostr metadata on login
|
|
||||||
this.broadcastNostrMetadata()
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = this.handleError(error, 'login')
|
const err = this.handleError(error, 'login')
|
||||||
eventBus.emit('auth:login-failed', { error: err }, 'auth-service')
|
eventBus.emit('auth:login-failed', { error: err }, 'auth-service')
|
||||||
|
|
@ -138,9 +133,6 @@ export class AuthService extends BaseService {
|
||||||
|
|
||||||
eventBus.emit('auth:login', { user: userData }, 'auth-service')
|
eventBus.emit('auth:login', { user: userData }, 'auth-service')
|
||||||
|
|
||||||
// Auto-broadcast Nostr metadata on registration
|
|
||||||
this.broadcastNostrMetadata()
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = this.handleError(error, 'register')
|
const err = this.handleError(error, 'register')
|
||||||
eventBus.emit('auth:login-failed', { error: err }, 'auth-service')
|
eventBus.emit('auth:login-failed', { error: err }, 'auth-service')
|
||||||
|
|
@ -195,10 +187,9 @@ export class AuthService extends BaseService {
|
||||||
prvkey: this.user.value?.prvkey || updatedUser.prvkey
|
prvkey: this.user.value?.prvkey || updatedUser.prvkey
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-broadcast Nostr metadata when profile is updated
|
// Kind-0 metadata is published server-side by lnbits's PATCH /auth handler
|
||||||
// Note: ProfileSettings component will also manually broadcast,
|
// (aiolabs/lnbits commit 869f67c3) once the cascade is deployed. The webapp
|
||||||
// but this ensures metadata stays in sync even if updated elsewhere
|
// no longer maintains its own broadcast path.
|
||||||
this.broadcastNostrMetadata()
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = this.handleError(error, 'updateProfile')
|
const err = this.handleError(error, 'updateProfile')
|
||||||
|
|
@ -208,26 +199,6 @@ export class AuthService extends BaseService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcast user metadata to Nostr relays (NIP-01 kind 0)
|
|
||||||
* Called automatically on login, registration, and profile updates
|
|
||||||
*/
|
|
||||||
private async broadcastNostrMetadata(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const metadataService = injectService<NostrMetadataService>(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
|
|
||||||
if (metadataService && this.user.value?.pubkey) {
|
|
||||||
// Broadcast in background - don't block login/update
|
|
||||||
metadataService.publishMetadata().catch(error => {
|
|
||||||
console.warn('Failed to broadcast Nostr metadata:', error)
|
|
||||||
// Don't throw - this is a non-critical background operation
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// If service isn't available yet, silently skip
|
|
||||||
console.debug('Nostr metadata service not yet available')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup when service is disposed
|
* Cleanup when service is disposed
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -122,32 +122,17 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="flex flex-col sm:flex-row gap-3">
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="isUpdating || !isFormValid"
|
:disabled="isUpdating || !isFormValid"
|
||||||
class="flex-1"
|
class="w-full"
|
||||||
>
|
>
|
||||||
<span v-if="isUpdating">Updating...</span>
|
<span v-if="isUpdating">Updating...</span>
|
||||||
<span v-else>Update Profile</span>
|
<span v-else>Update Profile</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
:disabled="isBroadcasting"
|
|
||||||
@click="broadcastMetadata"
|
|
||||||
class="flex-1"
|
|
||||||
>
|
|
||||||
<Radio class="mr-2 h-4 w-4" :class="{ 'animate-pulse': isBroadcasting }" />
|
|
||||||
<span v-if="isBroadcasting">Broadcasting...</span>
|
|
||||||
<span v-else>Broadcast to Nostr</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
Your profile is automatically broadcast to Nostr when you update it or log in.
|
Your profile is broadcast to Nostr automatically when you save changes.
|
||||||
Use the "Broadcast to Nostr" button to manually re-broadcast your profile.
|
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
@ -189,7 +174,7 @@ import * as z from 'zod'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { User, Zap, Hash, Radio } from 'lucide-vue-next'
|
import { User, Zap, Hash } from 'lucide-vue-next'
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
|
|
@ -215,19 +200,16 @@ import { useAuth } from '@/composables/useAuthService'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import type { ImageUploadService } from '../services/ImageUploadService'
|
import type { ImageUploadService } from '../services/ImageUploadService'
|
||||||
import type { NostrMetadataService } from '../nostr/nostr-metadata-service'
|
|
||||||
import { useToast } from '@/core/composables/useToast'
|
import { useToast } from '@/core/composables/useToast'
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
const { user, updateProfile, logout } = useAuth()
|
const { user, updateProfile, logout } = useAuth()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const imageService = injectService<ImageUploadService>(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE)
|
const imageService = injectService<ImageUploadService>(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE)
|
||||||
const metadataService = injectService<NostrMetadataService>(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
// Local state
|
// Local state
|
||||||
const isUpdating = ref(false)
|
const isUpdating = ref(false)
|
||||||
const isBroadcasting = ref(false)
|
|
||||||
const updateError = ref<string | null>(null)
|
const updateError = ref<string | null>(null)
|
||||||
const updateSuccess = ref(false)
|
const updateSuccess = ref(false)
|
||||||
const uploadedPicture = ref<any[]>([])
|
const uploadedPicture = ref<any[]>([])
|
||||||
|
|
@ -323,18 +305,12 @@ const updateUserProfile = async (formData: any) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update profile via AuthService (which updates LNbits)
|
// Update profile via AuthService (which updates LNbits).
|
||||||
|
// Kind-0 metadata publishing happens server-side as part of the
|
||||||
|
// PATCH /api/v1/auth handler (aiolabs/lnbits 869f67c3).
|
||||||
await updateProfile(updateData)
|
await updateProfile(updateData)
|
||||||
|
|
||||||
// Broadcast to Nostr automatically
|
toast.success('Profile updated!')
|
||||||
try {
|
|
||||||
await metadataService.publishMetadata()
|
|
||||||
toast.success('Profile updated and broadcast to Nostr!')
|
|
||||||
} catch (nostrError) {
|
|
||||||
console.error('Failed to broadcast to Nostr:', nostrError)
|
|
||||||
toast.warning('Profile updated, but failed to broadcast to Nostr')
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSuccess.value = true
|
updateSuccess.value = true
|
||||||
|
|
||||||
// Clear success message after 3 seconds
|
// Clear success message after 3 seconds
|
||||||
|
|
@ -352,22 +328,6 @@ const updateUserProfile = async (formData: any) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manually broadcast metadata to Nostr
|
|
||||||
const broadcastMetadata = async () => {
|
|
||||||
isBroadcasting.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await metadataService.publishMetadata()
|
|
||||||
toast.success(`Profile broadcast to ${result.success}/${result.total} relays!`)
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Failed to broadcast metadata'
|
|
||||||
console.error('Error broadcasting metadata:', error)
|
|
||||||
toast.error(`Failed to broadcast: ${errorMessage}`)
|
|
||||||
} finally {
|
|
||||||
isBroadcasting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log out + redirect to /login on this app's origin.
|
// Log out + redirect to /login on this app's origin.
|
||||||
const onLogout = async () => {
|
const onLogout = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
|
||||||
import type { NostrMetadataService, NostrMetadata } from '../nostr/nostr-metadata-service'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable for accessing Nostr metadata service
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* const { publishMetadata, getMetadata } = useNostrMetadata()
|
|
||||||
*
|
|
||||||
* // Get current metadata
|
|
||||||
* const metadata = getMetadata()
|
|
||||||
*
|
|
||||||
* // Publish metadata to Nostr relays
|
|
||||||
* await publishMetadata()
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function useNostrMetadata() {
|
|
||||||
const metadataService = injectService<NostrMetadataService>(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Publish user metadata to Nostr relays (NIP-01 kind 0)
|
|
||||||
*/
|
|
||||||
const publishMetadata = async (): Promise<{ success: number; total: number }> => {
|
|
||||||
return await metadataService.publishMetadata()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current user's Nostr metadata
|
|
||||||
*/
|
|
||||||
const getMetadata = (): NostrMetadata => {
|
|
||||||
return metadataService.getMetadata()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
publishMetadata,
|
|
||||||
getMetadata
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,6 @@ import type { App } from 'vue'
|
||||||
import type { ModulePlugin } from '@/core/types'
|
import type { ModulePlugin } from '@/core/types'
|
||||||
import { container, SERVICE_TOKENS } from '@/core/di-container'
|
import { container, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import { relayHub } from './nostr/relay-hub'
|
import { relayHub } from './nostr/relay-hub'
|
||||||
import { NostrMetadataService } from './nostr/nostr-metadata-service'
|
|
||||||
import { ProfileService } from './nostr/ProfileService'
|
import { ProfileService } from './nostr/ProfileService'
|
||||||
import { ReactionService } from './nostr/ReactionService'
|
import { ReactionService } from './nostr/ReactionService'
|
||||||
import { NostrTransportService } from './services/NostrTransportService'
|
import { NostrTransportService } from './services/NostrTransportService'
|
||||||
|
|
@ -30,7 +29,6 @@ import ProfileSettings from './components/ProfileSettings.vue'
|
||||||
const invoiceService = new InvoiceService()
|
const invoiceService = new InvoiceService()
|
||||||
const lnbitsAPI = new LnbitsAPI()
|
const lnbitsAPI = new LnbitsAPI()
|
||||||
const imageUploadService = new ImageUploadService()
|
const imageUploadService = new ImageUploadService()
|
||||||
const nostrMetadataService = new NostrMetadataService()
|
|
||||||
const profileService = new ProfileService()
|
const profileService = new ProfileService()
|
||||||
const reactionService = new ReactionService()
|
const reactionService = new ReactionService()
|
||||||
const nostrTransportService = new NostrTransportService()
|
const nostrTransportService = new NostrTransportService()
|
||||||
|
|
@ -48,7 +46,6 @@ export const baseModule: ModulePlugin = {
|
||||||
|
|
||||||
// Register core Nostr services
|
// Register core Nostr services
|
||||||
container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub)
|
container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub)
|
||||||
container.provide(SERVICE_TOKENS.NOSTR_METADATA_SERVICE, nostrMetadataService)
|
|
||||||
|
|
||||||
// Register auth service
|
// Register auth service
|
||||||
container.provide(SERVICE_TOKENS.AUTH_SERVICE, auth)
|
container.provide(SERVICE_TOKENS.AUTH_SERVICE, auth)
|
||||||
|
|
@ -113,10 +110,6 @@ export const baseModule: ModulePlugin = {
|
||||||
waitForDependencies: true, // ImageUploadService depends on ToastService
|
waitForDependencies: true, // ImageUploadService depends on ToastService
|
||||||
maxRetries: 3
|
maxRetries: 3
|
||||||
})
|
})
|
||||||
await nostrMetadataService.initialize({
|
|
||||||
waitForDependencies: true, // NostrMetadataService depends on AuthService and RelayHub
|
|
||||||
maxRetries: 3
|
|
||||||
})
|
|
||||||
await profileService.initialize({
|
await profileService.initialize({
|
||||||
waitForDependencies: true, // ProfileService depends on RelayHub
|
waitForDependencies: true, // ProfileService depends on RelayHub
|
||||||
maxRetries: 3
|
maxRetries: 3
|
||||||
|
|
@ -145,7 +138,6 @@ export const baseModule: ModulePlugin = {
|
||||||
await storageService.dispose()
|
await storageService.dispose()
|
||||||
await toastService.dispose()
|
await toastService.dispose()
|
||||||
await imageUploadService.dispose()
|
await imageUploadService.dispose()
|
||||||
await nostrMetadataService.dispose()
|
|
||||||
await profileService.dispose()
|
await profileService.dispose()
|
||||||
await reactionService.dispose()
|
await reactionService.dispose()
|
||||||
await nostrTransportService.dispose()
|
await nostrTransportService.dispose()
|
||||||
|
|
@ -156,7 +148,6 @@ export const baseModule: ModulePlugin = {
|
||||||
container.remove(SERVICE_TOKENS.LNBITS_API)
|
container.remove(SERVICE_TOKENS.LNBITS_API)
|
||||||
container.remove(SERVICE_TOKENS.INVOICE_SERVICE)
|
container.remove(SERVICE_TOKENS.INVOICE_SERVICE)
|
||||||
container.remove(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE)
|
container.remove(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE)
|
||||||
container.remove(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
|
|
||||||
container.remove(SERVICE_TOKENS.PROFILE_SERVICE)
|
container.remove(SERVICE_TOKENS.PROFILE_SERVICE)
|
||||||
container.remove(SERVICE_TOKENS.REACTION_SERVICE)
|
container.remove(SERVICE_TOKENS.REACTION_SERVICE)
|
||||||
|
|
||||||
|
|
@ -173,7 +164,6 @@ export const baseModule: ModulePlugin = {
|
||||||
invoiceService,
|
invoiceService,
|
||||||
pwaService,
|
pwaService,
|
||||||
imageUploadService,
|
imageUploadService,
|
||||||
nostrMetadataService,
|
|
||||||
profileService,
|
profileService,
|
||||||
reactionService
|
reactionService
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
import { BaseService } from '@/core/base/BaseService'
|
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
|
||||||
import { finalizeEvent, type EventTemplate } from 'nostr-tools'
|
|
||||||
import type { AuthService } from '@/modules/base/auth/auth-service'
|
|
||||||
import type { RelayHub } from '@/modules/base/nostr/relay-hub'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nostr User Metadata (NIP-01 kind 0)
|
|
||||||
* https://github.com/nostr-protocol/nips/blob/master/01.md
|
|
||||||
*/
|
|
||||||
export interface NostrMetadata {
|
|
||||||
name?: string // Display name (from username)
|
|
||||||
display_name?: string // Alternative display name
|
|
||||||
about?: string // Bio/description
|
|
||||||
picture?: string // Profile picture URL
|
|
||||||
banner?: string // Profile banner URL
|
|
||||||
nip05?: string // NIP-05 identifier (username@domain)
|
|
||||||
lud16?: string // Lightning Address (same as nip05)
|
|
||||||
website?: string // Personal website
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for publishing and managing Nostr user metadata (NIP-01 kind 0)
|
|
||||||
*
|
|
||||||
* This service handles:
|
|
||||||
* - Publishing user profile metadata to Nostr relays
|
|
||||||
* - Syncing LNbits user data with Nostr profile
|
|
||||||
* - Auto-broadcasting metadata on login and profile updates
|
|
||||||
*/
|
|
||||||
export class NostrMetadataService extends BaseService {
|
|
||||||
protected readonly metadata = {
|
|
||||||
name: 'NostrMetadataService',
|
|
||||||
version: '1.0.0',
|
|
||||||
dependencies: ['AuthService', 'RelayHub']
|
|
||||||
}
|
|
||||||
|
|
||||||
protected authService: AuthService | null = null
|
|
||||||
protected relayHub: RelayHub | null = null
|
|
||||||
|
|
||||||
protected async onInitialize(): Promise<void> {
|
|
||||||
console.log('NostrMetadataService: Starting initialization...')
|
|
||||||
|
|
||||||
this.authService = injectService<AuthService>(SERVICE_TOKENS.AUTH_SERVICE)
|
|
||||||
this.relayHub = injectService<RelayHub>(SERVICE_TOKENS.RELAY_HUB)
|
|
||||||
|
|
||||||
if (!this.authService) {
|
|
||||||
throw new Error('AuthService not available')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.relayHub) {
|
|
||||||
throw new Error('RelayHub service not available')
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('NostrMetadataService: Initialization complete')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build Nostr metadata from LNbits user data
|
|
||||||
*/
|
|
||||||
private buildMetadata(): NostrMetadata {
|
|
||||||
const user = this.authService?.user.value
|
|
||||||
if (!user) {
|
|
||||||
throw new Error('No authenticated user')
|
|
||||||
}
|
|
||||||
|
|
||||||
const lightningDomain = import.meta.env.VITE_LIGHTNING_DOMAIN || window.location.hostname
|
|
||||||
const username = user.username || user.id.slice(0, 8)
|
|
||||||
|
|
||||||
const metadata: NostrMetadata = {
|
|
||||||
name: username,
|
|
||||||
nip05: `${username}@${lightningDomain}`,
|
|
||||||
lud16: `${username}@${lightningDomain}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add optional fields from user.extra if they exist
|
|
||||||
if (user.extra?.display_name) {
|
|
||||||
metadata.display_name = user.extra.display_name
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.extra?.picture) {
|
|
||||||
metadata.picture = user.extra.picture
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Publish user metadata to Nostr relays (NIP-01 kind 0)
|
|
||||||
*
|
|
||||||
* This creates a replaceable event that updates the user's profile.
|
|
||||||
* Only the latest kind 0 event for a given pubkey is kept by relays.
|
|
||||||
*/
|
|
||||||
async publishMetadata(): Promise<{ success: number; total: number }> {
|
|
||||||
if (!this.authService?.isAuthenticated.value) {
|
|
||||||
throw new Error('Must be authenticated to publish metadata')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.relayHub?.isConnected.value) {
|
|
||||||
throw new Error('Not connected to relays')
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = this.authService.user.value
|
|
||||||
if (!user?.prvkey) {
|
|
||||||
throw new Error('User private key not available')
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const metadata = this.buildMetadata()
|
|
||||||
|
|
||||||
console.log('📤 Publishing Nostr metadata (kind 0):', metadata)
|
|
||||||
|
|
||||||
// Create kind 0 event (user metadata)
|
|
||||||
// Content is JSON-stringified metadata
|
|
||||||
const eventTemplate: EventTemplate = {
|
|
||||||
kind: 0,
|
|
||||||
content: JSON.stringify(metadata),
|
|
||||||
tags: [],
|
|
||||||
created_at: Math.floor(Date.now() / 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign the event
|
|
||||||
const privkeyBytes = this.hexToUint8Array(user.prvkey)
|
|
||||||
const signedEvent = finalizeEvent(eventTemplate, privkeyBytes)
|
|
||||||
|
|
||||||
console.log('✅ Metadata event signed:', signedEvent.id)
|
|
||||||
console.log('📋 Full signed event:', JSON.stringify(signedEvent, null, 2))
|
|
||||||
|
|
||||||
// Publish to all connected relays
|
|
||||||
const result = await this.relayHub.publishEvent(signedEvent)
|
|
||||||
|
|
||||||
console.log(`✅ Metadata published to ${result.success}/${result.total} relays`)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to publish metadata:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current user's Nostr metadata
|
|
||||||
*/
|
|
||||||
getMetadata(): NostrMetadata {
|
|
||||||
return this.buildMetadata()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to convert hex string to Uint8Array
|
|
||||||
*/
|
|
||||||
private hexToUint8Array(hex: string): Uint8Array {
|
|
||||||
const bytes = new Uint8Array(hex.length / 2)
|
|
||||||
for (let i = 0; i < hex.length; i += 2) {
|
|
||||||
bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
|
|
||||||
}
|
|
||||||
return bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async onDestroy(): Promise<void> {
|
|
||||||
// Cleanup if needed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue