-
Notifications
You must be signed in to change notification settings - Fork 297
feat: add purplepag.es as profile-only relay #3459
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat: add purplepag.es as profile-only relay #3459
Conversation
Add purplepag.es to the default bootstrap relays for new accounts. Existing accounts can manually add it from the recommended relays tab. The relay is filtered to only receive profile-related queries (kinds 0, 3, 10002) regardless of how it was added, ensuring it's used optimally for profile metadata lookup. Changes: - Add purplepag.es to BOOTSTRAP_RELAYS for new accounts - Filter purplepag.es to only receive profile-related requests - Show "Profile" badge on purplepag.es in relay views - Keep isProfileRelated check for subscription filtering Closes: damus-io#1331 Closes: damus-io#3174 Signed-off-by: alltheseas 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
be1290c to
8e1d167
Compare
|
I learned that purplepag.es servers also sorts of non-kind1 kinds including e.g. blossom servers. Relates to #3218 |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
📝 WalkthroughWalkthroughThe changes implement a profile-only relay system for the purplepag.es relay. They introduce infrastructure to identify profile-related requests, mark relays as profile-only, filter profile-only relays for non-profile requests, and update the UI to display profile-only indicators and handle the new relay descriptor. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant RelayPool
participant RelayDescriptor
participant Filter
Client->>RelayPool: send_raw(request)
RelayPool->>Filter: isProfileRelated(request)
Filter->>Filter: check if all filters only contain<br/>profileKinds (metadata, contacts, relay_list)
Filter-->>RelayPool: Boolean result
loop For each relay descriptor
RelayPool->>RelayDescriptor: check if isProfilesOnly<br/>or isPurplePages
alt Relay is profile-only and request is NOT profile-related
RelayPool->>RelayPool: Skip relay, do not send
else Send to relay
RelayPool->>Client: Dispatch request to relay
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Fix all issues with AI Agents 🤖
In @damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift:
- Around line 40-42: The appended .nwc descriptor can duplicate an existing
descriptor with the same URL (often added as .regular), causing apply()'s
first(where: { $0.url == url }) to pick the wrong variant; before appending the
nwc descriptor in the block that reads delegate.nwcWallet, check descriptors for
an existing descriptor with the same relay URL and either replace/update its
variant to .nwc (or remove the existing and append the .nwc one) so the list
contains a single entry per URL; this ensures apply() will match the intended
.nwc variant rather than a conflicting .regular entry.
In @damus/Core/Nostr/NostrRequest.swift:
- Around line 46-55: The isProfileRelated logic incorrectly treats empty filters
or empty kinds as profile-only due to vacuous truth; update the check in
NostrRequest.isProfileRelated (the guard for .typical -> .subscribe and the
return using sub.filters.allSatisfy) to first return false if
sub.filters.isEmpty, and inside the allSatisfy closure require that filter.kinds
is non-nil AND not empty (e.g., guard let kinds = filter.kinds, !kinds.isEmpty
else { return false }) before checking kinds.allSatisfy {
Self.profileKinds.contains($0) } so empty filters or empty kinds do not get
classified as profile-related.
🧹 Nitpick comments (4)
damus/Features/Relays/Views/RelayConfigView.swift (1)
40-41: Consider creating profile-only descriptor for purplepag.es.All recommended relays are created with
.readWriteconfiguration, but purplepag.es is a profile-only relay. While the filtering logic inRelayPool.send_rawhandles this via URL detection, using the appropriate descriptor would be more consistent.🔎 Suggested improvement
var recommended: [RelayPool.RelayDescriptor] { - // Use default bootstrap relays (includes purplepag.es) - return get_default_bootstrap_relays().map { RelayPool.RelayDescriptor(url: $0, info: .readWrite) } + // Use default bootstrap relays (includes purplepag.es) + return get_default_bootstrap_relays().map { url in + if url.absoluteString.contains("purplepag.es") { + return RelayPool.RelayDescriptor.profilesOnly(url: url) + } + return RelayPool.RelayDescriptor(url: url, info: .readWrite) + } }damus/Core/Nostr/RelayPool.swift (1)
428-433: Consider centralizing theisPurplePagesRelaycheck.The
purplepag.esURL detection is duplicated inRelayView.swift(line 35),RelayDetailView.swift(line 32), and here. Consider extracting this to a single location (e.g., onRelayURLor as a shared utility) to ensure consistency and reduce duplication.🔎 Suggested centralization
Add an extension on
RelayURL:extension RelayURL { var isPurplePages: Bool { absoluteString.contains("purplepag.es") } }Then use it consistently:
- let isPurplePagesRelay = relay.descriptor.url.absoluteString.contains("purplepag.es") - if (relay.descriptor.isProfilesOnly || isPurplePagesRelay) && !req.isProfileRelated { + if (relay.descriptor.isProfilesOnly || relay.descriptor.url.isPurplePages) && !req.isProfileRelated {damus/Features/Relays/Views/RelayDetailView.swift (1)
31-42: DuplicateisPurplePagesRelayimplementation.This is the same implementation as in
RelayView.swift(line 35) and similar to the check inRelayPool.swift(line 430). As noted in the RelayPool review, consider centralizing this to a single location.damus/Features/Relays/Views/RelayView.swift (1)
34-45: DuplicateisPurplePagesRelay- consolidate with other files.Same duplication issue as noted in
RelayDetailView.swiftandRelayPool.swift. TheisProfileOnlylogic with its fallback chain is good, but the underlying helper should be centralized.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swiftdamus/Core/Networking/NostrNetworkManager/UserRelayListManager.swiftdamus/Core/Nostr/NostrRequest.swiftdamus/Core/Nostr/Relay.swiftdamus/Core/Nostr/RelayPool.swiftdamus/Features/Relays/Models/RelayBootstrap.swiftdamus/Features/Relays/Views/RelayConfigView.swiftdamus/Features/Relays/Views/RelayDetailView.swiftdamus/Features/Relays/Views/RelayType.swiftdamus/Features/Relays/Views/RelayView.swiftdamus/Features/Settings/Models/UserSettingsStore.swift
🧰 Additional context used
🧬 Code graph analysis (5)
damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift (1)
damus/Core/Nostr/Relay.swift (1)
nwc(68-70)
damus/Core/Nostr/RelayPool.swift (4)
damus/Features/Relays/Views/RelayDetailView.swift (1)
isPurplePagesRelay(32-34)damus/Features/Relays/Views/RelayView.swift (1)
isPurplePagesRelay(35-37)damus/Notify/Notify.swift (1)
notify(34-39)damus/Notify/UpdateStatsNotify.swift (1)
update_stats(22-24)
damus/Features/Relays/Views/RelayDetailView.swift (3)
damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift (1)
getUserCurrentRelayList(63-68)damus/Features/Relays/Views/RelayView.swift (1)
isPurplePagesRelay(35-37)damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift (1)
getRelay(125-128)
damus/Features/Relays/Views/RelayConfigView.swift (2)
damus/Features/Relays/Models/RelayBootstrap.swift (1)
get_default_bootstrap_relays(67-75)damus/Shared/Utilities/Log.swift (1)
info(58-60)
damus/Features/Relays/Views/RelayView.swift (1)
damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift (1)
getRelay(125-128)
🔇 Additional comments (14)
damus/Features/Settings/Models/UserSettingsStore.swift (1)
249-254: LGTM!This is a trivial whitespace change (blank line removal) with no semantic impact. The formatting is consistent with the rest of the file.
damus/Features/Relays/Models/RelayBootstrap.swift (1)
16-16: LGTM!Adding purplepag.es to bootstrap relays is appropriate. The profile-only query filtering is correctly handled in
RelayPool.send_raw.damus/Features/Relays/Views/RelayConfigView.swift (1)
132-137: LGTM!Properly passing the descriptor to
RelayViewenables correct profile-only badge rendering.damus/Core/Nostr/RelayPool.swift (1)
473-481: LGTM!Good refactoring to use guard statements for cleaner control flow. The logic remains functionally equivalent while improving readability.
damus/Core/Nostr/Relay.swift (2)
71-74: Verify read-only configuration for profile-only relays.The factory method creates descriptors with
.readconfiguration. If users should be able to publish their own profile metadata (kind 0) to purplepag.es, this should be.readWrite. The PR description mentions "profile metadata queries" which suggests read-only is intentional, but please verify this aligns with the expected behavior.
31-36: LGTM!The new
profilesOnlyvariant is well-documented and integrates cleanly with the existing enum structure.damus/Features/Relays/Views/RelayType.swift (1)
10-31: LGTM!Clean implementation of the profile-only badge. The
HStackproperly handles the case where both paid and profile-only badges need to be displayed together.damus/Features/Relays/Views/RelayDetailView.swift (2)
25-29: LGTM!The
isConnectedcomputed property cleanly encapsulates the connection check logic.
103-110: LGTM!Profile badge is correctly displayed for profile-only relays, and the existing paid relay logic is preserved.
damus/Features/Relays/Views/RelayView.swift (2)
16-31: LGTM!The descriptor parameter with proper initialization enables profile-only detection for both connected and recommended relays. The fallback chain in
isProfileOnlycorrectly handles all cases.
71-75: LGTM!
RelayTypeis correctly invoked with theis_profile_onlyflag derived from the newisProfileOnlycomputed property.damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift (1)
38-43: These changes don't match the PR objectives.The PR description states this is for adding purplepag.es as a profile-only relay, introducing
RelayVariant.profilesOnly, and filtering profile queries. However, these changes implement NWC (Nostr Wallet Connect) relay management, which is unrelated to the stated objectives.Are these changes from a different PR that were accidentally included, or is the PR description incorrect?
damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift (1)
216-217: These changes don't match the PR objectives.Similar to the changes in UserRelayListManager.swift, this adds NWC wallet functionality, which is unrelated to the stated PR objective of adding purplepag.es as a profile-only relay.
Please clarify whether these files are from the correct PR or if the PR description needs to be updated.
damus/Core/Nostr/NostrRequest.swift (1)
43-43: TheprofileKindsset is correctly configured for its intended purpose. The values[.metadata, .contacts, .relay_list](kinds 0, 3, 10002) are intentional and explicitly documented inRelayPool.swiftwith the comment: "Filter purplepag.es to only receive profile-related requests (kinds 0, 3, 10002)."These three kinds represent the core profile metadata: user profile (kind 0), contact list (kind 3), and relay preferences (kind 10002). While NIP-51 defines additional list kinds like mute list (10000) and pinned notes (10001), these are user preference/content lists rather than profile metadata. The purplepag.es relay is specifically designed for profile data, not general user lists. Additionally, kind 10001 is not defined in the codebase's
NostrKindenum, and kind 30008 does not exist in the Nostr specification.
| if let nwcWallet = delegate.nwcWallet { | ||
| return regularRelayDescriptorList + [.nwc(url: nwcWallet.relay)] | ||
| descriptors.append(.nwc(url: nwcWallet.relay)) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential duplicate relay with conflicting variants.
If a user manually adds their NWC relay URL to their relay list, it will be created with variant .regular (line 264). This code then appends the same URL again with variant .nwc. When apply() processes the list and calls first(where: { $0.url == url }) (line 230), it will select the first descriptor with that URL—likely the .regular variant—causing the NWC relay to be misconfigured.
🔎 Proposed fix to prevent duplicate relay URLs
private func computeRelaysToConnectTo(with relayList: NIP65.RelayList) -> [RelayPool.RelayDescriptor] {
var descriptors = relayList.toRelayDescriptors()
if let nwcWallet = delegate.nwcWallet {
- descriptors.append(.nwc(url: nwcWallet.relay))
+ // Remove any existing descriptor with the NWC relay URL to avoid conflicts
+ descriptors.removeAll(where: { $0.url == nwcWallet.relay })
+ descriptors.append(.nwc(url: nwcWallet.relay))
}
return descriptors
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if let nwcWallet = delegate.nwcWallet { | |
| return regularRelayDescriptorList + [.nwc(url: nwcWallet.relay)] | |
| descriptors.append(.nwc(url: nwcWallet.relay)) | |
| } | |
| if let nwcWallet = delegate.nwcWallet { | |
| // Remove any existing descriptor with the NWC relay URL to avoid conflicts | |
| descriptors.removeAll(where: { $0.url == nwcWallet.relay }) | |
| descriptors.append(.nwc(url: nwcWallet.relay)) | |
| } |
🤖 Prompt for AI Agents
In @damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift around
lines 40-42, The appended .nwc descriptor can duplicate an existing descriptor
with the same URL (often added as .regular), causing apply()'s first(where: {
$0.url == url }) to pick the wrong variant; before appending the nwc descriptor
in the block that reads delegate.nwcWallet, check descriptors for an existing
descriptor with the same relay URL and either replace/update its variant to .nwc
(or remove the existing and append the .nwc one) so the list contains a single
entry per URL; this ensures apply() will match the intended .nwc variant rather
than a conflicting .regular entry.
| var isProfileRelated: Bool { | ||
| guard case .typical(let req) = self else { return false } | ||
| guard case .subscribe(let sub) = req else { return false } | ||
|
|
||
| // Check if ALL filters contain ONLY profile-related kinds | ||
| return sub.filters.allSatisfy { filter in | ||
| guard let kinds = filter.kinds else { return false } // No kinds specified = could be anything | ||
| return kinds.allSatisfy { Self.profileKinds.contains($0) } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against edge cases with empty filter or kinds arrays.
The allSatisfy method returns true for empty collections (vacuous truth), which could lead to incorrect classification:
-
Empty filters array: If
sub.filtersis empty, the function returnstrue, treating it as profile-related. However, a subscription with no filters would conceptually match all events and should not be sent to a profile-only relay. -
Empty kinds array: If
filter.kindsis an empty array (rather thannil), the innerallSatisfyreturnstrue, incorrectly treating it as profile-related. An empty kinds array typically means no kind restriction.
These edge cases could cause non-profile queries to be sent to purplepag.es, defeating the purpose of this filtering.
🔎 Proposed fix to handle edge cases
/// Whether this request is for profile-related data only
var isProfileRelated: Bool {
guard case .typical(let req) = self else { return false }
guard case .subscribe(let sub) = req else { return false }
-
+
+ // Must have at least one filter
+ guard !sub.filters.isEmpty else { return false }
+
// Check if ALL filters contain ONLY profile-related kinds
return sub.filters.allSatisfy { filter in
guard let kinds = filter.kinds else { return false } // No kinds specified = could be anything
+ guard !kinds.isEmpty else { return false } // Empty kinds = could be anything
return kinds.allSatisfy { Self.profileKinds.contains($0) }
}
}🤖 Prompt for AI Agents
In @damus/Core/Nostr/NostrRequest.swift around lines 46-55, The isProfileRelated
logic incorrectly treats empty filters or empty kinds as profile-only due to
vacuous truth; update the check in NostrRequest.isProfileRelated (the guard for
.typical -> .subscribe and the return using sub.filters.allSatisfy) to first
return false if sub.filters.isEmpty, and inside the allSatisfy closure require
that filter.kinds is non-nil AND not empty (e.g., guard let kinds =
filter.kinds, !kinds.isEmpty else { return false }) before checking
kinds.allSatisfy { Self.profileKinds.contains($0) } so empty filters or empty
kinds do not get classified as profile-related.


User Benefit
Improve chances that profiles are found (i.e. reduce chances profiles are not found).
Summary
Add
wss://purplepag.esas a system relay that's always included for all users but only used for profile metadata queries (kinds 0, 3, 10002). This improves profile metadata fetching without polluting the relay with non-profile requests.Changes:
profilesOnlyvariant toRelayVariantenumisProfileRelatedcheck toNostrRequestTypeto detect profile queriessend_rawto only receive profile requestsUserRelayListManagerStandard PR Checklist
Closes:orFixes:tags in the commit messages wherever applicable, or made sure those are not needed. See Submitting patchesTest report
Device: iPhone 16 Pro Simulator
iOS: 18.x
Damus: feature/purplepages-profile-relay (be1290c)
Setup: Fresh install on simulator
Steps:
Results:
NostrKind.metadata,NostrKind.contacts, andNostrKind.relay_listsubscriptions to purplepag.esOther notes
Closes #1331
Debug logging is included (prints to Xcode console only) to help verify filtering is working:
🟣 PURPLEPAGES: sending subscription with kinds: [...]🟣 PURPLEPAGES: received kind X eventSummary by CodeRabbit
Release Notes
✏️ Tip: You can customize this high-level summary in your review settings.