2025-03-28 10:18:44 -07:00
|
|
|
// lib/services/NostrBandService.ts
|
|
|
|
import { nip19 } from 'nostr-tools';
|
2025-04-04 15:46:31 -04:00
|
|
|
import { createLogger, enableModule } from '@/lib/utils/logger';
|
|
|
|
import { Platform } from 'react-native';
|
|
|
|
|
|
|
|
// Enable logging
|
|
|
|
enableModule('NostrBandService');
|
|
|
|
const logger = createLogger('NostrBandService');
|
|
|
|
const platform = Platform.OS === 'ios' ? 'iOS' : 'Android';
|
2025-03-28 10:18:44 -07:00
|
|
|
|
|
|
|
export interface ProfileStats {
|
|
|
|
pubkey: string;
|
|
|
|
followersCount: number;
|
|
|
|
followingCount: number;
|
|
|
|
isLoading: boolean;
|
|
|
|
error: Error | null;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface NostrBandProfileStatsResponse {
|
|
|
|
stats: {
|
|
|
|
[pubkey: string]: {
|
|
|
|
pubkey: string;
|
|
|
|
followers_pubkey_count?: number;
|
|
|
|
pub_following_pubkey_count?: number;
|
|
|
|
// Add other fields as needed from the API response
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Service for interacting with the NostrBand API
|
|
|
|
* This service provides methods to fetch statistics from nostr.band
|
|
|
|
*/
|
|
|
|
export class NostrBandService {
|
|
|
|
private readonly apiUrl = 'https://api.nostr.band';
|
2025-04-04 15:46:31 -04:00
|
|
|
private cacheDisabled = true; // Disable any internal caching
|
2025-03-28 10:18:44 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetches profile statistics from nostr.band API
|
|
|
|
* @param pubkey Pubkey in hex format or npub format
|
2025-04-04 15:46:31 -04:00
|
|
|
* @param forceFresh Whether to bypass any caching with a cache-busting parameter
|
2025-03-28 10:18:44 -07:00
|
|
|
* @returns Promise with profile stats
|
|
|
|
*/
|
2025-04-04 15:46:31 -04:00
|
|
|
async fetchProfileStats(pubkey: string, forceFresh: boolean = true): Promise<ProfileStats> {
|
2025-03-28 10:18:44 -07:00
|
|
|
try {
|
2025-04-04 15:46:31 -04:00
|
|
|
// Always log request details
|
|
|
|
logger.info(`[${platform}] Fetching profile stats for pubkey: ${pubkey.substring(0, 8)}...`);
|
|
|
|
|
2025-03-28 10:18:44 -07:00
|
|
|
// Check if pubkey is npub or hex and convert if needed
|
|
|
|
let hexPubkey = pubkey;
|
|
|
|
if (pubkey.startsWith('npub')) {
|
|
|
|
try {
|
|
|
|
const decoded = nip19.decode(pubkey);
|
|
|
|
if (decoded.type === 'npub') {
|
|
|
|
hexPubkey = decoded.data as string;
|
|
|
|
}
|
|
|
|
} catch (error) {
|
2025-04-04 15:46:31 -04:00
|
|
|
logger.error(`[${platform}] Error decoding npub:`, error);
|
2025-03-28 10:18:44 -07:00
|
|
|
throw new Error('Invalid npub format');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
// Always force cache busting
|
|
|
|
const cacheBuster = `?_t=${Date.now()}`;
|
2025-03-28 10:18:44 -07:00
|
|
|
const endpoint = `/v0/stats/profile/${hexPubkey}`;
|
2025-04-04 15:46:31 -04:00
|
|
|
const url = `${this.apiUrl}${endpoint}${cacheBuster}`;
|
2025-03-28 10:18:44 -07:00
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
logger.info(`[${platform}] Fetching from: ${url}`);
|
|
|
|
|
2025-04-04 18:00:20 -04:00
|
|
|
// Create AbortController with timeout for Android
|
|
|
|
const controller = new AbortController();
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
|
|
if (platform === 'Android') {
|
|
|
|
logger.warn(`[Android] Aborting stats fetch due to timeout for ${hexPubkey.substring(0, 8)}`);
|
|
|
|
controller.abort();
|
2025-04-04 15:46:31 -04:00
|
|
|
}
|
2025-04-04 18:00:20 -04:00
|
|
|
}, 5000); // 5 second timeout for Android
|
2025-03-28 10:18:44 -07:00
|
|
|
|
2025-04-04 18:00:20 -04:00
|
|
|
try {
|
|
|
|
// Fetch with explicit no-cache headers and abort signal
|
|
|
|
const response = await fetch(url, {
|
|
|
|
method: 'GET',
|
|
|
|
headers: {
|
|
|
|
'Accept': 'application/json',
|
|
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
|
|
'Pragma': 'no-cache',
|
|
|
|
'Expires': '0'
|
|
|
|
},
|
|
|
|
signal: controller.signal
|
|
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
2025-03-28 10:18:44 -07:00
|
|
|
|
2025-04-04 18:00:20 -04:00
|
|
|
if (!response.ok) {
|
|
|
|
const errorText = await response.text();
|
|
|
|
logger.error(`[${platform}] API error: ${response.status} - ${errorText}`);
|
|
|
|
throw new Error(`API error: ${response.status} - ${errorText}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse with timeout protection
|
|
|
|
const textData = await response.text();
|
|
|
|
const data = JSON.parse(textData) as NostrBandProfileStatsResponse;
|
2025-03-28 10:18:44 -07:00
|
|
|
|
2025-04-04 18:00:20 -04:00
|
|
|
// Check if we got valid data
|
|
|
|
if (!data || !data.stats || !data.stats[hexPubkey]) {
|
|
|
|
logger.error(`[${platform}] Invalid response from API:`, JSON.stringify(data));
|
|
|
|
|
|
|
|
// Special handling for Android - return fallback values rather than throwing
|
|
|
|
if (platform === 'Android') {
|
|
|
|
logger.warn(`[Android] Using fallback stats for ${hexPubkey.substring(0, 8)}`);
|
|
|
|
return {
|
|
|
|
pubkey: hexPubkey,
|
|
|
|
followersCount: 0,
|
|
|
|
followingCount: 0,
|
|
|
|
isLoading: false,
|
|
|
|
error: null
|
|
|
|
};
|
|
|
|
}
|
|
|
|
throw new Error('Invalid response from API');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extract relevant stats
|
|
|
|
const profileStats = data.stats[hexPubkey];
|
2025-04-04 15:46:31 -04:00
|
|
|
|
2025-04-04 18:00:20 -04:00
|
|
|
// Create result with real data - ensure we have non-null values
|
|
|
|
const result = {
|
|
|
|
pubkey: hexPubkey,
|
|
|
|
followersCount: profileStats.followers_pubkey_count ?? 0,
|
|
|
|
followingCount: profileStats.pub_following_pubkey_count ?? 0,
|
|
|
|
isLoading: false,
|
|
|
|
error: null
|
|
|
|
};
|
2025-04-04 15:46:31 -04:00
|
|
|
|
2025-04-04 18:00:20 -04:00
|
|
|
// Log the fetched stats
|
|
|
|
logger.info(`[${platform}] Fetched stats for ${hexPubkey.substring(0, 8)}:`, {
|
|
|
|
followersCount: result.followersCount,
|
|
|
|
followingCount: result.followingCount
|
|
|
|
});
|
|
|
|
|
|
|
|
return result;
|
|
|
|
} finally {
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
}
|
2025-03-28 10:18:44 -07:00
|
|
|
} catch (error) {
|
2025-04-04 15:46:31 -04:00
|
|
|
// Log the error with platform info
|
|
|
|
logger.error(`[${platform}] Error fetching profile stats:`, error);
|
|
|
|
|
2025-04-04 18:00:20 -04:00
|
|
|
// For Android, return fallback values rather than throwing
|
|
|
|
if (platform === 'Android') {
|
|
|
|
logger.warn(`[Android] Returning fallback stats for ${pubkey.substring(0, 8)} due to error`);
|
|
|
|
return {
|
|
|
|
pubkey: pubkey,
|
|
|
|
followersCount: 0,
|
|
|
|
followingCount: 0,
|
|
|
|
isLoading: false,
|
|
|
|
error: error instanceof Error ? error : new Error(String(error))
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// For other platforms, throw to allow React Query to handle retries
|
2025-04-04 15:46:31 -04:00
|
|
|
throw error;
|
2025-03-28 10:18:44 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Export a singleton instance
|
|
|
|
export const nostrBandService = new NostrBandService();
|