diff --git a/src/core/di-container.ts b/src/core/di-container.ts index 6c20e0f..fa6e762 100644 --- a/src/core/di-container.ts +++ b/src/core/di-container.ts @@ -144,9 +144,6 @@ export const SERVICE_TOKENS = { SUBMISSION_SERVICE: Symbol('submissionService'), LINK_PREVIEW_SERVICE: Symbol('linkPreviewService'), - // Nostr metadata services - NOSTR_METADATA_SERVICE: Symbol('nostrMetadataService'), - // Nostr transport (kind-21000 RPC over relays — LNbits backend) NOSTR_TRANSPORT_SERVICE: Symbol('nostrTransportService'), diff --git a/src/lib/api/lnbits.ts b/src/lib/api/lnbits.ts index 1e1ecc9..bdd2e18 100644 --- a/src/lib/api/lnbits.ts +++ b/src/lib/api/lnbits.ts @@ -112,12 +112,29 @@ export class LnbitsAPI extends BaseService { if (!response.ok) { const errorText = await response.text() + // Try to surface FastAPI's `{"detail": "..."}` shape; fall back to raw + // body for non-JSON errors. Without this, every backend error renders + // as a generic "API request failed: " and you can't distinguish + // "wrong endpoint" from "expired token" from "validation failure". + let detail: string = errorText + try { + const parsed = JSON.parse(errorText) + if (parsed && typeof parsed.detail === 'string') { + detail = parsed.detail + } else if (parsed && Array.isArray(parsed.detail)) { + // pydantic ValidationError: take the first msg + detail = parsed.detail[0]?.msg ?? errorText + } + } catch { + // body wasn't JSON; keep the raw text in `detail` + } console.error('LNBits API Error:', { + endpoint, status: response.status, statusText: response.statusText, - errorText + detail, }) - throw new Error(`API request failed: ${response.status} ${response.statusText}`) + throw new Error(`LNbits ${endpoint} ${response.status}: ${detail || response.statusText}`) } const data = await response.json() @@ -186,8 +203,12 @@ export class LnbitsAPI extends BaseService { } async updateProfile(data: Partial): Promise { - return this.request('/auth/update', { - method: 'PUT', + // aiolabs/lnbits PR #26 (gap-fill 869f67c3) wired + // _publish_nostr_metadata_event into PATCH /api/v1/auth + // (auth_api.py:546). The legacy PUT /auth/update route does not + // exist on the post-cascade server. + return this.request('/auth', { + method: 'PATCH', body: JSON.stringify(data), }) } diff --git a/src/modules/activities/composables/useActivityDetail.ts b/src/modules/activities/composables/useActivityDetail.ts index e29a272..9fc3ce4 100644 --- a/src/modules/activities/composables/useActivityDetail.ts +++ b/src/modules/activities/composables/useActivityDetail.ts @@ -32,18 +32,24 @@ export function useActivityDetail(activityId: string) { isLoading.value = true error.value = null - // Subscribe and wait for this specific event + // Scope both the subscription and the one-shot query to this + // activity's d-tag. Without this scope, the query asks every + // relay for every kind-31922/31923 event and races a 5s timeout + // to find ours — on a cold page refresh that race is often lost + // even when the activity is reachable. + const detailFilters = { dTags: [activityId] } + unsubscribe = nostrService.subscribeToCalendarEvents( (incoming) => { store.upsertActivity(incoming) if (incoming.id === activityId) { isLoading.value = false } - } + }, + detailFilters ) - // Also do a one-shot query - const results = await nostrService.queryCalendarEvents() + const results = await nostrService.queryCalendarEvents(detailFilters) store.upsertActivities(results) // If we still don't have it after query, stop loading diff --git a/src/modules/activities/services/ActivitiesNostrService.ts b/src/modules/activities/services/ActivitiesNostrService.ts index f098728..da5ed05 100644 --- a/src/modules/activities/services/ActivitiesNostrService.ts +++ b/src/modules/activities/services/ActivitiesNostrService.ts @@ -25,6 +25,8 @@ export interface CalendarEventFilters { hashtags?: string[] /** Filter by geohash prefix (NIP-52 'g' tag) */ geohash?: string + /** Filter by NIP-52 'd' tag — scopes the query to specific parameterized-replaceable events */ + dTags?: string[] } /** @@ -168,6 +170,7 @@ export class ActivitiesNostrService extends BaseService { if (filters?.authors?.length) filter.authors = filters.authors if (filters?.hashtags?.length) filter['#t'] = filters.hashtags if (filters?.geohash) filter['#g'] = [filters.geohash] + if (filters?.dTags?.length) filter['#d'] = filters.dTags return [filter] } diff --git a/src/modules/base/auth/auth-service.ts b/src/modules/base/auth/auth-service.ts index 7b4b6fe..4cc9523 100644 --- a/src/modules/base/auth/auth-service.ts +++ b/src/modules/base/auth/auth-service.ts @@ -2,9 +2,7 @@ import { ref, computed } from 'vue' import { BaseService } from '@/core/base/BaseService' 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 { NostrMetadataService } from '../nostr/nostr-metadata-service' import { getPendingAuthToken, removePendingAuthToken } from '@/lib/config/lnbits' export class AuthService extends BaseService { @@ -114,9 +112,6 @@ export class AuthService extends BaseService { eventBus.emit('auth:login', { user: userData }, 'auth-service') - // Auto-broadcast Nostr metadata on login - this.broadcastNostrMetadata() - } catch (error) { const err = this.handleError(error, 'login') 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') - // Auto-broadcast Nostr metadata on registration - this.broadcastNostrMetadata() - } catch (error) { const err = this.handleError(error, 'register') 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 } - // Auto-broadcast Nostr metadata when profile is updated - // Note: ProfileSettings component will also manually broadcast, - // but this ensures metadata stays in sync even if updated elsewhere - this.broadcastNostrMetadata() + // Kind-0 metadata is published server-side by lnbits's PATCH /auth handler + // (aiolabs/lnbits commit 869f67c3) once the cascade is deployed. The webapp + // no longer maintains its own broadcast path. } catch (error) { 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 { - try { - const metadataService = injectService(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 */ diff --git a/src/modules/base/components/ProfileSettings.vue b/src/modules/base/components/ProfileSettings.vue index 735a39a..1ad99ef 100644 --- a/src/modules/base/components/ProfileSettings.vue +++ b/src/modules/base/components/ProfileSettings.vue @@ -122,32 +122,17 @@ -
- - - -
+

- Your profile is automatically broadcast to Nostr when you update it or log in. - Use the "Broadcast to Nostr" button to manually re-broadcast your profile. + Your profile is broadcast to Nostr automatically when you save changes.

@@ -189,7 +174,7 @@ import * as z from 'zod' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Separator } from '@/components/ui/separator' -import { User, Zap, Hash, Radio } from 'lucide-vue-next' +import { User, Zap, Hash } from 'lucide-vue-next' import { FormControl, FormDescription, @@ -215,19 +200,16 @@ import { useAuth } from '@/composables/useAuthService' import { useRouter } from 'vue-router' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import type { ImageUploadService } from '../services/ImageUploadService' -import type { NostrMetadataService } from '../nostr/nostr-metadata-service' import { useToast } from '@/core/composables/useToast' // Services const { user, updateProfile, logout } = useAuth() const router = useRouter() const imageService = injectService(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE) -const metadataService = injectService(SERVICE_TOKENS.NOSTR_METADATA_SERVICE) const toast = useToast() // Local state const isUpdating = ref(false) -const isBroadcasting = ref(false) const updateError = ref(null) const updateSuccess = ref(false) const uploadedPicture = ref([]) @@ -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) - // Broadcast to Nostr automatically - 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') - } - + toast.success('Profile updated!') updateSuccess.value = true // 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. const onLogout = async () => { try { diff --git a/src/modules/base/composables/useNostrMetadata.ts b/src/modules/base/composables/useNostrMetadata.ts deleted file mode 100644 index dac17a4..0000000 --- a/src/modules/base/composables/useNostrMetadata.ts +++ /dev/null @@ -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(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 - } -} diff --git a/src/modules/base/index.ts b/src/modules/base/index.ts index 16b9fbc..c021e4f 100644 --- a/src/modules/base/index.ts +++ b/src/modules/base/index.ts @@ -2,7 +2,6 @@ import type { App } from 'vue' import type { ModulePlugin } from '@/core/types' import { container, SERVICE_TOKENS } from '@/core/di-container' import { relayHub } from './nostr/relay-hub' -import { NostrMetadataService } from './nostr/nostr-metadata-service' import { ProfileService } from './nostr/ProfileService' import { ReactionService } from './nostr/ReactionService' import { NostrTransportService } from './services/NostrTransportService' @@ -30,7 +29,6 @@ import ProfileSettings from './components/ProfileSettings.vue' const invoiceService = new InvoiceService() const lnbitsAPI = new LnbitsAPI() const imageUploadService = new ImageUploadService() -const nostrMetadataService = new NostrMetadataService() const profileService = new ProfileService() const reactionService = new ReactionService() const nostrTransportService = new NostrTransportService() @@ -48,7 +46,6 @@ export const baseModule: ModulePlugin = { // Register core Nostr services container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub) - container.provide(SERVICE_TOKENS.NOSTR_METADATA_SERVICE, nostrMetadataService) // Register auth service container.provide(SERVICE_TOKENS.AUTH_SERVICE, auth) @@ -113,10 +110,6 @@ export const baseModule: ModulePlugin = { waitForDependencies: true, // ImageUploadService depends on ToastService maxRetries: 3 }) - await nostrMetadataService.initialize({ - waitForDependencies: true, // NostrMetadataService depends on AuthService and RelayHub - maxRetries: 3 - }) await profileService.initialize({ waitForDependencies: true, // ProfileService depends on RelayHub maxRetries: 3 @@ -145,7 +138,6 @@ export const baseModule: ModulePlugin = { await storageService.dispose() await toastService.dispose() await imageUploadService.dispose() - await nostrMetadataService.dispose() await profileService.dispose() await reactionService.dispose() await nostrTransportService.dispose() @@ -156,7 +148,6 @@ export const baseModule: ModulePlugin = { container.remove(SERVICE_TOKENS.LNBITS_API) container.remove(SERVICE_TOKENS.INVOICE_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.REACTION_SERVICE) @@ -173,7 +164,6 @@ export const baseModule: ModulePlugin = { invoiceService, pwaService, imageUploadService, - nostrMetadataService, profileService, reactionService }, diff --git a/src/modules/base/nostr/nostr-metadata-service.ts b/src/modules/base/nostr/nostr-metadata-service.ts deleted file mode 100644 index 16e77f6..0000000 --- a/src/modules/base/nostr/nostr-metadata-service.ts +++ /dev/null @@ -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 { - console.log('NostrMetadataService: Starting initialization...') - - this.authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) - this.relayHub = injectService(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 { - // Cleanup if needed - } -}