import { ref, computed } from 'vue' import { eventBus } from '@/core/event-bus' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import { nip04, finalizeEvent, type Event, type EventTemplate } from 'nostr-tools' import type { ChatMessage, ChatPeer, UnreadMessageData, ChatConfig } from '../types' import { getAuthToken } from '@/lib/config/lnbits' import { config } from '@/lib/config' const UNREAD_MESSAGES_KEY = 'nostr-chat-unread-messages' const PEERS_KEY = 'nostr-chat-peers' export class ChatService { private messages = ref>(new Map()) private peers = ref>(new Map()) private config: ChatConfig private subscriptionUnsubscriber?: () => void private isInitialized = ref(false) private marketMessageHandler?: (event: any) => Promise constructor(config: ChatConfig) { this.config = config this.loadPeersFromStorage() // Defer initialization until services are available this.deferredInitialization() } // Register market message handler for forwarding market-related DMs setMarketMessageHandler(handler: (event: any) => Promise) { this.marketMessageHandler = handler } // Defer initialization until services are ready private deferredInitialization(): void { // Try initialization immediately this.tryInitialization() // Also listen for auth events to re-initialize when user logs in eventBus.on('auth:login', () => { console.log('💬 Auth login detected, initializing chat...') this.tryInitialization() }) } // Try to initialize services if they're available private async tryInitialization(): Promise { try { const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any if (!relayHub || !authService?.user?.value?.pubkey) { console.log('💬 Services not ready yet, will retry when auth completes...') return } console.log('💬 Services ready, initializing chat functionality...') // Load peers from API await this.loadPeersFromAPI().catch(error => { console.warn('Failed to load peers from API:', error) }) // Initialize message handling (subscription + history loading) await this.initializeMessageHandling() // Mark as initialized this.isInitialized.value = true console.log('💬 Chat service fully initialized and ready!') } catch (error) { console.error('💬 Failed to initialize chat:', error) this.isInitialized.value = false } } // Initialize message handling (subscription + history loading) async initializeMessageHandling(): Promise { // Set up real-time subscription this.setupMessageSubscription() // Load message history for known peers await this.loadMessageHistory() } // Computed properties get allPeers() { return computed(() => Array.from(this.peers.value.values())) } get totalUnreadCount() { return computed(() => { return Array.from(this.peers.value.values()) .reduce((total, peer) => total + peer.unreadCount, 0) }) } get isReady() { return computed(() => this.isInitialized.value) } // Get messages for a specific peer getMessages(peerPubkey: string): ChatMessage[] { return this.messages.value.get(peerPubkey) || [] } // Get peer by pubkey getPeer(pubkey: string): ChatPeer | undefined { return this.peers.value.get(pubkey) } // Add or update a peer addPeer(pubkey: string, name?: string): ChatPeer { let peer = this.peers.value.get(pubkey) if (!peer) { peer = { pubkey, name: name || `User ${pubkey.slice(0, 8)}`, unreadCount: 0, lastSeen: Date.now() } this.peers.value.set(pubkey, peer) this.savePeersToStorage() eventBus.emit('chat:peer-added', { peer }, 'chat-service') } else if (name && name !== peer.name) { peer.name = name this.savePeersToStorage() } return peer } // Add a message addMessage(peerPubkey: string, message: ChatMessage): void { if (!this.messages.value.has(peerPubkey)) { this.messages.value.set(peerPubkey, []) } const peerMessages = this.messages.value.get(peerPubkey)! // Avoid duplicates if (!peerMessages.some(m => m.id === message.id)) { peerMessages.push(message) // Sort by timestamp peerMessages.sort((a, b) => a.created_at - b.created_at) // Limit message count if (peerMessages.length > this.config.maxMessages) { peerMessages.splice(0, peerMessages.length - this.config.maxMessages) } // Update peer info const peer = this.addPeer(peerPubkey) peer.lastMessage = message peer.lastSeen = Date.now() // Update unread count if message is not sent by us if (!message.sent) { this.updateUnreadCount(peerPubkey, message) } // Emit events const eventType = message.sent ? 'chat:message-sent' : 'chat:message-received' eventBus.emit(eventType, { message, peerPubkey }, 'chat-service') } } // Mark messages as read for a peer markAsRead(peerPubkey: string): void { const peer = this.peers.value.get(peerPubkey) if (peer && peer.unreadCount > 0) { peer.unreadCount = 0 // Save unread state const unreadData: UnreadMessageData = { lastReadTimestamp: Date.now(), unreadCount: 0, processedMessageIds: new Set() } this.saveUnreadData(peerPubkey, unreadData) eventBus.emit('chat:unread-count-changed', { peerPubkey, count: 0, totalUnread: this.totalUnreadCount.value }, 'chat-service') } } // Refresh peers from API async refreshPeers(): Promise { return this.loadPeersFromAPI() } // Check if services are available for messaging private checkServicesAvailable(): { relayHub: any; authService: any } | null { const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any if (!relayHub || !authService?.user?.value?.prvkey) { return null } if (!relayHub.isConnected) { return null } return { relayHub, authService } } // Send a message async sendMessage(peerPubkey: string, content: string): Promise { try { const services = this.checkServicesAvailable() if (!services) { throw new Error('Chat services not ready. Please wait for connection to establish.') } const { relayHub, authService } = services const userPrivkey = authService.user.value.prvkey const userPubkey = authService.user.value.pubkey // Encrypt the message using NIP-04 const encryptedContent = await nip04.encrypt(userPrivkey, peerPubkey, content) // Create Nostr event for the encrypted message (kind 4 = encrypted direct message) const eventTemplate: EventTemplate = { kind: 4, created_at: Math.floor(Date.now() / 1000), tags: [['p', peerPubkey]], content: encryptedContent } // Finalize the event with signature const signedEvent = finalizeEvent(eventTemplate, userPrivkey) // Create local message for immediate display const message: ChatMessage = { id: signedEvent.id, content, created_at: signedEvent.created_at, sent: true, pubkey: userPubkey } // Add to local messages immediately this.addMessage(peerPubkey, message) // Publish to Nostr relays const result = await relayHub.publishEvent(signedEvent) console.log('Message published to relays:', { success: result.success, total: result.total }) } catch (error) { console.error('Failed to send message:', error) throw error } } // Private methods private updateUnreadCount(peerPubkey: string, message: ChatMessage): void { const unreadData = this.getUnreadData(peerPubkey) if (!unreadData.processedMessageIds.has(message.id)) { unreadData.processedMessageIds.add(message.id) unreadData.unreadCount++ const peer = this.peers.value.get(peerPubkey) if (peer) { peer.unreadCount = unreadData.unreadCount this.savePeersToStorage() } this.saveUnreadData(peerPubkey, unreadData) eventBus.emit('chat:unread-count-changed', { peerPubkey, count: unreadData.unreadCount, totalUnread: this.totalUnreadCount.value }, 'chat-service') } } private getUnreadData(peerPubkey: string): UnreadMessageData { try { const stored = localStorage.getItem(`${UNREAD_MESSAGES_KEY}-${peerPubkey}`) if (stored) { const data = JSON.parse(stored) return { ...data, processedMessageIds: new Set(data.processedMessageIds || []) } } } catch (error) { console.warn('Failed to load unread data for peer:', peerPubkey, error) } return { lastReadTimestamp: 0, unreadCount: 0, processedMessageIds: new Set() } } private saveUnreadData(peerPubkey: string, data: UnreadMessageData): void { try { const serializable = { ...data, processedMessageIds: Array.from(data.processedMessageIds) } localStorage.setItem(`${UNREAD_MESSAGES_KEY}-${peerPubkey}`, JSON.stringify(serializable)) } catch (error) { console.warn('Failed to save unread data for peer:', peerPubkey, error) } } // Load peers from API async loadPeersFromAPI(): Promise { try { const authToken = getAuthToken() if (!authToken) { throw new Error('No authentication token found') } const API_BASE_URL = config.api.baseUrl || 'http://localhost:5006' const response = await fetch(`${API_BASE_URL}/api/v1/auth/nostr/pubkeys`, { headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json' } }) if (!response.ok) { throw new Error(`Failed to load peers: ${response.status}`) } const data = await response.json() // Clear existing peers and load from API this.peers.value.clear() data.forEach((peer: any) => { const chatPeer: ChatPeer = { pubkey: peer.pubkey, name: peer.username || `User ${peer.pubkey.slice(0, 8)}`, unreadCount: 0, lastSeen: Date.now() } this.peers.value.set(peer.pubkey, chatPeer) }) // Save to storage this.savePeersToStorage() console.log(`Loaded ${data.length} peers from API`) } catch (error) { console.error('Failed to load peers from API:', error) throw error } } private loadPeersFromStorage(): void { try { const stored = localStorage.getItem(PEERS_KEY) if (stored) { const peersArray = JSON.parse(stored) as ChatPeer[] peersArray.forEach(peer => { this.peers.value.set(peer.pubkey, peer) }) } } catch (error) { console.warn('Failed to load peers from storage:', error) } } private savePeersToStorage(): void { try { const peersArray = Array.from(this.peers.value.values()) localStorage.setItem(PEERS_KEY, JSON.stringify(peersArray)) } catch (error) { console.warn('Failed to save peers to storage:', error) } } // Load message history for known peers private async loadMessageHistory(): Promise { try { const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any if (!relayHub || !authService?.user?.value?.pubkey) { console.warn('Cannot load message history: missing services') return } const userPubkey = authService.user.value.pubkey const userPrivkey = authService.user.value.prvkey const peerPubkeys = Array.from(this.peers.value.keys()) if (peerPubkeys.length === 0) { console.log('No peers to load message history for') return } console.log('Loading message history for', peerPubkeys.length, 'peers') // Query historical messages (kind 4) to/from known peers // We need separate queries for sent vs received messages due to different tagging const receivedEvents = await relayHub.queryEvents([ { kinds: [4], authors: peerPubkeys, // Messages from peers '#p': [userPubkey], // Messages tagged to us limit: 100 } ]) const sentEvents = await relayHub.queryEvents([ { kinds: [4], authors: [userPubkey], // Messages from us '#p': peerPubkeys, // Messages tagged to peers limit: 100 } ]) const events = [...receivedEvents, ...sentEvents] .sort((a, b) => a.created_at - b.created_at) // Sort by timestamp console.log('Found', events.length, 'historical messages:', receivedEvents.length, 'received,', sentEvents.length, 'sent') // Process historical messages for (const event of events) { try { const isFromUs = event.pubkey === userPubkey const peerPubkey = isFromUs ? event.tags.find((tag: string[]) => tag[0] === 'p')?.[1] // Get recipient from tag : event.pubkey // Sender is the peer if (!peerPubkey || peerPubkey === userPubkey) continue // Decrypt the message const decryptedContent = await nip04.decrypt(userPrivkey, peerPubkey, event.content) // Create a chat message const message: ChatMessage = { id: event.id, content: decryptedContent, created_at: event.created_at, sent: isFromUs, pubkey: event.pubkey } // Add the message (will avoid duplicates) this.addMessage(peerPubkey, message) } catch (error) { console.error('Failed to decrypt historical message:', error) } } console.log('Message history loaded successfully') } catch (error) { console.error('Failed to load message history:', error) } } // Setup subscription for incoming messages private setupMessageSubscription(): void { try { const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any if (!relayHub || !authService?.user?.value?.pubkey) { console.warn('💬 Cannot setup message subscription: missing services') return } if (!relayHub.isConnected) { console.warn('💬 RelayHub not connected, waiting for connection...') // Listen for connection event relayHub.on('connected', () => { console.log('💬 RelayHub connected, setting up message subscription...') this.setupMessageSubscription() }) return } const userPubkey = authService.user.value.pubkey const userPrivkey = authService.user.value.prvkey // Subscribe to encrypted direct messages (kind 4) addressed to this user this.subscriptionUnsubscriber = relayHub.subscribe({ id: 'chat-messages', filters: [ { kinds: [4], // Encrypted direct messages '#p': [userPubkey] // Messages tagged with our pubkey } ], onEvent: async (event: Event) => { try { // Find the sender's pubkey from the event const senderPubkey = event.pubkey // Skip our own messages if (senderPubkey === userPubkey) { return } // Decrypt the message const decryptedContent = await nip04.decrypt(userPrivkey, senderPubkey, event.content) // Check if this is a market-related message (JSON with type field) let isMarketMessage = false try { const parsedContent = JSON.parse(decryptedContent) if (parsedContent && typeof parsedContent.type === 'number' && (parsedContent.type === 1 || parsedContent.type === 2)) { // This is a market message (payment request type 1 or status update type 2) isMarketMessage = true console.log('🛒 Forwarding market message to market handler:', parsedContent.type) // Forward to market handler if (this.marketMessageHandler) { await this.marketMessageHandler(event) } else { console.warn('Market message handler not available, message will be treated as chat') } } } catch (e) { // Not JSON or not a market message, treat as regular chat } // Only process as chat message if it's not a market message if (!isMarketMessage) { // Create a chat message const message: ChatMessage = { id: event.id, content: decryptedContent, created_at: event.created_at, sent: false, pubkey: senderPubkey } // Ensure we have a peer record for the sender this.addPeer(senderPubkey) // Add the message this.addMessage(senderPubkey, message) console.log('Received encrypted chat message from:', senderPubkey.slice(0, 8)) } } catch (error) { console.error('Failed to decrypt incoming message:', error) } }, onEose: () => { console.log('Chat message subscription EOSE received') } }) console.log('Chat message subscription set up successfully') } catch (error) { console.error('Failed to setup message subscription:', error) } } // Cleanup destroy(): void { // Unsubscribe from message subscription if (this.subscriptionUnsubscriber) { this.subscriptionUnsubscriber() } this.messages.value.clear() this.peers.value.clear() } }