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:
parent
3bd87ee712
commit
c30e4ba6c5
2 changed files with 159 additions and 88 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue