feat: Enhance Nostr chat functionality with improved authentication and error handling

- Integrate authentication token retrieval for loading peers and current user data.
- Update API endpoints to use a configurable base URL for better flexibility.
- Implement enhanced error handling for API responses, including JSON parsing and logging.
- Refactor relay connection logic to utilize a SimplePool for managing multiple relays efficiently.
- Improve user feedback with console logs for connection status and error details.
This commit is contained in:
padreug 2025-08-05 23:32:36 +02:00
parent 3bd87ee712
commit c30e4ba6c5
2 changed files with 159 additions and 88 deletions

View file

@ -279,6 +279,8 @@ import { Badge } from '@/components/ui/badge'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar' import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
import { useNostrChat } from '@/composables/useNostrChat' import { useNostrChat } from '@/composables/useNostrChat'
import { getAuthToken } from '@/lib/config/lnbits'
import { config } from '@/lib/config'
// Types // Types
interface Peer { interface Peer {
@ -336,17 +338,33 @@ const currentMessages = computed(() => {
const loadPeers = async () => { const loadPeers = async () => {
try { try {
isLoading.value = true 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: { 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) { 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() const responseText = await response.text()
console.log('Peers API Response text:', responseText)
try {
const data = JSON.parse(responseText)
peers.value = data.map((peer: any) => ({ peers.value = data.map((peer: any) => ({
user_id: peer.user_id, user_id: peer.user_id,
username: peer.username, username: peer.username,
@ -354,6 +372,11 @@ const loadPeers = async () => {
})) }))
console.log(`Loaded ${peers.value.length} peers`) 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) { } catch (error) {
console.error('Failed to load peers:', error) console.error('Failed to load peers:', error)
} finally { } finally {

View file

@ -1,5 +1,9 @@
import { ref, computed, readonly } from 'vue' import { ref, computed, readonly } from 'vue'
import { useNostrStore } from '@/stores/nostr' 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 // Types
export interface ChatMessage { export interface ChatMessage {
@ -16,12 +20,19 @@ export interface NostrRelayConfig {
write?: boolean write?: boolean
} }
// Default relays - you can configure these // Get relays from config or use defaults
const DEFAULT_RELAYS: NostrRelayConfig[] = [ const getRelays = (): NostrRelayConfig[] => {
{ url: 'wss://nostr.atitlan.io', read: true, write: true }, 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://relay.damus.io', read: true, write: true },
{ url: 'wss://nos.lol', read: true, write: true } { url: 'wss://nos.lol', read: true, write: true }
] ]
}
export function useNostrChat() { export function useNostrChat() {
const nostrStore = useNostrStore() const nostrStore = useNostrStore()
@ -30,31 +41,24 @@ export function useNostrChat() {
const isConnected = ref(false) const isConnected = ref(false)
const messages = ref<Map<string, ChatMessage[]>>(new Map()) const messages = ref<Map<string, ChatMessage[]>>(new Map())
const currentUser = ref<{ pubkey: string; prvkey: string } | null>(null) const currentUser = ref<{ pubkey: string; prvkey: string } | null>(null)
const connectedRelays = ref<any[]>([]) const pool = ref<SimplePool | null>(null)
const processedMessageIds = ref(new Set<string>()) const processedMessageIds = ref(new Set<string>())
// Computed // Computed
const isLoggedIn = computed(() => !!currentUser.value) const isLoggedIn = computed(() => !!currentUser.value)
// Initialize NostrTools // Initialize Nostr pool
const waitForNostrTools = async (timeout = 5000): Promise<void> => { const initializePool = () => {
const start = Date.now() if (!pool.value) {
pool.value = new SimplePool()
while (Date.now() - start < timeout) {
if (window.NostrTools) {
return
} }
await new Promise(resolve => setTimeout(resolve, 100))
}
throw new Error('NostrTools failed to load within timeout period')
} }
// Connect to relays // Connect to relays
const connectToRelay = async (url: string): Promise<any> => { const connectToRelay = async (url: string): Promise<any> => {
try { try {
const relay = window.NostrTools.relayInit(url) initializePool()
await relay.connect() const relay = pool.value!.ensureRelay(url)
console.log(`Connected to relay: ${url}`) console.log(`Connected to relay: ${url}`)
return relay return relay
} catch (error) { } catch (error) {
@ -66,41 +70,42 @@ export function useNostrChat() {
// Connect to all relays // Connect to all relays
const connect = async () => { const connect = async () => {
try { try {
await waitForNostrTools()
// Get current user from LNBits // Get current user from LNBits
await loadCurrentUser() await loadCurrentUser()
if (!currentUser.value) { 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 // Connect to relays
const relayConfigs = getRelays()
const relays = await Promise.all( 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) const connectedRelays = relays.filter(relay => relay !== null)
isConnected.value = true isConnected.value = connectedRelays.length > 0
console.log(`Connected to ${connectedRelays.value.length} relays`) console.log(`Connected to ${connectedRelays.length} relays`)
} catch (error) { } catch (error) {
console.error('Failed to connect:', 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 // Disconnect from relays
const disconnect = () => { const disconnect = () => {
connectedRelays.value.forEach(relay => { if (pool.value) {
try { const relayConfigs = getRelays()
relay.close() pool.value.close(relayConfigs.map(r => r.url))
} catch (error) { pool.value = null
console.error('Error closing relay:', error)
} }
})
connectedRelays.value = []
isConnected.value = false isConnected.value = false
messages.value.clear() messages.value.clear()
processedMessageIds.value.clear() processedMessageIds.value.clear()
@ -109,21 +114,41 @@ export function useNostrChat() {
// Load current user from LNBits // Load current user from LNBits
const loadCurrentUser = async () => { const loadCurrentUser = async () => {
try { try {
// Get current user from LNBits API // Get current user from LNBits API using the auth endpoint
const response = await fetch('/users/api/v1/user/me', { 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: { 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) { if (response.ok) {
const user = await response.json() try {
const user = JSON.parse(responseText)
currentUser.value = { currentUser.value = {
pubkey: user.pubkey, pubkey: user.pubkey,
prvkey: user.prvkey // This should be available if user is admin 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 { } 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) { } catch (error) {
console.error('Failed to load current user:', error) console.error('Failed to load current user:', error)
@ -133,15 +158,33 @@ export function useNostrChat() {
// Subscribe to messages from a specific peer // Subscribe to messages from a specific peer
const subscribeToPeer = async (peerPubkey: string) => { const subscribeToPeer = async (peerPubkey: string) => {
if (!currentUser.value || !isConnected.value) { if (!currentUser.value) {
throw new Error('Not connected') 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 const myPubkey = currentUser.value.pubkey
// Subscribe to direct messages (kind 4) // Subscribe to direct messages (kind 4)
connectedRelays.value.forEach(relay => { const relayConfigs = getRelays()
const sub = relay.sub([ const sub = pool.value.subscribeMany(
relayConfigs.map(r => r.url),
[
{ {
kinds: [4], kinds: [4],
authors: [peerPubkey], authors: [peerPubkey],
@ -152,12 +195,15 @@ export function useNostrChat() {
authors: [myPubkey], authors: [myPubkey],
'#p': [peerPubkey] '#p': [peerPubkey]
} }
]) ],
{
sub.on('event', (event: any) => { onevent(event) {
handleIncomingMessage(event, peerPubkey) handleIncomingMessage(event, peerPubkey)
}) }
}) }
)
return sub
} }
// Handle incoming message // Handle incoming message
@ -170,7 +216,7 @@ export function useNostrChat() {
try { try {
// Decrypt the message // Decrypt the message
const decryptedContent = await window.NostrTools.nip04.decrypt( const decryptedContent = await nip04.decrypt(
currentUser.value!.prvkey, currentUser.value!.prvkey,
event.pubkey, event.pubkey,
event.content event.content
@ -205,46 +251,48 @@ export function useNostrChat() {
// Send message to a peer // Send message to a peer
const sendMessage = async (peerPubkey: string, content: string) => { const sendMessage = async (peerPubkey: string, content: string) => {
if (!currentUser.value || !isConnected.value) { if (!currentUser.value) {
throw new Error('Not connected') 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 { try {
// Encrypt the message // Encrypt the message
const encryptedContent = await window.NostrTools.nip04.encrypt( const encryptedContent = await nip04.encrypt(
currentUser.value.prvkey, currentUser.value.prvkey,
peerPubkey, peerPubkey,
content content
) )
// Create the event // Create the event template
const event = { const eventTemplate: EventTemplate = {
kind: 4, kind: 4,
pubkey: currentUser.value.pubkey,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: [['p', peerPubkey]], tags: [['p', peerPubkey]],
content: encryptedContent content: encryptedContent
} }
// Sign the event // Finalize the event (sign it)
event.id = window.NostrTools.getEventHash(event) const event = finalizeEvent(eventTemplate, hexToBytes(currentUser.value.prvkey))
event.sig = window.NostrTools.getSignature(event, currentUser.value.prvkey)
// Publish to relays // Publish to relays
const publishPromises = connectedRelays.value.map(relay => { const relayConfigs = getRelays()
return new Promise<void>((resolve, reject) => { const publishPromises = relayConfigs.map(relay => {
const pub = relay.publish(event) return pool.value!.publish([relay.url], 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))
})
})
}) })
await Promise.all(publishPromises) await Promise.all(publishPromises)