import { ref, reactive } from 'vue' import { BaseService } from '@/core/base/BaseService' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import { finalizeEvent, type EventTemplate } from 'nostr-tools' import type { Event as NostrEvent } from 'nostr-tools' export interface ScheduledEvent { id: string pubkey: string created_at: number dTag: string // Unique identifier from 'd' tag title: string start: string // ISO date string (YYYY-MM-DD or ISO datetime) end?: string description?: string location?: string status: string eventType?: string // 'task' for completable events, 'announcement' for informational content: string tags: string[][] } export interface EventCompletion { id: string eventAddress: string // "31922:pubkey:d-tag" pubkey: string // Who completed it created_at: number completed: boolean completedAt?: number notes: string } export class ScheduledEventService extends BaseService { protected readonly metadata = { name: 'ScheduledEventService', version: '1.0.0', dependencies: [] } protected relayHub: any = null protected authService: any = null // Scheduled events state - indexed by event address private _scheduledEvents = reactive(new Map()) private _completions = reactive(new Map()) private _isLoading = ref(false) protected async onInitialize(): Promise { console.log('ScheduledEventService: Starting initialization...') this.relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) this.authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) if (!this.relayHub) { throw new Error('RelayHub service not available') } console.log('ScheduledEventService: Initialization complete') } /** * Handle incoming scheduled event (kind 31922) * Made public so FeedService can route kind 31922 events to this service */ public handleScheduledEvent(event: NostrEvent): void { try { // Extract event data from tags const dTag = event.tags.find(tag => tag[0] === 'd')?.[1] if (!dTag) { console.warn('Scheduled event missing d tag:', event.id) return } const title = event.tags.find(tag => tag[0] === 'title')?.[1] || 'Untitled Event' const start = event.tags.find(tag => tag[0] === 'start')?.[1] const end = event.tags.find(tag => tag[0] === 'end')?.[1] const description = event.tags.find(tag => tag[0] === 'description')?.[1] const location = event.tags.find(tag => tag[0] === 'location')?.[1] const status = event.tags.find(tag => tag[0] === 'status')?.[1] || 'pending' const eventType = event.tags.find(tag => tag[0] === 'event-type')?.[1] if (!start) { console.warn('Scheduled event missing start date:', event.id) return } // Create event address: "kind:pubkey:d-tag" const eventAddress = `31922:${event.pubkey}:${dTag}` const scheduledEvent: ScheduledEvent = { id: event.id, pubkey: event.pubkey, created_at: event.created_at, dTag, title, start, end, description, location, status, eventType, content: event.content, tags: event.tags } // Store or update the event (replaceable by d-tag) this._scheduledEvents.set(eventAddress, scheduledEvent) } catch (error) { console.error('Failed to handle scheduled event:', error) } } /** * Handle RSVP/completion event (kind 31925) * Made public so FeedService can route kind 31925 events to this service */ public handleCompletionEvent(event: NostrEvent): void { console.log('🔔 ScheduledEventService: Received completion event (kind 31925)', event.id) try { // Find the event being responded to const aTag = event.tags.find(tag => tag[0] === 'a')?.[1] if (!aTag) { console.warn('Completion event missing a tag:', event.id) return } const completed = event.tags.find(tag => tag[0] === 'completed')?.[1] === 'true' const completedAtTag = event.tags.find(tag => tag[0] === 'completed_at')?.[1] const completedAt = completedAtTag ? parseInt(completedAtTag) : undefined console.log('📋 Completion details:', { aTag, completed, pubkey: event.pubkey, eventId: event.id }) const completion: EventCompletion = { id: event.id, eventAddress: aTag, pubkey: event.pubkey, created_at: event.created_at, completed, completedAt, notes: event.content } // Store completion (most recent one wins) const existing = this._completions.get(aTag) if (!existing || event.created_at > existing.created_at) { this._completions.set(aTag, completion) console.log('✅ Stored completion for:', aTag, '- completed:', completed) } else { console.log('⏭️ Skipped older completion for:', aTag) } } catch (error) { console.error('Failed to handle completion event:', error) } } /** * Get all scheduled events */ getScheduledEvents(): ScheduledEvent[] { return Array.from(this._scheduledEvents.values()) } /** * Get events scheduled for a specific date (YYYY-MM-DD) */ getEventsForDate(date: string): ScheduledEvent[] { return this.getScheduledEvents().filter(event => { // Simple date matching (start date) // For ISO datetime strings, extract just the date part const eventDate = event.start.split('T')[0] return eventDate === date }) } /** * Get events for today */ getTodaysEvents(): ScheduledEvent[] { const today = new Date().toISOString().split('T')[0] return this.getEventsForDate(today) } /** * Get completion status for an event */ getCompletion(eventAddress: string): EventCompletion | undefined { return this._completions.get(eventAddress) } /** * Check if an event is completed */ isCompleted(eventAddress: string): boolean { const completion = this.getCompletion(eventAddress) return completion?.completed || false } /** * Mark an event as complete */ async completeEvent(event: ScheduledEvent, notes: string = ''): Promise { if (!this.authService?.isAuthenticated?.value) { throw new Error('Must be authenticated to complete events') } if (!this.relayHub?.isConnected) { throw new Error('Not connected to relays') } const userPrivkey = this.authService.user.value?.prvkey if (!userPrivkey) { throw new Error('User private key not available') } try { this._isLoading.value = true const eventAddress = `31922:${event.pubkey}:${event.dTag}` // Create RSVP/completion event (NIP-52) const eventTemplate: EventTemplate = { kind: 31925, // Calendar Event RSVP content: notes, tags: [ ['a', eventAddress], ['status', 'accepted'], ['completed', 'true'], ['completed_at', Math.floor(Date.now() / 1000).toString()] ], created_at: Math.floor(Date.now() / 1000) } // Sign the event const privkeyBytes = this.hexToUint8Array(userPrivkey) const signedEvent = finalizeEvent(eventTemplate, privkeyBytes) // Publish the completion console.log('📤 Publishing completion event (kind 31925) for:', eventAddress) const result = await this.relayHub.publishEvent(signedEvent) console.log('✅ Completion event published to', result.success, '/', result.total, 'relays') // Optimistically update local state console.log('🔄 Optimistically updating local state') this.handleCompletionEvent(signedEvent) } catch (error) { console.error('Failed to complete event:', error) throw error } finally { this._isLoading.value = false } } /** * Uncomplete an event (publish new RSVP with completed=false) */ async uncompleteEvent(event: ScheduledEvent): Promise { if (!this.authService?.isAuthenticated?.value) { throw new Error('Must be authenticated to uncomplete events') } if (!this.relayHub?.isConnected) { throw new Error('Not connected to relays') } const userPrivkey = this.authService.user.value?.prvkey if (!userPrivkey) { throw new Error('User private key not available') } try { this._isLoading.value = true const eventAddress = `31922:${event.pubkey}:${event.dTag}` // Create RSVP event with completed=false const eventTemplate: EventTemplate = { kind: 31925, content: '', tags: [ ['a', eventAddress], ['status', 'tentative'], ['completed', 'false'] ], created_at: Math.floor(Date.now() / 1000) } // Sign the event const privkeyBytes = this.hexToUint8Array(userPrivkey) const signedEvent = finalizeEvent(eventTemplate, privkeyBytes) // Publish the uncomplete await this.relayHub.publishEvent(signedEvent) // Optimistically update local state this.handleCompletionEvent(signedEvent) } catch (error) { console.error('Failed to uncomplete event:', error) throw error } finally { this._isLoading.value = false } } /** * 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 } /** * Get all scheduled events */ get scheduledEvents(): Map { return this._scheduledEvents } /** * Get all completions */ get completions(): Map { return this._completions } /** * Check if currently loading */ get isLoading(): boolean { return this._isLoading.value } /** * Cleanup */ protected async onDestroy(): Promise { this._scheduledEvents.clear() this._completions.clear() } }