basic relay managment system working

This commit is contained in:
DocNR 2025-03-09 12:48:24 -04:00
parent 07fada6d07
commit 4cd62cf775
5 changed files with 573 additions and 411 deletions

View File

@ -1,6 +1,6 @@
// app/(tabs)/library/programs.tsx
import React, { useState, useEffect } from 'react';
import { View, ScrollView, TextInput, ActivityIndicator, Platform, TouchableOpacity } from 'react-native';
import { View, ScrollView, TextInput, ActivityIndicator, Platform, Alert, TouchableOpacity } from 'react-native';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@ -17,6 +17,7 @@ import { FilterSheet, type FilterOptions, type SourceType } from '@/components/l
import { Separator } from '@/components/ui/separator';
import { NostrEventKind } from '@/types/nostr';
import { useNDKStore } from '@/lib/stores/ndk';
import * as SecureStore from 'expo-secure-store';
// Define relay status
enum RelayStatus {
@ -150,34 +151,84 @@ export default function ProgramsScreen() {
const resetDatabase = async () => {
try {
await db.withTransactionAsync(async () => {
// Drop all tables
const tables = await db.getAllAsync<{ name: string }>(
setTestResults(null);
// Clear stored keys first
try {
await SecureStore.deleteItemAsync('nostr_privkey');
console.log('[Database Reset] Cleared stored Nostr keys');
} catch (keyError) {
console.warn('[Database Reset] Error clearing keys:', keyError);
}
// Define explicit type for tables
let tables: { name: string }[] = [];
// Try to get existing tables
try {
tables = await db.getAllAsync<{ name: string }>(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
);
for (const { name } of tables) {
await db.execAsync(`DROP TABLE IF EXISTS ${name}`);
console.log(`[Database Reset] Found ${tables.length} tables to drop`);
} catch (tableListError) {
console.warn('[Database Reset] Error listing tables:', tableListError);
// Initialize with empty array if query fails
tables = [];
}
// Drop tables one by one
for (const table of tables) {
try {
await db.execAsync(`DROP TABLE IF EXISTS "${table.name}";`);
console.log(`[Database Reset] Dropped table: ${table.name}`);
} catch (dropError) {
console.error(`[Database Reset] Error dropping table ${table.name}:`, dropError);
}
// Recreate schema
await schema.createTables(db);
});
}
// Use a delay to allow any pending operations to complete
await new Promise(resolve => setTimeout(resolve, 1000));
// Create a completely new database instance instead of using the existing one
// This will bypass the "Access to closed resource" issue
Alert.alert(
'Database Tables Dropped',
'All database tables have been dropped. The app needs to be restarted to complete the reset process.',
[
{
text: 'Restart Now',
style: 'destructive',
onPress: () => {
// In a production app, you would use something like RN's DevSettings.reload()
// For Expo, we'll suggest manual restart
Alert.alert(
'Manual Restart Required',
'Please completely close the app and reopen it to finish the database reset.',
[{ text: 'OK', style: 'default' }]
);
}
}
]
);
setTestResults({
success: true,
message: 'Database reset successfully'
message: 'Database tables dropped. Please restart the app to complete the reset.'
});
// Refresh database status
checkDatabase();
inspectDatabase();
} catch (error) {
console.error('Error resetting database:', error);
console.error('[Database Reset] Error resetting database:', error);
setTestResults({
success: false,
message: error instanceof Error ? error.message : 'Unknown error during reset'
message: error instanceof Error ? error.message : 'Unknown error during database reset'
});
// Still recommend a restart since the database might be in an inconsistent state
Alert.alert(
'Database Reset Error',
'There was an error during database reset. Please restart the app and try again.',
[{ text: 'OK', style: 'default' }]
);
}
};

View File

@ -1,217 +1,92 @@
// components/RelayManagement.tsx
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, TextInput, ActivityIndicator, Alert, TouchableOpacity, Modal, ScrollView } from 'react-native';
import { Switch } from 'react-native-gesture-handler'; // Or your UI library's Switch component
import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, TouchableOpacity, Modal, ActivityIndicator } from 'react-native';
import { useRelayStore } from '@/lib/stores/relayStore';
import { useNDKStore } from '@/lib/stores/ndk';
import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { X } from 'lucide-react-native';
import { RelayWithStatus } from '@/lib/db/services/RelayService';
interface Props {
// Define proper interface for component props
interface RelayManagementProps {
isVisible: boolean;
onClose: () => void;
}
export default function RelayManagement({ isVisible, onClose }: Props) {
// Get relay state and actions from the store
// Simple RelayManagement component
export default function RelayManagement({ isVisible, onClose }: RelayManagementProps) {
const relays = useRelayStore(state => state.relays);
const isLoading = useRelayStore(state => state.isLoading);
const isRefreshing = useRelayStore(state => state.isRefreshing);
const isSaving = useRelayStore(state => state.isSaving);
const loadRelays = useRelayStore(state => state.loadRelays);
const addRelay = useRelayStore(state => state.addRelay);
const removeRelay = useRelayStore(state => state.removeRelay);
const updateRelay = useRelayStore(state => state.updateRelay);
const applyChanges = useRelayStore(state => state.applyChanges);
const resetToDefaults = useRelayStore(state => state.resetToDefaults);
// Get current user for import/export functions
const { ndk, currentUser } = useNDKStore();
// Local state
const [newRelayUrl, setNewRelayUrl] = useState('');
const [isAddingRelay, setIsAddingRelay] = useState(false);
// Load relays when component mounts or becomes visible
useEffect(() => {
if (isVisible) {
console.log('[RelayManagement] Component became visible, loading relays');
loadRelays();
}
}, [isVisible, loadRelays]);
// Debug logging
useEffect(() => {
if (isVisible) {
console.log('[RelayManagement] Component state:', {
relaysCount: relays.length,
isLoading,
isRefreshing,
isAddingRelay
});
// Log the first relay for inspection
if (relays.length > 0) {
console.log('[RelayManagement] First relay:', relays[0]);
} else {
console.log('[RelayManagement] No relays loaded');
}
}
}, [isVisible, relays, isLoading, isRefreshing, isAddingRelay]);
}, [isVisible]);
// Function to add a new relay
const handleAddRelay = async () => {
if (!newRelayUrl || !newRelayUrl.startsWith('wss://')) {
Alert.alert('Invalid Relay URL', 'Relay URL must start with wss://');
return;
}
try {
await addRelay(newRelayUrl);
setNewRelayUrl('');
setIsAddingRelay(false);
} catch (error) {
Alert.alert('Error', error instanceof Error ? error.message : 'Failed to add relay');
}
};
// Function to handle relay removal with confirmation
const handleRemoveRelay = (url: string) => {
Alert.alert(
'Remove Relay',
`Are you sure you want to remove ${url}?`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Remove',
style: 'destructive',
onPress: async () => {
try {
await removeRelay(url);
} catch (error) {
Alert.alert('Error', 'Failed to remove relay');
}
}
}
]
);
};
// Function to toggle read/write permission
const handleTogglePermission = (url: string, permission: 'read' | 'write') => {
const relay = relays.find(r => r.url === url);
if (relay) {
console.log(`[RelayManagement] Toggling ${permission} for relay ${url}`);
updateRelay(url, { [permission]: !relay[permission] });
}
};
// Function to apply changes
const handleApplyChanges = async () => {
try {
console.log('[RelayManagement] Applying changes...');
const success = await applyChanges();
if (success) {
Alert.alert('Success', 'Relay configuration applied successfully!');
}
} catch (error) {
Alert.alert('Error', 'Failed to apply relay configuration');
}
};
// Function to reset to defaults with confirmation
const handleResetToDefaults = () => {
Alert.alert(
'Reset to Defaults',
'Are you sure you want to reset all relays to the default configuration?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Reset',
style: 'destructive',
onPress: async () => {
try {
await resetToDefaults();
Alert.alert('Success', 'Relays reset to defaults');
} catch (error) {
Alert.alert('Error', 'Failed to reset relays');
}
}
}
]
);
};
// Function to get color based on relay status
const getStatusColor = (status: string) => {
// Status indicator color with proper typing
const getStatusColor = (status: string): string => {
switch (status) {
case 'connected': return '#10b981'; // Green
case 'connecting': return '#f59e0b'; // Amber
case 'error': return '#ef4444'; // Red
case 'error':
case 'disconnected': return '#ef4444'; // Red
default: return '#6b7280'; // Gray
}
};
// Render a relay item
const renderRelayItem = ({ item }: { item: RelayWithStatus }) => {
console.log(`[RelayManagement] Rendering relay: ${item.url}, status: ${item.status}`);
return (
<View style={{ padding: 16, borderBottomWidth: 1, borderBottomColor: '#e5e7eb' }}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<View style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}>
<View
style={{
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: getStatusColor(item.status),
marginRight: 8
}}
/>
<Text numberOfLines={1} style={{ flex: 1 }}>{item.url}</Text>
</View>
<TouchableOpacity
onPress={() => handleRemoveRelay(item.url)}
style={{ padding: 8 }}
>
<Text style={{ color: '#ef4444' }}>Remove</Text>
</TouchableOpacity>
// Render a relay item with proper typing
const renderRelayItem = ({ item }: { item: RelayWithStatus }) => (
<View style={{
padding: 12,
borderBottomWidth: 1,
borderBottomColor: '#e5e7eb',
backgroundColor: 'white',
}}>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}>
<View style={{
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: getStatusColor(item.status),
marginRight: 8
}} />
<Text style={{ flex: 1 }}>{item.url}</Text>
</View>
<View style={{ flexDirection: 'row', marginTop: 12, justifyContent: 'space-around' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ marginRight: 8 }}>Read</Text>
<Switch
value={item.read}
onValueChange={() => handleTogglePermission(item.url, 'read')}
/>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ marginRight: 8 }}>Write</Text>
<Switch
value={item.write}
onValueChange={() => handleTogglePermission(item.url, 'write')}
/>
</View>
<Text style={{ fontSize: 12, color: '#6b7280' }}>
{item.status}
</Text>
</View>
<View style={{
flexDirection: 'row',
justifyContent: 'space-around',
marginTop: 8,
paddingTop: 8,
borderTopWidth: 1,
borderTopColor: '#f3f4f6'
}}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ marginRight: 8 }}>Read</Text>
<Switch
checked={item.read}
onCheckedChange={() => updateRelay(item.url, { read: !item.read })}
/>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ marginRight: 8 }}>Write</Text>
<Switch
checked={item.write}
onCheckedChange={() => updateRelay(item.url, { write: !item.write })}
/>
</View>
</View>
);
};
// Reset component state
const resetComponent = () => {
setIsAddingRelay(false);
setNewRelayUrl('');
loadRelays();
};
// Error handler
const handleError = (error: any) => {
console.error('[RelayManagement] Error:', error);
Alert.alert('Error', error instanceof Error ? error.message : 'An unknown error occurred');
resetComponent();
};
</View>
);
return (
<Modal
@ -220,148 +95,60 @@ export default function RelayManagement({ isVisible, onClose }: Props) {
transparent={true}
onRequestClose={onClose}
>
<View style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', alignItems: 'center' }}>
<View style={{ width: '90%', maxHeight: '80%', backgroundColor: '#fff', borderRadius: 12 }}>
{/* Header */}
<View style={{ padding: 16, borderBottomWidth: 1, borderBottomColor: '#e5e7eb', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<View style={{
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'flex-end',
}}>
<View style={{
backgroundColor: 'white',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingBottom: 20,
maxHeight: '80%',
}}>
<View style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#e5e7eb',
}}>
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>Manage Relays</Text>
<TouchableOpacity onPress={onClose}>
<Text>Close</Text>
<X size={24} />
</TouchableOpacity>
</View>
{/* Content */}
<View style={{ flex: 1 }}>
{isLoading ? (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }}>
<ActivityIndicator size="large" />
<Text style={{ marginTop: 16 }}>Loading relays...</Text>
</View>
) : (
<>
{/* Relay list */}
{relays.length === 0 ? (
<View style={{ padding: 20, alignItems: 'center' }}>
<Text style={{ marginBottom: 16 }}>No relays configured</Text>
<TouchableOpacity
onPress={handleResetToDefaults}
style={{ padding: 10, backgroundColor: '#e5e7eb', borderRadius: 8 }}
>
<Text>Reset to Defaults</Text>
</TouchableOpacity>
</View>
) : (
<FlatList
data={relays}
keyExtractor={(item) => item.url}
renderItem={renderRelayItem}
ListEmptyComponent={
<View style={{ padding: 20, alignItems: 'center' }}>
<Text>No relays found. Try resetting to defaults.</Text>
</View>
}
/>
)}
{isLoading ? (
<View style={{ padding: 20, alignItems: 'center' }}>
<ActivityIndicator size="large" />
<Text style={{ marginTop: 10 }}>Loading relays...</Text>
</View>
) : (
<>
<FlatList
data={relays}
keyExtractor={(item) => item.url}
renderItem={renderRelayItem}
style={{ maxHeight: '70%' }}
/>
<View style={{ padding: 16 }}>
<Button onPress={loadRelays} style={{ marginBottom: 8 }}>
<Text style={{ color: 'white' }}>Refresh Relays</Text>
</Button>
{/* Add relay section */}
{isAddingRelay ? (
<View style={{ padding: 16, borderTopWidth: 1, borderTopColor: '#e5e7eb' }}>
<Text style={{ marginBottom: 8 }}>Add New Relay</Text>
<TextInput
style={{
borderWidth: 1,
borderColor: '#e5e7eb',
borderRadius: 8,
padding: 10,
marginBottom: 12
}}
placeholder="wss://relay.example.com"
value={newRelayUrl}
onChangeText={setNewRelayUrl}
autoCapitalize="none"
/>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<TouchableOpacity
onPress={() => setIsAddingRelay(false)}
style={{
padding: 10,
backgroundColor: '#e5e7eb',
borderRadius: 8,
flex: 1,
marginRight: 8,
alignItems: 'center'
}}
>
<Text>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={handleAddRelay}
style={{
padding: 10,
backgroundColor: '#3b82f6',
borderRadius: 8,
flex: 1,
alignItems: 'center'
}}
disabled={!newRelayUrl.startsWith('wss://')}
>
<Text style={{ color: '#fff' }}>Add Relay</Text>
</TouchableOpacity>
</View>
</View>
) : (
<View style={{ padding: 16, borderTopWidth: 1, borderTopColor: '#e5e7eb' }}>
<TouchableOpacity
onPress={() => setIsAddingRelay(true)}
style={{
padding: 10,
backgroundColor: '#e5e7eb',
borderRadius: 8,
alignItems: 'center'
}}
>
<Text>Add New Relay</Text>
</TouchableOpacity>
</View>
)}
</>
)}
</View>
{/* Footer */}
<View style={{ padding: 16, borderTopWidth: 1, borderTopColor: '#e5e7eb' }}>
<TouchableOpacity
onPress={handleApplyChanges}
style={{
padding: 14,
backgroundColor: '#3b82f6',
borderRadius: 8,
alignItems: 'center'
}}
disabled={isSaving}
>
{isSaving ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={{ color: '#fff', fontWeight: '500' }}>Apply Changes</Text>
)}
</TouchableOpacity>
<TouchableOpacity
onPress={handleResetToDefaults}
style={{
padding: 14,
backgroundColor: 'transparent',
borderRadius: 8,
alignItems: 'center',
marginTop: 8
}}
>
<Text>Reset to Defaults</Text>
</TouchableOpacity>
</View>
<Button
variant="outline"
onPress={onClose}
>
<Text>Close</Text>
</Button>
</View>
</>
)}
</View>
</View>
</Modal>

