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 { 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,17 +338,33 @@ 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()
const responseText = await response.text()
console.log('Peers API Response text:', responseText)
try {
const data = JSON.parse(responseText)
peers.value = data.map((peer: any) => ({
user_id: peer.user_id,
username: peer.username,
@ -354,6 +372,11 @@ const loadPeers = async () => {
}))
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 {

View file

@ -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 },
// 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<Map<string, ChatMessage[]>>(new Map())
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>())
// Computed
const isLoggedIn = computed(() => !!currentUser.value)
// Initialize NostrTools
const waitForNostrTools = async (timeout = 5000): Promise<void> => {
const start = Date.now()
while (Date.now() - start < timeout) {
if (window.NostrTools) {
return
// Initialize Nostr pool
const initializePool = () => {
if (!pool.value) {
pool.value = new SimplePool()
}
await new Promise(resolve => setTimeout(resolve, 100))
}
throw new Error('NostrTools failed to load within timeout period')
}
// Connect to relays
const connectToRelay = async (url: string): Promise<any> => {
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)
if (pool.value) {
const relayConfigs = getRelays()
pool.value.close(relayConfigs.map(r => r.url))
pool.value = null
}
})
connectedRelays.value = []
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()
try {
const user = JSON.parse(responseText)
currentUser.value = {
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 {
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]
}
])
sub.on('event', (event: any) => {
],
{
onevent(event) {
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<void>((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)