diff --git a/src/modules/activities/components/CreateActivityDialog.vue b/src/modules/activities/components/CreateActivityDialog.vue index 0c6935a..2c5654c 100644 --- a/src/modules/activities/components/CreateActivityDialog.vue +++ b/src/modules/activities/components/CreateActivityDialog.vue @@ -16,8 +16,8 @@ import { Button } from '@/components/ui/button' import { CalendarPlus } from 'lucide-vue-next' import { useAuth } from '@/composables/useAuthService' import { tryInjectService, SERVICE_TOKENS } from '@/core/di-container' -import type { ActivitiesNostrService } from '../services/ActivitiesNostrService' -import type { CalendarTimeEvent } from '../types/nip52' +import type { TicketApiService } from '../services/TicketApiService' +import type { CreateEventRequest } from '../types/ticket' import type { ActivityCategory } from '../types/category' import CategorySelector from './CategorySelector.vue' import LocationPicker from './LocationPicker.vue' @@ -67,56 +67,64 @@ const form = useForm({ const isFormValid = computed(() => form.meta.value.valid) const onSubmit = form.handleSubmit(async (values) => { - const nostrService = tryInjectService(SERVICE_TOKENS.ACTIVITIES_NOSTR_SERVICE) - if (!nostrService) { + const ticketApi = tryInjectService(SERVICE_TOKENS.TICKET_API) + if (!ticketApi) { toast.error('Activities service not available') return } - const signingKey = currentUser.value?.prvkey - if (!signingKey) { - toast.error('Signing key not available. Please log in again.') + const invoiceKey = currentUser.value?.wallets?.[0]?.inkey + if (!invoiceKey) { + toast.error('No wallet available. Please log in first.') return } isPublishing.value = true try { - // Build unix timestamps - const startTimestamp = Math.floor(new Date(`${values.startDate}T${values.startTime}`).getTime() / 1000) - let endTimestamp: number | undefined - if (values.endDate && values.endTime) { - endTimestamp = Math.floor(new Date(`${values.endDate}T${values.endTime}`).getTime() / 1000) + // Compose ISO 8601 datetime strings the events extension parses. + const startIso = `${values.startDate}T${values.startTime}` + const endIso = + values.endDate && values.endTime + ? `${values.endDate}T${values.endTime}` + : undefined + + // Fold summary + description into `info` since the events extension + // CreateEventRequest has no separate summary field. + const info = + values.summary && values.description + ? `${values.summary}\n\n${values.description}` + : values.description || values.summary || '' + + // Ticket-less activity — amount_tickets and price_per_ticket both + // pinned at 0 (events extension treats 0 as "unlimited / not + // ticketed" per models.py:45-46). Server-side `signer.sign_event` + // produces the kind-31922 calendar event and publishes via the + // operator's configured relays — no webapp signing path needed. + const eventData: CreateEventRequest = { + name: values.title, + info, + event_start_date: startIso, + event_end_date: endIso, + location: location.value || null, + banner: values.image || null, + categories: selectedCategories.value, + amount_tickets: 0, + price_per_ticket: 0, } - // Generate a unique d-tag - const dTag = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + await ticketApi.createEvent(eventData, invoiceKey) - const eventData: Partial = { - dTag, - title: values.title, - summary: values.summary || undefined, - content: values.description, - image: values.image || undefined, - start: startTimestamp, - end: endTimestamp, - startTzid: Intl.DateTimeFormat().resolvedOptions().timeZone, - location: location.value || undefined, - hashtags: selectedCategories.value, - } - - const result = await nostrService.publishCalendarEvent(eventData, signingKey) - - if (result.success > 0) { - toast.success(`Activity published to ${result.success} relay${result.success > 1 ? 's' : ''}`) - emit('created') - handleClose() - } else { - toast.error('Failed to publish to any relay') - } + // Approval workflow caveat: non-admin users on instances with + // `auto_approve=false` (the default) land in the proposal queue; + // their event isn't published to relays until an admin approves. + // Admins-and-auto-approve-on instances publish immediately. + toast.success('Activity created!') + emit('created') + handleClose() } catch (err) { - console.error('Failed to publish activity:', err) - toast.error(err instanceof Error ? err.message : 'Failed to publish activity') + console.error('Failed to create activity:', err) + toast.error(err instanceof Error ? err.message : 'Failed to create activity') } finally { isPublishing.value = false } diff --git a/src/modules/activities/services/ActivitiesNostrService.ts b/src/modules/activities/services/ActivitiesNostrService.ts index f098728..0b0a00e 100644 --- a/src/modules/activities/services/ActivitiesNostrService.ts +++ b/src/modules/activities/services/ActivitiesNostrService.ts @@ -1,12 +1,10 @@ import { BaseService } from '@/core/base/BaseService' -import { finalizeEvent, type Event as NostrEvent, type EventTemplate } from 'nostr-tools' +import type { Event as NostrEvent } from 'nostr-tools' import type { SubscriptionConfig } from '@/modules/base/nostr/relay-hub' import { NIP52_KINDS, parseCalendarTimeEvent, parseCalendarDateEvent, - buildCalendarTimeEventTags, - type CalendarTimeEvent, } from '../types/nip52' import { calendarTimeEventToActivity, @@ -28,7 +26,15 @@ export interface CalendarEventFilters { } /** - * Service for subscribing to and publishing NIP-52 Calendar Events via RelayHub. + * Service for subscribing to NIP-52 Calendar Events via RelayHub. + * + * Publishing kind-31922 calendar events lives server-side in the + * `aiolabs/events` LNbits extension (signer-abstraction branch, commit + * 66076d6) — `POST /events/api/v1/events` constructs and signs the + * event via NostrSigner and broadcasts it to the operator's configured + * relays. The webapp constructs only the request payload; see + * CreateActivityDialog for the flow. + * * Extends BaseService for standardized dependency injection and lifecycle. */ export class ActivitiesNostrService extends BaseService { @@ -105,32 +111,6 @@ export class ActivitiesNostrService extends BaseService { return activities } - /** - * Publish a NIP-52 time-based calendar event. - * Requires an authenticated user with a signing key. - */ - async publishCalendarEvent( - eventData: Partial, - signingKeyHex: string - ): Promise<{ success: number; total: number }> { - if (!this.relayHub) { - throw new Error('RelayHub not available') - } - - const tags = buildCalendarTimeEventTags(eventData) - const template: EventTemplate = { - kind: NIP52_KINDS.CALENDAR_TIME_EVENT, - created_at: Math.floor(Date.now() / 1000), - content: eventData.content ?? '', - tags, - } - - const privkeyBytes = hexToUint8Array(signingKeyHex) - const signedEvent = finalizeEvent(template, privkeyBytes) - - return await this.relayHub.publishEvent(signedEvent) - } - /** * Parse a raw Nostr event into an Activity view model. */ @@ -179,11 +159,3 @@ export class ActivitiesNostrService extends BaseService { this.activeUnsubscribes = [] } } - -function 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 -}