Merge pull request #1391 from PR0M3TH3AN/codex/prioritize-kind-10050-relays-in-dms

Prioritize NIP-17 relay hints for DM send/subscribe
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-19 10:31:29 -05:00
commit e1d58a2530
5 files changed
+194 -80

No files matched your search

+3 -5
View File
@@ -3223,11 +3223,9 @@ class Application {
: [];
if (enabled && !relayHints.length) {
this.showError("Recipient has not shared NIP-17 relay hints yet.");
if (controller?.setPrivacyToggleState) {
controller.setPrivacyToggleState(false);
}
return;
this.showStatus(
"Privacy warning: this recipient has not shared NIP-17 relays, so we'll use your default relays.",
);
}
this.showStatus(
+44 -60
View File
@@ -113,6 +113,10 @@ import {
} from "./dmDecryptWorkerClient.js";
import { devLogger, userLogger } from "../utils/logger.js";
import { updateConversationFromMessage, writeMessages } from "../storage/dmDb.js";
import {
DM_RELAY_WARNING_FALLBACK,
resolveDmRelaySelection,
} from "../services/dmNostrService.js";
import {
DEFAULT_NIP07_ENCRYPTION_METHODS,
DEFAULT_NIP07_PERMISSION_METHODS,
@@ -4480,68 +4484,14 @@ export class NostrClient {
const signerCapabilities = resolveSignerCapabilities(signer);
const useNip17 = Boolean(resolvedOptions.useNip17);
const resolveNip17RelayList = async (pubkey, relayHints) => {
const direct = sanitizeRelayList(relayHints);
if (direct.length) {
return direct;
}
const relayCandidates = sanitizeRelayList(
const resolveNip17RelaySelection = async (pubkey, relayHints) => {
const discoveryRelays = sanitizeRelayList(
Array.isArray(this.readRelays) && this.readRelays.length
? this.readRelays
: Array.isArray(this.relays) && this.relays.length
? this.relays
: RELAY_URLS,
);
if (!relayCandidates.length || !this.pool) {
return [];
}
try {
await this.ensurePool();
const events = await this.pool.list(relayCandidates, [
{ kinds: [10050], authors: [pubkey], limit: 1 },
]);
const sorted = Array.isArray(events)
? events
.filter((entry) => entry && entry.pubkey === pubkey)
.sort((a, b) => (b?.created_at || 0) - (a?.created_at || 0))
: [];
if (!sorted.length) {
return [];
}
const tags = Array.isArray(sorted[0]?.tags) ? sorted[0].tags : [];
const relayList = tags
.filter((tag) => Array.isArray(tag) && tag[0] === "relay")
.map((tag) => (typeof tag[1] === "string" ? tag[1].trim() : ""))
.filter(Boolean);
return sanitizeRelayList(relayList);
} catch (error) {
devLogger.warn("[nostr] Failed to load NIP-17 relay list.", error);
return [];
}
};
if (useNip17) {
if (!signerCapabilities.nip44 || typeof signer.nip44Encrypt !== "function") {
return { ok: false, error: "nip44-unsupported" };
}
const recipientRelays = await resolveNip17RelayList(
targetHex,
resolvedOptions.recipientRelayHints,
);
if (!recipientRelays.length) {
return { ok: false, error: "nip17-relays-missing" };
}
const senderRelays = await resolveNip17RelayList(
actorHex,
resolvedOptions.senderRelayHints,
);
const fallbackRelays = sanitizeRelayList(
Array.isArray(this.writeRelays) && this.writeRelays.length
? this.writeRelays
@@ -4549,7 +4499,41 @@ export class NostrClient {
? this.relays
: RELAY_URLS,
);
const senderRelayTargets = senderRelays.length ? senderRelays : fallbackRelays;
return resolveDmRelaySelection({
pubkey,
relayHints,
discoveryRelays,
fallbackRelays,
pool: this.pool,
log: { dev: devLogger, user: userLogger },
});
};
if (useNip17) {
if (!signerCapabilities.nip44 || typeof signer.nip44Encrypt !== "function") {
return { ok: false, error: "nip44-unsupported" };
}
const recipientSelection = await resolveNip17RelaySelection(
targetHex,
resolvedOptions.recipientRelayHints,
);
if (!recipientSelection.relays.length) {
return { ok: false, error: "nip17-relays-unavailable" };
}
const senderSelection = await resolveNip17RelaySelection(
actorHex,
resolvedOptions.senderRelayHints,
);
const senderRelayTargets = senderSelection.relays;
const relayWarning =
recipientSelection.warning === DM_RELAY_WARNING_FALLBACK ||
senderSelection.warning === DM_RELAY_WARNING_FALLBACK
? DM_RELAY_WARNING_FALLBACK
: null;
const tools = await ensureNostrTools();
const getPublicKey =
@@ -4690,7 +4674,7 @@ export class NostrClient {
try {
recipientGiftWrap = await buildGiftWrapForRecipient(
targetHex,
recipientRelays[0] || "",
recipientSelection.relays[0] || "",
);
} catch (error) {
devLogger.warn("[nostr] Failed to build NIP-17 recipient wrap.", error);
@@ -4726,7 +4710,7 @@ export class NostrClient {
}
const recipientPublishResults = await Promise.all(
recipientRelays.map((url) =>
recipientSelection.relays.map((url) =>
publishEventToRelay(this.pool, url, recipientGiftWrap.wrap),
),
);
@@ -4776,7 +4760,7 @@ export class NostrClient {
devLogger.warn("[nostr] Failed to persist DM publish state.", error);
}
return { ok: true };
return relayWarning ? { ok: true, warning: relayWarning } : { ok: true };
}
const encryptionErrors = [];
+73
View File
@@ -1,4 +1,5 @@
import { ensureNostrTools, resolveSimplePoolConstructor, shimLegacySimplePoolMethods } from "../nostr/toolkit.js";
import { sanitizeRelayList } from "../nostr/nip46Client.js";
import { publishEventToRelay } from "../nostrPublish.js";
import logger from "../utils/logger.js";
@@ -9,6 +10,7 @@ const DEFAULT_BACKOFF = {
};
const MAX_SEEN_EVENT_IDS = 5000;
export const DM_RELAY_WARNING_FALLBACK = "dm-relays-fallback";
function normalizeRelayList(relays) {
if (!Array.isArray(relays)) {
@@ -44,6 +46,77 @@ function normalizeLogger(candidate) {
return logger;
}
function extractRelayHintsFromEvent(event) {
const tags = Array.isArray(event?.tags) ? event.tags : [];
return sanitizeRelayList(
tags
.filter((tag) => Array.isArray(tag) && tag[0] === "relay")
.map((tag) => (typeof tag[1] === "string" ? tag[1].trim() : "")),
);
}
export async function resolveDmRelaySelection({
pubkey,
relayHints = [],
discoveryRelays = [],
fallbackRelays = [],
pool = null,
log = null,
} = {}) {
const resolvedLogger = normalizeLogger(log || logger);
const normalizedPubkey = normalizePubkey(pubkey);
const hintedRelays = sanitizeRelayList(Array.isArray(relayHints) ? relayHints : []);
if (hintedRelays.length) {
return { relays: hintedRelays, source: "hints", warning: null };
}
const discoveryList = sanitizeRelayList(
Array.isArray(discoveryRelays) ? discoveryRelays : [],
);
const fallbackList = sanitizeRelayList(
Array.isArray(fallbackRelays) && fallbackRelays.length
? fallbackRelays
: discoveryRelays,
);
if (
normalizedPubkey &&
pool &&
typeof pool.list === "function" &&
discoveryList.length
) {
try {
const events = await pool.list(discoveryList, [
{ kinds: [10050], authors: [normalizedPubkey], limit: 1 },
]);
const sorted = Array.isArray(events)
? events
.filter((entry) => entry && entry.pubkey === normalizedPubkey)
.sort((a, b) => (b?.created_at || 0) - (a?.created_at || 0))
: [];
if (sorted.length) {
const relays = extractRelayHintsFromEvent(sorted[0]);
if (relays.length) {
return { relays, source: "kind-10050", warning: null };
}
}
} catch (error) {
resolvedLogger.dev.warn("[dmNostrService] Failed to fetch DM relay hints.", error);
}
}
if (fallbackList.length) {
return {
relays: fallbackList,
source: "fallback",
warning: DM_RELAY_WARNING_FALLBACK,
};
}
return { relays: [], source: "none", warning: null };
}
function clampNumber(value, minimum, maximum) {
if (!Number.isFinite(value)) {
return minimum;
+44 -3
View File
@@ -2,8 +2,12 @@ import { nostrClient } from "../nostrClientFacade.js";
import { convertEventToVideo } from "../nostr/index.js";
import { accessControl } from "../accessControl.js";
import { ALLOW_NSFW_CONTENT } from "../config.js";
import { userLogger } from "../utils/logger.js";
import { devLogger, userLogger } from "../utils/logger.js";
import moderationService from "./moderationService.js";
import {
DM_RELAY_WARNING_FALLBACK,
resolveDmRelaySelection,
} from "./dmNostrService.js";
import {
loadDirectMessageSnapshot,
saveDirectMessageSnapshot,
@@ -874,7 +878,7 @@ export class NostrService {
return snapshot;
}
ensureDirectMessageSubscription({ actorPubkey, relays, ...handlers } = {}) {
async ensureDirectMessageSubscription({ actorPubkey, relays, ...handlers } = {}) {
if (this.dmSubscription) {
return this.dmSubscription;
}
@@ -885,11 +889,40 @@ export class NostrService {
return null;
}
let resolvedRelays = Array.isArray(relays) ? relays : null;
let relaySelection = null;
if (!resolvedRelays) {
const discoveryRelays = Array.isArray(this.nostrClient?.readRelays)
? this.nostrClient.readRelays
: Array.isArray(this.nostrClient?.relays)
? this.nostrClient.relays
: [];
let pool = this.nostrClient?.pool || null;
if (!pool && typeof this.nostrClient?.ensurePool === "function") {
try {
pool = await this.nostrClient.ensurePool();
} catch (error) {
devLogger.warn("[nostrService] Failed to initialize DM relay pool.", error);
}
}
relaySelection = await resolveDmRelaySelection({
pubkey: normalizedActor,
discoveryRelays,
fallbackRelays: discoveryRelays,
pool,
log: { dev: devLogger, user: userLogger },
});
resolvedRelays = relaySelection.relays;
}
try {
const subscription = this.nostrClient.subscribeDirectMessages(
normalizedActor,
{
relays,
relays: resolvedRelays,
onEvent: handlers.onEvent,
onMessage: (message, context = {}) => {
this.applyDirectMessage(message, {
@@ -957,6 +990,14 @@ export class NostrService {
this.dmSubscription = subscription;
this.dmActorPubkey = normalizedActor;
this.emit("directMessages:subscribed", { subscription });
if (relaySelection?.warning === DM_RELAY_WARNING_FALLBACK) {
this.emit("directMessages:relayWarning", {
actorPubkey: normalizedActor,
warning: relaySelection.warning,
relays: resolvedRelays,
source: relaySelection.source,
});
}
return subscription;
} catch (error) {
userLogger.warn(
+30 -12
View File
@@ -1752,6 +1752,9 @@ export class ProfileModalController {
subscribe("directMessages:failure", (detail) => {
this.handleDirectMessagesError(detail);
});
subscribe("directMessages:relayWarning", (detail) => {
this.handleDirectMessagesRelayWarning(detail);
});
this.directMessagesUnsubscribes = unsubscribes;
@@ -1995,7 +1998,7 @@ export class ProfileModalController {
}
}
ensureDirectMessageSubscription(actorPubkey = null) {
async ensureDirectMessageSubscription(actorPubkey = null) {
if (
!this.nostrService ||
typeof this.nostrService.ensureDirectMessageSubscription !== "function"
@@ -2028,7 +2031,7 @@ export class ProfileModalController {
let subscription = null;
try {
subscription = this.nostrService.ensureDirectMessageSubscription({
subscription = await this.nostrService.ensureDirectMessageSubscription({
actorPubkey: normalizedActor,
});
} catch (error) {
@@ -2099,7 +2102,7 @@ export class ProfileModalController {
}
this.setMessagesLoadingState("loading");
this.ensureDirectMessageSubscription(normalized);
void this.ensureDirectMessageSubscription(normalized);
this.updateMessagesReloadState();
if (this.getActivePane() === "messages") {
@@ -2329,10 +2332,9 @@ export class ProfileModalController {
: [];
if (enabled && !relayHints.length) {
this.showError("Recipient has not shared NIP-17 relay hints yet.");
this.setPrivacyToggleState(false);
this.dmPrivacyToggleTouched = false;
return;
this.showStatus(
"Privacy warning: this recipient has not shared NIP-17 relays, so we'll use your default relays.",
);
}
this.dmPrivacyToggleTouched = true;
@@ -2356,6 +2358,8 @@ export class ProfileModalController {
return "Your signer does not support NIP-44 encryption required for NIP-17.";
case "nip17-relays-missing":
return "Recipient has not shared NIP-17 relay hints yet.";
case "nip17-relays-unavailable":
return "No DM relays are available to deliver this message.";
case "nip17-keygen-failed":
return "We couldn’t create secure wrapper keys for NIP-17 delivery.";
case "extension-permission-denied":
@@ -2411,10 +2415,9 @@ export class ProfileModalController {
: false;
if (useNip17 && !recipientRelayHints.length) {
this.showError("Recipient has not shared NIP-17 relay hints yet.");
this.setPrivacyToggleState(false);
this.dmPrivacyToggleTouched = false;
return;
this.showStatus(
"Privacy warning: this recipient has not shared NIP-17 relays, so we'll use your default relays.",
);
}
if (
@@ -2447,6 +2450,11 @@ export class ProfileModalController {
if (result?.ok) {
input.value = "";
this.showSuccess("Message sent.");
if (result?.warning === "dm-relays-fallback") {
this.showStatus(
"Privacy warning: this message used default relays because no NIP-17 relay list was found.",
);
}
void this.populateProfileMessages({ force: true, reason: "send-message" });
return;
}
@@ -2949,7 +2957,7 @@ export class ProfileModalController {
}
}
this.ensureDirectMessageSubscription(actor);
void this.ensureDirectMessageSubscription(actor);
}
resumeProfileMessages() {
@@ -3051,6 +3059,16 @@ export class ProfileModalController {
this.updateMessagesReloadState();
}
handleDirectMessagesRelayWarning(detail = {}) {
if (detail?.warning !== "dm-relays-fallback") {
return;
}
this.showStatus(
"Privacy warning: direct messages are using your default relays because no NIP-17 relay list is available.",
);
}
registerEventListeners() {
if (this.closeButton instanceof HTMLElement) {
this.closeButton.addEventListener("click", () => {