Implement LNbits integration in AuthService and enhance ChatComponent for improved user experience
- Refactor AuthService to integrate LNbits authentication, including fetching user data from the API and handling token validation. - Update ChatComponent to reflect changes in peer management, replacing user_id with pubkey and username with name for better clarity. - Enhance connection status indicators in ChatComponent for improved user feedback during chat initialization.
This commit is contained in:
parent
d33d2abf8a
commit
daa9656680
4 changed files with 421 additions and 129 deletions
|
|
@ -1,7 +1,10 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { eventBus } from '@/core/event-bus'
|
||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import { nip04, getEventHash, 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'
|
||||
|
|
@ -10,10 +13,67 @@ export class ChatService {
|
|||
private messages = ref<Map<string, ChatMessage[]>>(new Map())
|
||||
private peers = ref<Map<string, ChatPeer>>(new Map())
|
||||
private config: ChatConfig
|
||||
private subscriptionUnsubscriber?: () => void
|
||||
private isInitialized = ref(false)
|
||||
|
||||
constructor(config: ChatConfig) {
|
||||
this.config = config
|
||||
this.loadPeersFromStorage()
|
||||
|
||||
// Defer initialization until services are available
|
||||
this.deferredInitialization()
|
||||
}
|
||||
|
||||
// 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<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.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<void> {
|
||||
// Set up real-time subscription
|
||||
this.setupMessageSubscription()
|
||||
|
||||
// Load message history for known peers
|
||||
await this.loadMessageHistory()
|
||||
}
|
||||
|
||||
// Computed properties
|
||||
|
|
@ -28,6 +88,10 @@ export class ChatService {
|
|||
})
|
||||
}
|
||||
|
||||
get isReady() {
|
||||
return computed(() => this.isInitialized.value)
|
||||
}
|
||||
|
||||
// Get messages for a specific peer
|
||||
getMessages(peerPubkey: string): ChatMessage[] {
|
||||
return this.messages.value.get(peerPubkey) || []
|
||||
|
|
@ -120,31 +184,70 @@ export class ChatService {
|
|||
}
|
||||
}
|
||||
|
||||
// Refresh peers from API
|
||||
async refreshPeers(): Promise<void> {
|
||||
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<void> {
|
||||
try {
|
||||
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
||||
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any
|
||||
const services = this.checkServicesAvailable()
|
||||
|
||||
if (!relayHub || !authService?.user?.value?.privkey) {
|
||||
throw new Error('Required services not available')
|
||||
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
|
||||
}
|
||||
|
||||
// Create message
|
||||
// Finalize the event with signature
|
||||
const signedEvent = finalizeEvent(eventTemplate, userPrivkey)
|
||||
|
||||
// Create local message for immediate display
|
||||
const message: ChatMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
id: signedEvent.id,
|
||||
content,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
created_at: signedEvent.created_at,
|
||||
sent: true,
|
||||
pubkey: authService.user.value.pubkey
|
||||
pubkey: userPubkey
|
||||
}
|
||||
|
||||
// Add to local messages immediately
|
||||
this.addMessage(peerPubkey, message)
|
||||
|
||||
// TODO: Implement actual Nostr message sending
|
||||
// This would involve encrypting the message and publishing to relays
|
||||
console.log('Sending message:', { peerPubkey, content })
|
||||
// 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)
|
||||
|
|
@ -209,6 +312,52 @@ export class ChatService {
|
|||
}
|
||||
}
|
||||
|
||||
// Load peers from API
|
||||
async loadPeersFromAPI(): Promise<void> {
|
||||
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)
|
||||
|
|
@ -232,8 +381,163 @@ export class ChatService {
|
|||
}
|
||||
}
|
||||
|
||||
// Load message history for known peers
|
||||
private async loadMessageHistory(): Promise<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 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
|
||||
const events = await relayHub.queryEvents([
|
||||
{
|
||||
kinds: [4],
|
||||
authors: [userPubkey, ...peerPubkeys], // Messages from us or peers
|
||||
'#p': [userPubkey], // Messages tagged with our pubkey
|
||||
limit: 100 // Limit to last 100 messages per conversation
|
||||
}
|
||||
])
|
||||
|
||||
console.log('Found', events.length, 'historical messages')
|
||||
|
||||
// Process historical messages
|
||||
for (const event of events) {
|
||||
try {
|
||||
const isFromUs = event.pubkey === userPubkey
|
||||
const peerPubkey = isFromUs
|
||||
? event.tags.find(tag => 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)
|
||||
|
||||
// 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 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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue