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 { 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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue