clone of repo on github
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

864 lines
24 KiB

import NDK, { NDKKind, NDKRelay, NDKUser } from "@nostr-dev-kit/ndk";
import {
anonymousRelays,
localRelays,
lowbandwidthRelays,
searchRelays,
secondaryRelays,
} from "../consts.ts";
import { getRelaySetForNetworkCondition } from "./network_detection.ts";
import { networkCondition } from "../stores/networkStore.ts";
import { get } from "svelte/store";
/**
* Normalizes a relay URL to a standard format
* @param url The relay URL to normalize
* @returns The normalized relay URL
*/
export function normalizeRelayUrl(url: string): string {
let normalized = url.toLowerCase().trim();
// Ensure protocol is present
if (!normalized.startsWith("ws://") && !normalized.startsWith("wss://")) {
normalized = "wss://" + normalized;
}
// Remove trailing slash
normalized = normalized.replace(/\/$/, "");
return normalized;
}
/**
* Normalizes an array of relay URLs
* @param urls Array of relay URLs to normalize
* @returns Array of normalized relay URLs
*/
export function normalizeRelayUrls(urls: string[]): string[] {
return urls.map(normalizeRelayUrl);
}
/**
* Removes duplicates from an array of relay URLs
* @param urls Array of relay URLs
* @returns Array of unique relay URLs
*/
export function deduplicateRelayUrls(urls: string[]): string[] {
const normalized = normalizeRelayUrls(urls);
return [...new Set(normalized)];
}
/**
* Tests connection to a local relay (ws:// protocol)
* @param relayUrl The local relay URL to test (should be ws://)
* @param ndk The NDK instance
* @returns Promise that resolves to connection status
*/
export function testLocalRelayConnection(
relayUrl: string,
ndk: NDK,
): Promise<{
connected: boolean;
requiresAuth: boolean;
error?: string;
actualUrl?: string;
}> {
// Only test connections on client-side
if (typeof window === "undefined") {
return Promise.resolve({
connected: false,
requiresAuth: false,
error: "Server-side rendering - connection test skipped",
actualUrl: relayUrl,
});
}
return new Promise((resolve) => {
try {
// Ensure the URL is using ws:// protocol for local relays
const localUrl = relayUrl.replace(/^wss:\/\//, "ws://");
// Use the existing NDK instance instead of creating a new one
const relay = new NDKRelay(localUrl, undefined, ndk);
let authRequired = false;
let connected = false;
let error: string | undefined;
let actualUrl: string | undefined;
const timeout = setTimeout(() => {
try {
relay.disconnect();
} catch {
// Silently ignore disconnect errors
}
resolve({
connected: false,
requiresAuth: authRequired,
error: "Connection timeout",
actualUrl,
});
}, 3000);
// Wrap all event handlers in try-catch to prevent errors from bubbling up
relay.on("connect", () => {
try {
connected = true;
actualUrl = localUrl;
clearTimeout(timeout);
relay.disconnect();
resolve({
connected: true,
requiresAuth: authRequired,
error,
actualUrl,
});
} catch {
// Silently handle any errors in connect handler
clearTimeout(timeout);
resolve({
connected: false,
requiresAuth: false,
error: "Connection handler error",
actualUrl: localUrl,
});
}
});
relay.on("notice", (message: string) => {
try {
if (message.includes("auth-required")) {
authRequired = true;
}
} catch {
// Silently ignore notice handler errors
}
});
relay.on("disconnect", () => {
try {
if (!connected) {
error = "Connection failed";
clearTimeout(timeout);
resolve({
connected: false,
requiresAuth: authRequired,
error,
actualUrl,
});
}
} catch {
// Silently handle any errors in disconnect handler
clearTimeout(timeout);
resolve({
connected: false,
requiresAuth: false,
error: "Disconnect handler error",
actualUrl: localUrl,
});
}
});
// Wrap the connect call in try-catch
try {
relay.connect();
} catch (connectError) {
// Silently handle connection errors
clearTimeout(timeout);
resolve({
connected: false,
requiresAuth: false,
error: "Connection failed",
actualUrl: localUrl,
});
}
} catch (outerError) {
// Catch any other errors that might occur during setup
resolve({
connected: false,
requiresAuth: false,
error: "Setup failed",
actualUrl: relayUrl,
});
}
});
}
/**
* Tests connection to a remote relay (wss:// protocol)
* @param relayUrl The remote relay URL to test
* @param ndk The NDK instance
* @returns Promise that resolves to connection status
*/
export function testRemoteRelayConnection(
relayUrl: string,
ndk: NDK,
): Promise<{
connected: boolean;
requiresAuth: boolean;
error?: string;
actualUrl?: string;
}> {
// Only test connections on client-side
if (typeof window === "undefined") {
return Promise.resolve({
connected: false,
requiresAuth: false,
error: "Server-side rendering - connection test skipped",
actualUrl: relayUrl,
});
}
return new Promise((resolve) => {
// Ensure the URL is using wss:// protocol for remote relays
const secureUrl = relayUrl.replace(/^ws:\/\//, "wss://");
console.debug(
`[relay_management.ts] Testing remote relay connection: ${secureUrl}`,
);
// Use the existing NDK instance instead of creating a new one
const relay = new NDKRelay(secureUrl, undefined, ndk);
let authRequired = false;
let connected = false;
let error: string | undefined;
let actualUrl: string | undefined;
const timeout = setTimeout(() => {
console.debug(
`[relay_management.ts] Relay ${secureUrl} connection timeout`,
);
relay.disconnect();
resolve({
connected: false,
requiresAuth: authRequired,
error: "Connection timeout",
actualUrl,
});
}, 3000);
relay.on("connect", () => {
console.debug(
`[relay_management.ts] Relay ${secureUrl} connected successfully`,
);
connected = true;
actualUrl = secureUrl;
clearTimeout(timeout);
relay.disconnect();
resolve({
connected: true,
requiresAuth: authRequired,
error,
actualUrl,
});
});
relay.on("notice", (message: string) => {
if (message.includes("auth-required")) {
authRequired = true;
}
});
relay.on("disconnect", () => {
if (!connected) {
console.debug(
`[relay_management.ts] Relay ${secureUrl} disconnected without connecting`,
);
error = "Connection failed";
clearTimeout(timeout);
resolve({
connected: false,
requiresAuth: authRequired,
error,
actualUrl,
});
}
});
relay.connect();
});
}
/**
* Tests connection to a relay and returns connection status
* @param relayUrl The relay URL to test
* @param ndk The NDK instance
* @returns Promise that resolves to connection status
*/
export function testRelayConnection(
relayUrl: string,
ndk: NDK,
): Promise<{
connected: boolean;
requiresAuth: boolean;
error?: string;
actualUrl?: string;
}> {
// Determine if this is a local or remote relay
if (relayUrl.includes("localhost") || relayUrl.includes("127.0.0.1")) {
return testLocalRelayConnection(relayUrl, ndk);
} else {
return testRemoteRelayConnection(relayUrl, ndk);
}
}
/**
* Tests connection to local relays
* @param localRelayUrls Array of local relay URLs to test
* @param ndk NDK instance
* @returns Promise that resolves to array of working local relay URLs
*/
async function testLocalRelays(
localRelayUrls: string[],
ndk: NDK,
): Promise<string[]> {
try {
const workingRelays: string[] = [];
if (localRelayUrls.length === 0) {
return workingRelays;
}
// Test local relays quietly, without logging failures
await Promise.all(
localRelayUrls.map(async (url) => {
try {
const result = await testLocalRelayConnection(url, ndk);
if (result.connected) {
workingRelays.push(url);
console.debug(
`[relay_management.ts] Local relay connected: ${url}`,
);
}
// Don't log failures - local relays are optional
} catch {
// Silently ignore local relay failures - they're optional
}
}),
);
if (workingRelays.length > 0) {
console.info(
`[relay_management.ts] Found ${workingRelays.length} working local relays`,
);
}
return workingRelays;
} catch {
// If anything goes wrong with the entire local relay testing process,
// just return an empty array silently
return [];
}
}
/**
* Discovers local relays by testing common localhost URLs
* @param ndk NDK instance
* @returns Promise that resolves to array of working local relay URLs
*/
export async function discoverLocalRelays(ndk: NDK): Promise<string[]> {
try {
// If no local relays are configured, return empty array
if (localRelays.length === 0) {
console.debug("[relay_management.ts] No local relays configured");
return [];
}
// Convert wss:// URLs from consts to ws:// for local testing
const localRelayUrls = localRelays.map((url: string) =>
url.replace(/^wss:\/\//, "ws://")
);
const workingRelays = await testLocalRelays(localRelayUrls, ndk);
// If no local relays are working, return empty array
// The network detection logic will provide fallback relays
return workingRelays;
} catch {
// Silently fail and return empty array
return [];
}
}
/**
* Fetches user's local relays from kind 10432 event
* @param ndk NDK instance
* @param user User to fetch local relays for
* @returns Promise that resolves to array of local relay URLs
*/
export async function getUserLocalRelays(
ndk: NDK,
user: NDKUser,
): Promise<string[]> {
try {
const localRelayEvent = await ndk.fetchEvent(
{
kinds: [10432 as NDKKind],
authors: [user.pubkey],
},
{
groupable: false,
skipVerification: false,
skipValidation: false,
},
);
if (!localRelayEvent) {
return [];
}
const localRelays: string[] = [];
localRelayEvent.tags.forEach((tag) => {
if (tag[0] === "r" && tag[1]) {
localRelays.push(tag[1]);
}
});
return localRelays;
} catch (error) {
console.info(
"[relay_management.ts] Error fetching user local relays:",
error,
);
return [];
}
}
/**
* Fetches user's blocked relays from kind 10006 event
* @param ndk NDK instance
* @param user User to fetch blocked relays for
* @returns Promise that resolves to array of blocked relay URLs
*/
export async function getUserBlockedRelays(
ndk: NDK,
user: NDKUser,
): Promise<string[]> {
try {
const blockedRelayEvent = await ndk.fetchEvent(
{
kinds: [10006],
authors: [user.pubkey],
},
{
groupable: false,
skipVerification: false,
skipValidation: false,
},
);
if (!blockedRelayEvent) {
return [];
}
const blockedRelays: string[] = [];
blockedRelayEvent.tags.forEach((tag) => {
if (tag[0] === "r" && tag[1]) {
blockedRelays.push(tag[1]);
}
});
return blockedRelays;
} catch (error) {
console.info(
"[relay_management.ts] Error fetching user blocked relays:",
error,
);
return [];
}
}
/**
* Fetches user's outbox relays from NIP-65 relay list
* @param ndk NDK instance
* @param user User to fetch outbox relays for
* @returns Promise that resolves to array of outbox relay URLs
*/
export async function getUserOutboxRelays(
ndk: NDK,
user: NDKUser,
): Promise<string[]> {
try {
console.debug(
"[relay_management.ts] Fetching outbox relays for user:",
user.pubkey,
);
const relayList = await ndk.fetchEvent(
{
kinds: [10002],
authors: [user.pubkey],
},
{
groupable: false,
skipVerification: false,
skipValidation: false,
},
);
if (!relayList) {
console.debug("[relay_management.ts] No relay list found for user");
return [];
}
console.debug(
"[relay_management.ts] Found relay list event:",
relayList.id,
);
console.debug("[relay_management.ts] Relay list tags:", relayList.tags);
const outboxRelays: string[] = [];
relayList.tags.forEach((tag) => {
console.debug("[relay_management.ts] Processing tag:", tag);
if (tag[0] === "w" && tag[1]) {
outboxRelays.push(tag[1]);
console.debug("[relay_management.ts] Added outbox relay:", tag[1]);
} else if (tag[0] === "r" && tag[1]) {
// Some relay lists use 'r' for both inbox and outbox
outboxRelays.push(tag[1]);
console.debug("[relay_management.ts] Added relay (r tag):", tag[1]);
} else {
console.debug(
"[relay_management.ts] Skipping tag:",
tag[0],
"value:",
tag[1],
);
}
});
console.debug("[relay_management.ts] Final outbox relays:", outboxRelays);
return outboxRelays;
} catch (error) {
console.info(
"[relay_management.ts] Error fetching user outbox relays:",
error,
);
return [];
}
}
/**
* Gets browser extension's relay configuration by querying the extension directly
* @returns Promise that resolves to array of extension relay URLs
*/
export async function getExtensionRelays(): Promise<string[]> {
try {
// Check if we're in a browser environment with extension support
if (typeof window === "undefined" || !globalThis.nostr) {
console.debug("[relay_management.ts] No globalThis.nostr available");
return [];
}
console.debug(
"[relay_management.ts] Extension available, checking for getRelays()",
);
const extensionRelays: string[] = [];
// Try to get relays from the extension's API
// Different extensions may expose their relay config differently
if (globalThis.nostr.getRelays) {
console.debug(
"[relay_management.ts] getRelays() method found, calling it...",
);
try {
const relays = await globalThis.nostr.getRelays();
console.debug("[relay_management.ts] getRelays() returned:", relays);
if (relays && typeof relays === "object") {
// Convert relay object to array of URLs
const relayUrls = Object.keys(relays);
extensionRelays.push(...relayUrls);
console.debug(
"[relay_management.ts] Got relays from extension:",
relayUrls,
);
}
} catch (error) {
console.debug(
"[relay_management.ts] Extension getRelays() failed:",
error,
);
}
} else {
console.debug(
"[relay_management.ts] getRelays() method not found on globalThis.nostr",
);
}
// If getRelays() didn't work, try alternative methods
if (extensionRelays.length === 0) {
// Some extensions might expose relays through other methods
// This is a fallback for extensions that don't expose getRelays()
console.debug(
"[relay_management.ts] Extension does not expose relay configuration",
);
}
console.debug(
"[relay_management.ts] Final extension relays:",
extensionRelays,
);
return extensionRelays;
} catch (error) {
console.debug(
"[relay_management.ts] Error getting extension relays:",
error,
);
return [];
}
}
/**
* Tests a set of relays in batches to avoid overwhelming them
* @param relayUrls Array of relay URLs to test
* @param ndk NDK instance
* @returns Promise that resolves to array of working relay URLs
*/
async function testRelaySet(relayUrls: string[], ndk: NDK): Promise<string[]> {
const workingRelays: string[] = [];
const maxConcurrent = 2; // Reduce to 2 relays at a time to avoid overwhelming them
console.debug(
`[relay_management.ts] Testing ${relayUrls.length} relays in batches of ${maxConcurrent}`,
);
console.debug(`[relay_management.ts] Relay URLs to test:`, relayUrls);
for (let i = 0; i < relayUrls.length; i += maxConcurrent) {
const batch = relayUrls.slice(i, i + maxConcurrent);
console.debug(
`[relay_management.ts] Testing batch ${
Math.floor(i / maxConcurrent) + 1
}:`,
batch,
);
const batchPromises = batch.map(async (url) => {
try {
console.debug(`[relay_management.ts] Testing relay: ${url}`);
const result = await testRelayConnection(url, ndk);
console.debug(
`[relay_management.ts] Relay ${url} test result:`,
result,
);
return result.connected ? url : null;
} catch (error) {
console.debug(
`[relay_management.ts] Failed to test relay ${url}:`,
error,
);
return null;
}
});
const batchResults = await Promise.allSettled(batchPromises);
const batchWorkingRelays = batchResults
.filter((result): result is PromiseFulfilledResult<string | null> =>
result.status === "fulfilled"
)
.map((result) => result.value)
.filter((url): url is string => url !== null);
console.debug(
`[relay_management.ts] Batch ${
Math.floor(i / maxConcurrent) + 1
} working relays:`,
batchWorkingRelays,
);
workingRelays.push(...batchWorkingRelays);
}
console.debug(
`[relay_management.ts] Total working relays after testing:`,
workingRelays,
);
return workingRelays;
}
/**
* Builds a complete relay set for a user, including local, user-specific, and fallback relays
* @param ndk NDK instance
* @param user NDKUser or null for anonymous access
* @returns Promise that resolves to inbox and outbox relay arrays
*/
export async function buildCompleteRelaySet(
ndk: NDK,
user: NDKUser | null,
): Promise<{ inboxRelays: string[]; outboxRelays: string[] }> {
console.debug(
"[relay_management.ts] buildCompleteRelaySet: Starting with user:",
user?.pubkey || "null",
);
// Discover local relays first
const discoveredLocalRelays = await discoverLocalRelays(ndk);
console.debug(
"[relay_management.ts] buildCompleteRelaySet: Discovered local relays:",
discoveredLocalRelays,
);
// Get user-specific relays if available
let userOutboxRelays: string[] = [];
let userLocalRelays: string[] = [];
let blockedRelays: string[] = [];
let extensionRelays: string[] = [];
if (user) {
console.debug(
"[relay_management.ts] buildCompleteRelaySet: Fetching user-specific relays for:",
user.pubkey,
);
try {
userOutboxRelays = await getUserOutboxRelays(ndk, user);
console.debug(
"[relay_management.ts] buildCompleteRelaySet: User outbox relays:",
userOutboxRelays,
);
} catch (error) {
console.debug(
"[relay_management.ts] Error fetching user outbox relays:",
error,
);
}
try {
userLocalRelays = await getUserLocalRelays(ndk, user);
console.debug(
"[relay_management.ts] buildCompleteRelaySet: User local relays:",
userLocalRelays,
);
} catch (error) {
console.debug(
"[relay_management.ts] Error fetching user local relays:",
error,
);
}
try {
blockedRelays = await getUserBlockedRelays(ndk, user);
console.debug(
"[relay_management.ts] buildCompleteRelaySet: User blocked relays:",
blockedRelays,
);
} catch {
// Silently ignore blocked relay fetch errors
}
try {
extensionRelays = await getExtensionRelays();
console.debug(
"[relay_management.ts] Extension relays gathered:",
extensionRelays,
);
} catch (error) {
console.debug(
"[relay_management.ts] Error fetching extension relays:",
error,
);
}
} else {
console.debug(
"[relay_management.ts] buildCompleteRelaySet: No user provided, skipping user-specific relays",
);
}
// Build initial relay sets and deduplicate
const finalInboxRelays = deduplicateRelayUrls([
...discoveredLocalRelays,
...userLocalRelays,
]);
const finalOutboxRelays = deduplicateRelayUrls([
...discoveredLocalRelays,
...userOutboxRelays,
...extensionRelays,
]);
// Test relays and filter out non-working ones
let testedInboxRelays: string[] = [];
let testedOutboxRelays: string[] = [];
if (finalInboxRelays.length > 0) {
testedInboxRelays = await testRelaySet(finalInboxRelays, ndk);
}
if (finalOutboxRelays.length > 0) {
testedOutboxRelays = await testRelaySet(finalOutboxRelays, ndk);
}
// If no relays passed testing, use remote relays without testing
if (testedInboxRelays.length === 0 && testedOutboxRelays.length === 0) {
const remoteRelays = deduplicateRelayUrls([
...secondaryRelays,
...searchRelays,
]);
return {
inboxRelays: remoteRelays,
outboxRelays: remoteRelays,
};
}
// Always include some remote relays as fallback, even when local relays are working
const fallbackRelays = deduplicateRelayUrls([
...anonymousRelays,
...secondaryRelays,
]);
// Use tested relays and add fallback relays
const inboxRelays = testedInboxRelays.length > 0
? deduplicateRelayUrls([...testedInboxRelays, ...fallbackRelays])
: deduplicateRelayUrls(fallbackRelays);
const outboxRelays = testedOutboxRelays.length > 0
? deduplicateRelayUrls([...testedOutboxRelays, ...fallbackRelays])
: deduplicateRelayUrls(fallbackRelays);
// Apply network condition optimization
const currentNetworkCondition = get(networkCondition);
const networkOptimizedRelaySet = getRelaySetForNetworkCondition(
currentNetworkCondition,
discoveredLocalRelays,
lowbandwidthRelays,
{ inboxRelays, outboxRelays },
);
// Filter out blocked relays and deduplicate final sets
const finalRelaySet = {
inboxRelays: deduplicateRelayUrls(
networkOptimizedRelaySet.inboxRelays.filter((r: string) =>
!blockedRelays.includes(r)
),
),
outboxRelays: deduplicateRelayUrls(
networkOptimizedRelaySet.outboxRelays.filter((r: string) =>
!blockedRelays.includes(r)
),
),
};
// Ensure we always have at least some relays
if (
finalRelaySet.inboxRelays.length === 0 &&
finalRelaySet.outboxRelays.length === 0
) {
console.warn(
"[relay_management.ts] No relays available, using anonymous relays as final fallback",
);
return {
inboxRelays: deduplicateRelayUrls(anonymousRelays),
outboxRelays: deduplicateRelayUrls(anonymousRelays),
};
}
console.debug(
"[relay_management.ts] buildCompleteRelaySet: Final relay sets - inbox:",
finalRelaySet.inboxRelays.length,
"outbox:",
finalRelaySet.outboxRelays.length,
);
console.debug(
"[relay_management.ts] buildCompleteRelaySet: Final inbox relays:",
finalRelaySet.inboxRelays,
);
console.debug(
"[relay_management.ts] buildCompleteRelaySet: Final outbox relays:",
finalRelaySet.outboxRelays,
);
return finalRelaySet;
}