View File

@ -40,11 +40,23 @@ export interface RelayWithStatus extends RelayConfig {
export class RelayService {
private db: SQLiteDatabase;
private ndk: NDKCommon | null = null;
private debug: boolean = false;
constructor(db: SQLiteDatabase) {
this.db = db;
}
enableDebug() {
this.debug = true;
console.log('[RelayService] Debug mode enabled');
}
private logDebug(message: string, ...args: any[]) {
if (this.debug) {
console.log(`[RelayService Debug] ${message}`, ...args);
}
}
/**
* Set NDK instance for relay operations
*/
@ -84,24 +96,21 @@ export class RelayService {
if (!this.ndk) {
console.warn('[RelayService] NDK not initialized, returning relays with disconnected status');
// Return relays with disconnected status if NDK not initialized
return relays.map(relay => ({
...relay,
status: 'disconnected'
}));
}
// Log the relays in the NDK pool for debugging
console.log('[RelayService] Checking status for relays. Current NDK pool:');
this.ndk.pool.relays.forEach((ndkRelay, url) => {
console.log(` - ${url}: status=${ndkRelay.status}`);
});
return relays.map(relay => {
let status: 'connected' | 'connecting' | 'disconnected' | 'error' = 'disconnected';
try {
const ndkRelay = this.ndk?.pool.getRelay(relay.url);
if (ndkRelay) {
status = this.getRelayStatus(ndkRelay);
}
} catch (error) {
console.error(`[RelayService] Error getting status for relay ${relay.url}:`, error);
}
const status = this.getRelayStatus(relay);
console.log(`[RelayService] Status for relay ${relay.url}: ${status}`);
return {
...relay,
@ -114,13 +123,18 @@ export class RelayService {
}
}
private normalizeRelayUrl(url: string): string {
// Remove trailing slash if present
return url.replace(/\/$/, '');
}
/**
* Add a new relay to the database
*/
async addRelay(url: string, read = true, write = true, priority?: number): Promise<boolean> {
try {
// Normalize the URL
url = url.trim();
url = this.normalizeRelayUrl(url.trim());
// Validate URL format
if (!url.startsWith('wss://')) {
@ -408,6 +422,24 @@ export class RelayService {
console.log(`[RelayService] Found relay list in event created at ${new Date(latestCreatedAt * 1000).toISOString()}`);
// Safely log event details without circular references
try {
console.log('[RelayService] Event ID:', latestEvent.id);
console.log('[RelayService] Event Kind:', latestEvent.kind);
console.log('[RelayService] Event Created At:', latestEvent.created_at);
console.log('[RelayService] Event Tags Count:', latestEvent.tags ? latestEvent.tags.length : 0);
// Safely log the tags
if (latestEvent.tags && Array.isArray(latestEvent.tags)) {
console.log('[RelayService] Tags:');
latestEvent.tags.forEach((tag: any[], index: number) => {
console.log(` Tag ${index}:`, JSON.stringify(tag));
});
}
} catch (error) {
console.log('[RelayService] Error logging event details:', error);
}
// Get highest current priority
const highestPriority = await this.db.getFirstAsync<{ priority: number }>(
'SELECT MAX(priority) as priority FROM relays'
@ -417,52 +449,295 @@ export class RelayService {
let importCount = 0;
let updatedCount = 0;
// Check if any relay tags exist
let relayTagsFound = false;
// Process each relay in the event
for (const tag of latestEvent.tags) {
if (tag[0] === 'r') {
const url = tag[1];
// Check for read/write specification in the tag
let read = true;
let write = true;
if (tag.length > 2) {
read = tag[2] !== 'write'; // If "write", then not read
write = tag[2] !== 'read'; // If "read", then not write
}
if (latestEvent.tags && Array.isArray(latestEvent.tags)) {
for (const tag of latestEvent.tags) {
try {
// Check if the relay already exists
const existingRelay = await this.db.getFirstAsync<{ url: string }>(
'SELECT url FROM relays WHERE url = ?',
[url]
);
console.log(`[RelayService] Processing tag: ${JSON.stringify(tag)}`);
const now = Date.now();
if (existingRelay) {
// Update existing relay
await this.db.runAsync(
'UPDATE relays SET read = ?, write = ?, updated_at = ? WHERE url = ?',
[read ? 1 : 0, write ? 1 : 0, now, url]
);
updatedCount++;
} else {
// Add new relay with incremented priority
maxPriority++;
await this.db.runAsync(
'INSERT INTO relays (url, read, write, priority, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[url, read ? 1 : 0, write ? 1 : 0, maxPriority, now, now]
);
importCount++;
// More flexible tag detection - handle 'r', 'R', or 'relay' tag types
if ((tag[0] === 'r' || tag[0] === 'R' || tag[0] === 'relay') && tag.length > 1 && tag[1]) {
relayTagsFound = true;
console.log(`[RelayService] Found relay tag: ${tag[1]}`);
const url = tag[1];
// Ensure URL is properly formatted
if (!url.startsWith('wss://') && !url.startsWith('ws://')) {
console.log(`[RelayService] Skipping invalid relay URL: ${url}`);
continue;
}
// Check for read/write specification in the tag
let read = true;
let write = true;
if (tag.length > 2) {
// Handle various common formatting patterns
const readWriteSpec = tag[2]?.toLowerCase();
if (readWriteSpec === 'write') {
read = false;
write = true;
console.log(`[RelayService] Relay ${url} configured as write-only`);
} else if (readWriteSpec === 'read') {
read = true;
write = false;
console.log(`[RelayService] Relay ${url} configured as read-only`);
} else {
console.log(`[RelayService] Unrecognized read/write spec: ${readWriteSpec}, using default (read+write)`);
}
}
try {
// Check if the relay already exists
const existingRelay = await this.db.getFirstAsync<{ url: string }>(
'SELECT url FROM relays WHERE url = ?',
[url]
);
const now = Date.now();
if (existingRelay) {
// Update existing relay
await this.db.runAsync(
'UPDATE relays SET read = ?, write = ?, updated_at = ? WHERE url = ?',
[read ? 1 : 0, write ? 1 : 0, now, url]
);
updatedCount++;
console.log(`[RelayService] Updated existing relay: ${url} (read=${read}, write=${write})`);
} else {
// Add new relay with incremented priority
maxPriority++;
await this.db.runAsync(
'INSERT INTO relays (url, read, write, priority, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[url, read ? 1 : 0, write ? 1 : 0, maxPriority, now, now]
);
importCount++;
console.log(`[RelayService] Added new relay: ${url} (read=${read}, write=${write}, priority=${maxPriority})`);
}
} catch (innerError) {
console.error(`[RelayService] Error importing relay ${url}:`, innerError);
// Continue with other relays
}
}
} catch (innerError) {
console.error(`[RelayService] Error importing relay ${url}:`, innerError);
// Continue with other relays
} catch (tagError) {
console.log('[RelayService] Error processing tag:', tagError);
}
}
}
// Check for relays in content (some clients store them there)
if (!relayTagsFound) {
console.log('[RelayService] No relay tags found in event tags, checking content...');
try {
// Only try to parse the content if it's a string
if (typeof latestEvent.content === 'string') {
const contentObj = JSON.parse(latestEvent.content);
// Only log specific properties to avoid circular references
console.log('[RelayService] Content has relays property:', contentObj.hasOwnProperty('relays'));
// Some clients store relays in content as an object
if (contentObj.relays && typeof contentObj.relays === 'object') {
console.log('[RelayService] Found relay URLs in content:', Object.keys(contentObj.relays));
// Process relays from content object
for (const [url, permissions] of Object.entries(contentObj.relays)) {
try {
if (typeof url === 'string' && (url.startsWith('wss://') || url.startsWith('ws://'))) {
relayTagsFound = true;
let read = true;
let write = true;
// Handle different formats of permissions
if (typeof permissions === 'object' && permissions !== null) {
// Format: { "wss://relay.example.com": { "read": true, "write": false } }
if ('read' in permissions) read = Boolean((permissions as any).read);
if ('write' in permissions) write = Boolean((permissions as any).write);
} else if (typeof permissions === 'string') {
// Format: { "wss://relay.example.com": "read" }
read = (permissions as string).includes('read');
write = (permissions as string).includes('write');
}
console.log(`[RelayService] Found relay in content: ${url} (read=${read}, write=${write})`);
// Then add or update the relay just like above...
try {
const existingRelay = await this.db.getFirstAsync<{ url: string }>(
'SELECT url FROM relays WHERE url = ?',
[url]
);
const now = Date.now();
if (existingRelay) {
await this.db.runAsync(
'UPDATE relays SET read = ?, write = ?, updated_at = ? WHERE url = ?',
[read ? 1 : 0, write ? 1 : 0, now, url]
);
updatedCount++;
console.log(`[RelayService] Updated existing relay from content: ${url} (read=${read}, write=${write})`);
} else {
maxPriority++;
await this.db.runAsync(
'INSERT INTO relays (url, read, write, priority, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[url, read ? 1 : 0, write ? 1 : 0, maxPriority, now, now]
);
importCount++;
console.log(`[RelayService] Added new relay from content: ${url} (read=${read}, write=${write}, priority=${maxPriority})`);
}
} catch (innerError) {
console.error(`[RelayService] Error importing relay ${url} from content:`, innerError);
}
}
} catch (relayError) {
console.log('[RelayService] Error processing relay from content:', relayError);
}
}
}
} else {
console.log('[RelayService] Content is not a string:', typeof latestEvent.content);
}
} catch (e) {
// Convert the unknown error to a string safely
const errorMessage = e instanceof Error ? e.message : String(e);
console.log('[RelayService] Content is not JSON or does not contain relay information:', errorMessage);
}
}
// Check the raw event string that might be available
if (!relayTagsFound && latestEvent.rawEvent && typeof latestEvent.rawEvent === 'string') {
console.log('[RelayService] Checking raw event string for relay information');
try {
const rawEventObj = JSON.parse(latestEvent.rawEvent);
if (rawEventObj.tags && Array.isArray(rawEventObj.tags)) {
console.log(`[RelayService] Raw event has ${rawEventObj.tags.length} tags`);
for (const tag of rawEventObj.tags) {
try {
if ((tag[0] === 'r' || tag[0] === 'R') && tag.length > 1 && tag[1]) {
relayTagsFound = true;
const url = tag[1];
console.log(`[RelayService] Found relay in raw event: ${url}`);
// Process like above...
if (url.startsWith('wss://') || url.startsWith('ws://')) {
let read = true;
let write = true;
if (tag.length > 2) {
const readWriteSpec = tag[2]?.toLowerCase();
if (readWriteSpec === 'write') {
read = false;
write = true;
} else if (readWriteSpec === 'read') {
read = true;
write = false;
}
}
try {
const existingRelay = await this.db.getFirstAsync<{ url: string }>(
'SELECT url FROM relays WHERE url = ?',
[url]
);
const now = Date.now();
if (existingRelay) {
await this.db.runAsync(
'UPDATE relays SET read = ?, write = ?, updated_at = ? WHERE url = ?',
[read ? 1 : 0, write ? 1 : 0, now, url]
);
updatedCount++;
} else {
maxPriority++;
await this.db.runAsync(
'INSERT INTO relays (url, read, write, priority, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[url, read ? 1 : 0, write ? 1 : 0, maxPriority, now, now]
);
importCount++;
}
} catch (innerError) {
console.error(`[RelayService] Error importing relay ${url} from raw event:`, innerError);
}
}
}
} catch (tagError) {
console.log('[RelayService] Error processing tag from raw event:', tagError);
}
}
}
} catch (rawError) {
// Convert the unknown error to a string safely
const errorMessage = rawError instanceof Error ? rawError.message : String(rawError);
console.log('[RelayService] Error parsing raw event:', errorMessage);
}
}
// Try to access user cached relays
if (!relayTagsFound && ndk && ndk.pool && ndk.pool.relays) {
console.log('[RelayService] Checking for relays in the user NDK pool');
try {
// Try to access the user's connected relays
const userRelays = Array.from(ndk.pool.relays.keys());
if (userRelays.length > 0) {
console.log(`[RelayService] Found ${userRelays.length} relays in user's NDK pool:`, userRelays);
// Import these relays
for (const url of userRelays) {
if (typeof url === 'string' && (url.startsWith('wss://') || url.startsWith('ws://'))) {
try {
const existingRelay = await this.db.getFirstAsync<{ url: string }>(
'SELECT url FROM relays WHERE url = ?',
[url]
);
const now = Date.now();
if (existingRelay) {
// We'll only update the timestamp, not the permissions
await this.db.runAsync(
'UPDATE relays SET updated_at = ? WHERE url = ?',
[now, url]
);
updatedCount++;
console.log(`[RelayService] Updated existing relay from NDK pool: ${url}`);
} else {
maxPriority++;
await this.db.runAsync(
'INSERT INTO relays (url, read, write, priority, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[url, 1, 1, maxPriority, now, now]
);
importCount++;
console.log(`[RelayService] Added new relay from NDK pool: ${url}`);
}
} catch (innerError) {
console.error(`[RelayService] Error importing relay ${url} from NDK pool:`, innerError);
}
}
}
// Set flag to true because we found relays
relayTagsFound = userRelays.length > 0;
}
} catch (poolError) {
console.log('[RelayService] Error accessing NDK pool relays:', poolError);
}
}
if (!relayTagsFound) {
console.log('[RelayService] No relay information found in any format');
}
console.log(`[RelayService] Imported ${importCount} new relays, updated ${updatedCount} existing relays`);
return importCount > 0 || updatedCount > 0;
} catch (error) {
@ -483,19 +758,25 @@ export class RelayService {
// Add default relays
const now = Date.now();
let addedCount = 0;
for (let i = 0; i < DEFAULT_RELAYS.length; i++) {
const url = DEFAULT_RELAYS[i];
const priority = DEFAULT_RELAYS.length - i; // Higher priority for first relays
await this.db.runAsync(
'INSERT INTO relays (url, read, write, priority, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[url, 1, 1, priority, now, now]
);
try {
await this.db.runAsync(
'INSERT INTO relays (url, read, write, priority, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[url, 1, 1, priority, now, now]
);
addedCount++;
} catch (innerError) {
console.error(`[RelayService] Error adding default relay ${url}:`, innerError);
}
}
console.log(`[RelayService] Successfully reset to ${DEFAULT_RELAYS.length} default relays`);
return true;
console.log(`[RelayService] Successfully reset to ${addedCount} default relays`);
return addedCount > 0;
} catch (error) {
console.error('[RelayService] Error resetting relays to defaults:', error);
throw error;
@ -603,22 +884,42 @@ export class RelayService {
* Helper to convert NDK relay status to our status format
*/
private getRelayStatus(relay: any): 'connected' | 'connecting' | 'disconnected' | 'error' {
try {
if (relay.status === NDK_RELAY_STATUS.CONNECTED) {
try {
// Check if the relay has a trailing slash in the URL
const urlWithoutSlash = relay.url ? relay.url.replace(/\/$/, '') : '';
const urlWithSlash = urlWithoutSlash + '/';
// Try to get the relay from NDK pool - check both with and without trailing slash
const ndkRelay = this.ndk?.pool.getRelay(urlWithoutSlash) ||
this.ndk?.pool.getRelay(urlWithSlash);
if (ndkRelay) {
console.log(`[RelayService] Detailed relay status for ${relay.url}: status=${ndkRelay.status}, connected=${!!ndkRelay.connected}`);
// The most reliable way to check connection status is to check the 'connected' property
if (ndkRelay.connected) {
return 'connected';
} else if (
relay.status === NDK_RELAY_STATUS.CONNECTING ||
relay.status === NDK_RELAY_STATUS.RECONNECTING
) {
}
// NDK relay status: 0=connecting, 1=connected, 2=disconnecting, 3=disconnected, 4=reconnecting, 5=auth_required
if (ndkRelay.status === 1) {
return 'connected';
} else if (ndkRelay.status === 0 || ndkRelay.status === 4) { // CONNECTING or RECONNECTING
return 'connecting';
} else if (ndkRelay.status === 5) { // AUTH_REQUIRED - This is actually a connected state!
return 'connected'; // This is the key fix
} else {
return 'disconnected';
}
} catch (error) {
console.error(`[RelayService] Error getting relay status:`, error);
return 'disconnected';
}
// If we can't find the relay in the NDK pool
return 'disconnected';
} catch (error) {
console.error(`[RelayService] Error getting relay status:`, error);
return 'disconnected';
}
}
/**
* Check and debug relays table and content
@ -674,9 +975,10 @@ export class RelayService {
);
// If we have relays and they're not just the defaults, skip import
if (existingCount?.count > DEFAULT_RELAYS.length) {
console.log(`[RelayService] Using existing relay configuration (${existingCount?.count} relays)`);
return;
if (existingCount && existingCount.count !== undefined && existingCount.count > 0) {
console.log(`[RelayService] Found ${existingCount.count} existing relays, checking if we need to import more`);
} else {
console.log('[RelayService] No existing relays found, will attempt to import');
}
console.log('[RelayService] Attempting to import user relay preferences');
@ -689,10 +991,19 @@ export class RelayService {
// Apply the imported configuration immediately
await this.applyRelayConfig(ndk);
} else {
console.log('[RelayService] No relay preferences found, using defaults');
console.log('[RelayService] No relay preferences found, resetting to defaults');
await this.resetToDefaults();
await this.applyRelayConfig(ndk);
}
} catch (error) {
console.error('[RelayService] Error importing user relays:', error);
// On error, reset to defaults
try {
console.log('[RelayService] Error occurred, resetting to defaults');
await this.resetToDefaults();
await this.applyRelayConfig(ndk);
} catch (resetError) {
console.error('[RelayService] Error resetting to defaults:', resetError);
}
}
}
}
}}

View File

@ -19,6 +19,8 @@ export async function initializeNDK() {
// Initialize database and relay service
const db = openDatabaseSync('powr.db');
const relayService = new RelayService(db);
relayService.enableDebug();
// Load relays from database or use defaults
console.log('[NDK] Loading relay configuration...');
@ -97,7 +99,6 @@ export async function initializeNDK() {
});
try {
// Connect to relays
console.log('[NDK] Connecting to relays...');
await ndk.connect();
@ -111,6 +112,17 @@ export async function initializeNDK() {
console.log(`[NDK] Connected to ${connectedRelays.length}/${relays.length} relays`);
// Add more detailed relay status logging
console.log('[NDK] Detailed relay status:');
relays.forEach(url => {
const relay = ndk.pool.getRelay(url);
console.log(` - ${url}: ${relay ?
(relay.status === 1 ? 'connected' :
relay.status === 0 ? 'connecting' :
relay.status === 3 ? 'disconnected' :
`status=${relay.status}`) : 'not found'}`);
});
return {
ndk,
relayStatus,

View File

@ -10,6 +10,7 @@ import NDK, {
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
import * as SecureStore from 'expo-secure-store';
import { RelayService } from '@/lib/db/services/RelayService';
import { openDatabaseSync } from 'expo-sqlite';
// Constants for SecureStore
const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey';