From 414b79565c6e022a487d448e2f461a92024b8f9e Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 29 May 2026 21:28:48 +0200 Subject: [PATCH 1/3] chore(base): delete nostr-metadata-service + retire webapp-side kind-0 broadcast paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lnbits's cascade now publishes kind-0 user metadata server-side on account creation AND on every PATCH /api/v1/auth (aiolabs/lnbits commit 869f67c3 folded into PR #26, deployed to aio-demo via server-deploy e2eed9c). The webapp no longer needs its own kind-0 publish surface. Changes: - Delete src/modules/base/nostr/nostr-metadata-service.ts (162 lines). Server now owns kind-0 lifecycle via NostrSigner.sign_event. - Delete src/modules/base/composables/useNostrMetadata.ts (had zero callers; was just a thin wrapper around the deleted service). - Remove NOSTR_METADATA_SERVICE token from di-container.ts. - Remove all NostrMetadataService imports / instantiations / registrations / dispose calls from src/modules/base/index.ts. - src/modules/base/auth/auth-service.ts: - Drop the broadcastNostrMetadata() helper entirely. - Drop its callers in login() (was line 118 pre-edit) and register() (was line 142 pre-edit) — both flagged for removal by lnbits in the 01:45Z coordination handoff. Login-time republish was always redundant for kind-0 (replaceable event); register-time is covered by lnbits's create_user_account -> _publish_nostr_metadata_event path. - Drop the auto-broadcast in updateProfile() too — covered by the PATCH /api/v1/auth handler's _publish_nostr_metadata_event call per the gap-fill commit. - Leave the prvkey/pubkey preservation in updateProfile() in place for now; the prvkey field removal is the atomic phase-1 final PR per design doc Q1.2 Option (b). - src/modules/base/components/ProfileSettings.vue: - Remove the "Broadcast to Nostr" button + isBroadcasting state + Radio icon + broadcastMetadata() handler. Manual re-broadcast was a local-testing safety net for relay resets that's no longer needed once the server publishes automatically on profile save. - Simplify the post-save toast to a generic "Profile updated!". - Update the helper text accordingly. This is webapp's bucket-A leg per aiolabs/lnbits#9 phase-1 plan. Refs: - log:2026-05-29T01:45Z (lnbits handoff identifying the auth-service line numbers to drop) - ~/dev/coordination/webapp-design-questions.md Q2.3 (decision context) - aiolabs/lnbits PR #26 commit 869f67c3 (server-side kind-0 publish) - aiolabs/lnbits dev tip 861f427c, deployed to aio-demo Co-Authored-By: Claude Opus 4.7 (1M context) --- src/core/di-container.ts | 3 - src/modules/base/auth/auth-service.ts | 35 +--- .../base/components/ProfileSettings.vue | 68 ++------ .../base/composables/useNostrMetadata.ts | 39 ----- src/modules/base/index.ts | 10 -- .../base/nostr/nostr-metadata-service.ts | 162 ------------------ 6 files changed, 17 insertions(+), 300 deletions(-) delete mode 100644 src/modules/base/composables/useNostrMetadata.ts delete mode 100644 src/modules/base/nostr/nostr-metadata-service.ts 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/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 - } -} -- 2.53.0 From 261eded3166e80289647c957aec6823a7da41c33 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 30 May 2026 07:45:35 +0200 Subject: [PATCH 2/3] fix(api): align webapp client with post-cascade lnbits + surface error detail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes that together make the kind-0 server-side publish path (this PR's whole reason for existing) actually work end-to-end against the deployed cascade: 1. **updateProfile() uses PATCH /api/v1/auth, not PUT /auth/update.** aiolabs/lnbits PR #26 gap-fill (869f67c3) wired _publish_nostr_metadata_event into the PATCH handler at auth_api.py:546. The legacy `/auth/update` route doesn't exist on the post-cascade server — a `PUT /auth/update` request gets routed into the `/auth/{provider}` SSO wildcard which only allows GET and returns 405. Caught while smoke-testing this PR against a local regtest pointed at the issue-18-phase-2.3 branch. 2. **request() parses FastAPI's `{"detail": "..."}` error shape.** The old error path threw `API request failed: 405 Method Not Allowed` for the regtest's 405 above — useful only if you also opened the network panel and read the response body manually. Now we parse the detail (string or pydantic-validation array), include the endpoint path, and throw `LNbits /auth 405: Method Not Allowed`. Falls back to raw text for non-JSON bodies. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/api/lnbits.ts | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) 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), }) } -- 2.53.0 From cb6e1351fb6299de46b5bf8b622d72ae0dd4dd5c Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 30 May 2026 08:03:55 +0200 Subject: [PATCH 3/3] fix(activities): scope detail-page query by NIP-52 d-tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `useActivityDetail.load()` previously asked every relay for every kind-31922/31923 event and raced a 5s timeout to find the one matching the route param. On a cold refresh of the detail page, the race was often lost — the store starts empty (no feed subscription to populate it), the relay sprays the whole calendar, and the matching event may arrive after the timeout, leaving the user with "Activity not found" on a valid URL. Add a `dTags` field to `CalendarEventFilters` and emit it as the nostr `#d` tag filter. Detail-page subscribe + query both scope to the single activity, so the relay resolves a parameterized-replaceable lookup in milliseconds instead of streaming the whole calendar. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../activities/composables/useActivityDetail.ts | 14 ++++++++++---- .../activities/services/ActivitiesNostrService.ts | 3 +++ 2 files changed, 13 insertions(+), 4 deletions(-) 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] } -- 2.53.0