Browse Source

normalize and deduplicate relays

master
Silberengel 1 week ago
parent
commit
338962eccf
  1. 1
      src/main.ts
  2. 10
      src/nostr/relayClient.ts
  3. 88
      src/relayManager.ts
  4. 14
      src/ui/settingsTab.ts

1
src/main.ts

@ -256,6 +256,7 @@ export default class ScriptoriumPlugin extends Plugin {
return; return;
} }
// Relays are already normalized and deduplicated by getWriteRelays
new Notice(`Publishing ${events.length} event(s) to ${writeRelays.length} relay(s)...`); new Notice(`Publishing ${events.length} event(s) to ${writeRelays.length} relay(s)...`);
const results = await publishEventsWithRetry(writeRelays, events, this.settings.privateKey); const results = await publishEventsWithRetry(writeRelays, events, this.settings.privateKey);

10
src/nostr/relayClient.ts

@ -2,6 +2,7 @@ import { Relay } from "nostr-tools";
import { SignedEvent, PublishingResult } from "../types"; import { SignedEvent, PublishingResult } from "../types";
import { ensureAuthenticated, handleAuthRequiredError } from "./authHandler"; import { ensureAuthenticated, handleAuthRequiredError } from "./authHandler";
import { safeConsoleError } from "../utils/security"; import { safeConsoleError } from "../utils/security";
import { deduplicateRelayUrls, normalizeRelayUrl } from "../relayManager";
/** /**
* Publish a single event to a relay * Publish a single event to a relay
@ -118,11 +119,18 @@ export async function publishEventsWithRetry(
privkey: string, privkey: string,
maxRetries: number = 3 maxRetries: number = 3
): Promise<PublishingResult[][]> { ): Promise<PublishingResult[][]> {
// Normalize and deduplicate relay URLs before publishing
const normalizedUrls = deduplicateRelayUrls(relayUrls.map(url => normalizeRelayUrl(url)));
if (normalizedUrls.length === 0) {
return [];
}
let attempts = 0; let attempts = 0;
let results: PublishingResult[][] = []; let results: PublishingResult[][] = [];
while (attempts < maxRetries) { while (attempts < maxRetries) {
results = await publishEventsToRelays(relayUrls, events, privkey); results = await publishEventsToRelays(normalizedUrls, events, privkey);
// Check if all events succeeded on at least one relay // Check if all events succeeded on at least one relay
const allSucceeded = results.some((relayResults) => const allSucceeded = results.some((relayResults) =>

88
src/relayManager.ts

@ -29,7 +29,7 @@ export function parseRelayList(event: any): RelayInfo[] {
for (const tag of event.tags) { for (const tag of event.tags) {
if (tag[0] === "r" && tag[1]) { if (tag[0] === "r" && tag[1]) {
const url = tag[1]; const url = normalizeRelayUrl(tag[1]);
const read = tag.length > 2 ? tag[2] === "read" || tag[2] === undefined : true; const read = tag.length > 2 ? tag[2] === "read" || tag[2] === undefined : true;
const write = tag.length > 2 ? tag[2] === "write" || tag[2] === undefined : true; const write = tag.length > 2 ? tag[2] === "write" || tag[2] === undefined : true;
@ -77,7 +77,8 @@ export async function fetchRelayListFromRelay(
clearTimeout(timer); clearTimeout(timer);
relay?.close(); relay?.close();
const relayList = parseRelayList(event); const relayList = parseRelayList(event);
resolve(relayList.length > 0 ? relayList : null); const normalized = normalizeRelayList(relayList);
resolve(normalized.length > 0 ? normalized : null);
}, },
oneose: () => { oneose: () => {
clearTimeout(timer); clearTimeout(timer);
@ -123,10 +124,10 @@ export async function fetchRelayList(
} }
} }
// If none found, return default fallback // If none found, return default fallback (normalized)
return [ return [
{ {
url: DEFAULT_FALLBACK_RELAY, url: normalizeRelayUrl(DEFAULT_FALLBACK_RELAY),
read: true, read: true,
write: true, write: true,
}, },
@ -134,17 +135,82 @@ export async function fetchRelayList(
} }
/** /**
* Get write relays from relay list * Normalize a relay URL
* - Removes trailing slashes
* - Ensures lowercase
* - Validates wss:// or ws:// protocol
*/
export function normalizeRelayUrl(url: string): string {
if (!url) return url;
let normalized = url.trim().toLowerCase();
// Remove trailing slashes
normalized = normalized.replace(/\/+$/, "");
// Ensure protocol is present
if (!normalized.startsWith("wss://") && !normalized.startsWith("ws://")) {
// Default to wss:// if no protocol
normalized = "wss://" + normalized;
}
return normalized;
}
/**
* Deduplicate relay URLs by normalizing and comparing
*/
export function deduplicateRelayUrls(urls: string[]): string[] {
const seen = new Set<string>();
const unique: string[] = [];
for (const url of urls) {
const normalized = normalizeRelayUrl(url);
if (!seen.has(normalized)) {
seen.add(normalized);
unique.push(normalized);
}
}
return unique;
}
/**
* Normalize and deduplicate relay list
*/
export function normalizeRelayList(relayList: RelayInfo[]): RelayInfo[] {
const seen = new Set<string>();
const normalized: RelayInfo[] = [];
for (const relay of relayList) {
const normalizedUrl = normalizeRelayUrl(relay.url);
if (!seen.has(normalizedUrl)) {
seen.add(normalizedUrl);
normalized.push({
url: normalizedUrl,
read: relay.read,
write: relay.write,
});
}
}
return normalized;
}
/**
* Get write relays from relay list (normalized and deduplicated)
*/ */
export function getWriteRelays(relayList: RelayInfo[]): string[] { export function getWriteRelays(relayList: RelayInfo[]): string[] {
return relayList.filter((r) => r.write).map((r) => r.url); const writeRelays = relayList.filter((r) => r.write).map((r) => r.url);
return deduplicateRelayUrls(writeRelays);
} }
/** /**
* Get read relays from relay list * Get read relays from relay list (normalized and deduplicated)
*/ */
export function getReadRelays(relayList: RelayInfo[]): string[] { export function getReadRelays(relayList: RelayInfo[]): string[] {
return relayList.filter((r) => r.read).map((r) => r.url); const readRelays = relayList.filter((r) => r.read).map((r) => r.url);
return deduplicateRelayUrls(readRelays);
} }
/** /**
@ -162,12 +228,12 @@ export function addTheCitadelIfMissing(relayList: RelayInfo[]): RelayInfo[] {
return relayList; return relayList;
} }
return [ return normalizeRelayList([
...relayList, ...relayList,
{ {
url: DEFAULT_FALLBACK_RELAY, url: normalizeRelayUrl(DEFAULT_FALLBACK_RELAY),
read: true, read: true,
write: true, write: true,
}, },
]; ]);
} }

14
src/ui/settingsTab.ts

@ -1,7 +1,7 @@
import { App, PluginSettingTab, Setting, Notice } from "obsidian"; import { App, PluginSettingTab, Setting, Notice } from "obsidian";
import ScriptoriumPlugin from "../main"; import ScriptoriumPlugin from "../main";
import { EventKind } from "../types"; import { EventKind } from "../types";
import { fetchRelayList, addTheCitadelIfMissing, includesTheCitadel, getReadRelays } from "../relayManager"; import { fetchRelayList, addTheCitadelIfMissing, includesTheCitadel, getReadRelays, normalizeRelayUrl, normalizeRelayList } from "../relayManager";
import { getPubkeyFromPrivkey, getNpubFromPrivkey } from "../nostr/eventBuilder"; import { getPubkeyFromPrivkey, getNpubFromPrivkey } from "../nostr/eventBuilder";
import { fetchUserProfile } from "../nostr/profileFetcher"; import { fetchUserProfile } from "../nostr/profileFetcher";
@ -240,8 +240,9 @@ export class ScriptoriumSettingTab extends PluginSettingTab {
if (this.plugin.settings.suggestTheCitadel && !includesTheCitadel(relayList)) { if (this.plugin.settings.suggestTheCitadel && !includesTheCitadel(relayList)) {
finalList = addTheCitadelIfMissing(relayList); finalList = addTheCitadelIfMissing(relayList);
} }
this.plugin.settings.relayList = finalList; // Normalize and deduplicate before saving
this.plugin.settings.relayList = normalizeRelayList(finalList);
await this.plugin.saveSettings(); await this.plugin.saveSettings();
await this.display(); await this.display();
} catch (error: any) { } catch (error: any) {
@ -290,12 +291,15 @@ export class ScriptoriumSettingTab extends PluginSettingTab {
const input = button.buttonEl.previousElementSibling as HTMLInputElement; const input = button.buttonEl.previousElementSibling as HTMLInputElement;
const url = input.value.trim(); const url = input.value.trim();
if (url) { if (url) {
if (!this.plugin.settings.relayList.some((r) => r.url === url)) { const normalizedUrl = normalizeRelayUrl(url);
if (!this.plugin.settings.relayList.some((r) => normalizeRelayUrl(r.url) === normalizedUrl)) {
this.plugin.settings.relayList.push({ this.plugin.settings.relayList.push({
url, url: normalizedUrl,
read: true, read: true,
write: true, write: true,
}); });
// Normalize and deduplicate the entire list
this.plugin.settings.relayList = normalizeRelayList(this.plugin.settings.relayList);
await this.plugin.saveSettings(); await this.plugin.saveSettings();
await this.display(); await this.display();
} }

Loading…
Cancel
Save