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. 12
      src/ui/settingsTab.ts

1
src/main.ts

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

10
src/nostr/relayClient.ts

@ -2,6 +2,7 @@ import { Relay } from "nostr-tools"; @@ -2,6 +2,7 @@ import { Relay } from "nostr-tools";
import { SignedEvent, PublishingResult } from "../types";
import { ensureAuthenticated, handleAuthRequiredError } from "./authHandler";
import { safeConsoleError } from "../utils/security";
import { deduplicateRelayUrls, normalizeRelayUrl } from "../relayManager";
/**
* Publish a single event to a relay
@ -118,11 +119,18 @@ export async function publishEventsWithRetry( @@ -118,11 +119,18 @@ export async function publishEventsWithRetry(
privkey: string,
maxRetries: number = 3
): 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 results: PublishingResult[][] = [];
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
const allSucceeded = results.some((relayResults) =>

88
src/relayManager.ts

@ -29,7 +29,7 @@ export function parseRelayList(event: any): RelayInfo[] { @@ -29,7 +29,7 @@ export function parseRelayList(event: any): RelayInfo[] {
for (const tag of event.tags) {
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 write = tag.length > 2 ? tag[2] === "write" || tag[2] === undefined : true;
@ -77,7 +77,8 @@ export async function fetchRelayListFromRelay( @@ -77,7 +77,8 @@ export async function fetchRelayListFromRelay(
clearTimeout(timer);
relay?.close();
const relayList = parseRelayList(event);
resolve(relayList.length > 0 ? relayList : null);
const normalized = normalizeRelayList(relayList);
resolve(normalized.length > 0 ? normalized : null);
},
oneose: () => {
clearTimeout(timer);
@ -123,10 +124,10 @@ export async function fetchRelayList( @@ -123,10 +124,10 @@ export async function fetchRelayList(
}
}
// If none found, return default fallback
// If none found, return default fallback (normalized)
return [
{
url: DEFAULT_FALLBACK_RELAY,
url: normalizeRelayUrl(DEFAULT_FALLBACK_RELAY),
read: true,
write: true,
},
@ -134,17 +135,82 @@ export async function fetchRelayList( @@ -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[] {
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[] {
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[] { @@ -162,12 +228,12 @@ export function addTheCitadelIfMissing(relayList: RelayInfo[]): RelayInfo[] {
return relayList;
}
return [
return normalizeRelayList([
...relayList,
{
url: DEFAULT_FALLBACK_RELAY,
url: normalizeRelayUrl(DEFAULT_FALLBACK_RELAY),
read: true,
write: true,
},
];
]);
}

12
src/ui/settingsTab.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { App, PluginSettingTab, Setting, Notice } from "obsidian";
import ScriptoriumPlugin from "../main";
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 { fetchUserProfile } from "../nostr/profileFetcher";
@ -241,7 +241,8 @@ export class ScriptoriumSettingTab extends PluginSettingTab { @@ -241,7 +241,8 @@ export class ScriptoriumSettingTab extends PluginSettingTab {
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.display();
} catch (error: any) {
@ -290,12 +291,15 @@ export class ScriptoriumSettingTab extends PluginSettingTab { @@ -290,12 +291,15 @@ export class ScriptoriumSettingTab extends PluginSettingTab {
const input = button.buttonEl.previousElementSibling as HTMLInputElement;
const url = input.value.trim();
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({
url,
url: normalizedUrl,
read: true,
write: true,
});
// Normalize and deduplicate the entire list
this.plugin.settings.relayList = normalizeRelayList(this.plugin.settings.relayList);
await this.plugin.saveSettings();
await this.display();
}

Loading…
Cancel
Save