diff --git a/src/components/nostr/ChatComponent.vue b/src/components/nostr/ChatComponent.vue index 55d95a9..3b00cc2 100644 --- a/src/components/nostr/ChatComponent.vue +++ b/src/components/nostr/ChatComponent.vue @@ -279,6 +279,8 @@ import { Badge } from '@/components/ui/badge' import { ScrollArea } from '@/components/ui/scroll-area' import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar' import { useNostrChat } from '@/composables/useNostrChat' +import { getAuthToken } from '@/lib/config/lnbits' +import { config } from '@/lib/config' // Types interface Peer { @@ -336,24 +338,45 @@ const currentMessages = computed(() => { const loadPeers = async () => { try { isLoading.value = true - const response = await fetch('/users/api/v1/nostr/pubkeys', { + const authToken = getAuthToken() + if (!authToken) { + console.warn('No authentication token found - cannot load peers') + return + } + + 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 ${localStorage.getItem('admin_token')}` + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' } }) + console.log('Peers API Response status:', response.status) + if (!response.ok) { - throw new Error('Failed to load peers') + const errorText = await response.text() + console.error('Peers API Error:', response.status, errorText) + throw new Error(`Failed to load peers: ${response.status}`) } - const data = await response.json() - peers.value = data.map((peer: any) => ({ - user_id: peer.user_id, - username: peer.username, - pubkey: peer.pubkey - })) + const responseText = await response.text() + console.log('Peers API Response text:', responseText) - console.log(`Loaded ${peers.value.length} peers`) + try { + const data = JSON.parse(responseText) + peers.value = data.map((peer: any) => ({ + user_id: peer.user_id, + username: peer.username, + pubkey: peer.pubkey + })) + + console.log(`Loaded ${peers.value.length} peers`) + } catch (parseError) { + console.error('JSON Parse Error for peers:', parseError) + console.error('Response was:', responseText) + throw new Error('Invalid JSON response from peers API') + } } catch (error) { console.error('Failed to load peers:', error) } finally { diff --git a/src/composables/useNostrChat.ts b/src/composables/useNostrChat.ts index 4133fb2..d84856d 100644 --- a/src/composables/useNostrChat.ts +++ b/src/composables/useNostrChat.ts @@ -1,5 +1,9 @@ import { ref, computed, readonly } from 'vue' import { useNostrStore } from '@/stores/nostr' +import { SimplePool, nip04, finalizeEvent, type EventTemplate } from 'nostr-tools' +import { hexToBytes } from '@/lib/utils/crypto' +import { getAuthToken } from '@/lib/config/lnbits' +import { config } from '@/lib/config' // Types export interface ChatMessage { @@ -16,12 +20,19 @@ export interface NostrRelayConfig { write?: boolean } -// Default relays - you can configure these -const DEFAULT_RELAYS: NostrRelayConfig[] = [ - { url: 'wss://nostr.atitlan.io', read: true, write: true }, - { url: 'wss://relay.damus.io', read: true, write: true }, - { url: 'wss://nos.lol', read: true, write: true } -] +// Get relays from config or use defaults +const getRelays = (): NostrRelayConfig[] => { + const configuredRelays = config.nostr.relays + if (configuredRelays && configuredRelays.length > 0) { + return configuredRelays.map((url: string) => ({ url, read: true, write: true })) + } + + // Fallback relays if none configured + return [ + { url: 'wss://relay.damus.io', read: true, write: true }, + { url: 'wss://nos.lol', read: true, write: true } + ] +} export function useNostrChat() { const nostrStore = useNostrStore() @@ -30,31 +41,24 @@ export function useNostrChat() { const isConnected = ref(false) const messages = ref>(new Map()) const currentUser = ref<{ pubkey: string; prvkey: string } | null>(null) - const connectedRelays = ref([]) + const pool = ref(null) const processedMessageIds = ref(new Set()) // Computed const isLoggedIn = computed(() => !!currentUser.value) - // Initialize NostrTools - const waitForNostrTools = async (timeout = 5000): Promise => { - const start = Date.now() - - while (Date.now() - start < timeout) { - if (window.NostrTools) { - return - } - await new Promise(resolve => setTimeout(resolve, 100)) + // Initialize Nostr pool + const initializePool = () => { + if (!pool.value) { + pool.value = new SimplePool() } - - throw new Error('NostrTools failed to load within timeout period') } // Connect to relays const connectToRelay = async (url: string): Promise => { try { - const relay = window.NostrTools.relayInit(url) - await relay.connect() + initializePool() + const relay = pool.value!.ensureRelay(url) console.log(`Connected to relay: ${url}`) return relay } catch (error) { @@ -66,41 +70,42 @@ export function useNostrChat() { // Connect to all relays const connect = async () => { try { - await waitForNostrTools() - // Get current user from LNBits await loadCurrentUser() if (!currentUser.value) { - throw new Error('No user logged in') + console.warn('No user logged in - chat functionality will be limited') + // Don't throw error, just continue without user data + // The chat will still work for viewing messages, but sending will fail } + // Initialize pool + initializePool() + // Connect to relays + const relayConfigs = getRelays() const relays = await Promise.all( - DEFAULT_RELAYS.map(relay => connectToRelay(relay.url)) + relayConfigs.map(relay => connectToRelay(relay.url)) ) - connectedRelays.value = relays.filter(relay => relay !== null) - isConnected.value = true + const connectedRelays = relays.filter(relay => relay !== null) + isConnected.value = connectedRelays.length > 0 - console.log(`Connected to ${connectedRelays.value.length} relays`) + console.log(`Connected to ${connectedRelays.length} relays`) } catch (error) { console.error('Failed to connect:', error) - throw error + // Don't throw error, just log it and continue + // This allows the chat to still work for viewing messages } } // Disconnect from relays const disconnect = () => { - connectedRelays.value.forEach(relay => { - try { - relay.close() - } catch (error) { - console.error('Error closing relay:', error) - } - }) - - connectedRelays.value = [] + if (pool.value) { + const relayConfigs = getRelays() + pool.value.close(relayConfigs.map(r => r.url)) + pool.value = null + } isConnected.value = false messages.value.clear() processedMessageIds.value.clear() @@ -109,21 +114,41 @@ export function useNostrChat() { // Load current user from LNBits const loadCurrentUser = async () => { try { - // Get current user from LNBits API - const response = await fetch('/users/api/v1/user/me', { + // Get current user from LNBits API using the auth endpoint + 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/me`, { headers: { - 'Authorization': `Bearer ${localStorage.getItem('admin_token')}` + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' } }) + console.log('API Response status:', response.status) + console.log('API Response headers:', response.headers) + + const responseText = await response.text() + console.log('API Response text:', responseText) + if (response.ok) { - const user = await response.json() - currentUser.value = { - pubkey: user.pubkey, - prvkey: user.prvkey // This should be available if user is admin + try { + const user = JSON.parse(responseText) + currentUser.value = { + pubkey: user.pubkey, + prvkey: user.prvkey + } + } catch (parseError) { + console.error('JSON Parse Error:', parseError) + console.error('Response was:', responseText) + throw new Error('Invalid JSON response from API') } } else { - throw new Error('Failed to load current user') + console.error('API Error:', response.status, responseText) + throw new Error(`Failed to load current user: ${response.status}`) } } catch (error) { console.error('Failed to load current user:', error) @@ -133,15 +158,33 @@ export function useNostrChat() { // Subscribe to messages from a specific peer const subscribeToPeer = async (peerPubkey: string) => { - if (!currentUser.value || !isConnected.value) { - throw new Error('Not connected') + if (!currentUser.value) { + console.warn('No user logged in - cannot subscribe to peer messages') + return null + } + + // Check if we have a pool and are connected + if (!pool.value) { + console.warn('No pool available - initializing...') + initializePool() + } + + if (!isConnected.value) { + console.warn('Not connected to relays - attempting to connect...') + await connect() + } + + if (!pool.value) { + throw new Error('Failed to initialize Nostr pool') } const myPubkey = currentUser.value.pubkey // Subscribe to direct messages (kind 4) - connectedRelays.value.forEach(relay => { - const sub = relay.sub([ + const relayConfigs = getRelays() + const sub = pool.value.subscribeMany( + relayConfigs.map(r => r.url), + [ { kinds: [4], authors: [peerPubkey], @@ -152,12 +195,15 @@ export function useNostrChat() { authors: [myPubkey], '#p': [peerPubkey] } - ]) + ], + { + onevent(event) { + handleIncomingMessage(event, peerPubkey) + } + } + ) - sub.on('event', (event: any) => { - handleIncomingMessage(event, peerPubkey) - }) - }) + return sub } // Handle incoming message @@ -170,7 +216,7 @@ export function useNostrChat() { try { // Decrypt the message - const decryptedContent = await window.NostrTools.nip04.decrypt( + const decryptedContent = await nip04.decrypt( currentUser.value!.prvkey, event.pubkey, event.content @@ -205,46 +251,48 @@ export function useNostrChat() { // Send message to a peer const sendMessage = async (peerPubkey: string, content: string) => { - if (!currentUser.value || !isConnected.value) { - throw new Error('Not connected') + if (!currentUser.value) { + throw new Error('No user logged in - please authenticate first') + } + + // Check if we have a pool and are connected + if (!pool.value) { + console.warn('No pool available - initializing...') + initializePool() + } + + if (!isConnected.value) { + console.warn('Not connected to relays - attempting to connect...') + await connect() + } + + if (!pool.value) { + throw new Error('Failed to initialize Nostr pool') } try { // Encrypt the message - const encryptedContent = await window.NostrTools.nip04.encrypt( + const encryptedContent = await nip04.encrypt( currentUser.value.prvkey, peerPubkey, content ) - // Create the event - const event = { + // Create the event template + const eventTemplate: EventTemplate = { kind: 4, - pubkey: currentUser.value.pubkey, created_at: Math.floor(Date.now() / 1000), tags: [['p', peerPubkey]], content: encryptedContent } - // Sign the event - event.id = window.NostrTools.getEventHash(event) - event.sig = window.NostrTools.getSignature(event, currentUser.value.prvkey) + // Finalize the event (sign it) + const event = finalizeEvent(eventTemplate, hexToBytes(currentUser.value.prvkey)) // Publish to relays - const publishPromises = connectedRelays.value.map(relay => { - return new Promise((resolve, reject) => { - const pub = relay.publish(event) - - pub.on('ok', () => { - console.log('Message published successfully') - resolve() - }) - - pub.on('failed', (reason: string) => { - console.error('Failed to publish message:', reason) - reject(new Error(reason)) - }) - }) + const relayConfigs = getRelays() + const publishPromises = relayConfigs.map(relay => { + return pool.value!.publish([relay.url], event) }) await Promise.all(publishPromises)