WIP: Improve Nostr user metadata fetching
Use a multi-step process, caching results in between, while informing the user about the current steps/progress
This commit is contained in:
@@ -3,25 +3,14 @@
|
|||||||
module Settings
|
module Settings
|
||||||
class NostrEditProfileComponent < ViewComponent::Base
|
class NostrEditProfileComponent < ViewComponent::Base
|
||||||
def initialize(user:, profile_event:)
|
def initialize(user:, profile_event:)
|
||||||
if profile_event.present?
|
|
||||||
@user = user
|
@user = user
|
||||||
@profile_event = profile_event
|
@profile_event = profile_event
|
||||||
@profile = JSON.parse(profile_event["content"])
|
@profile = {}
|
||||||
|
@display_name = nil
|
||||||
|
|
||||||
|
if profile_event.present? && profile_event["content"].present?
|
||||||
|
@profile = JSON.parse(profile_event["content"]) rescue {}
|
||||||
@display_name = @profile["display_name"] || @profile["displayName"]
|
@display_name = @profile["display_name"] || @profile["displayName"]
|
||||||
|
|
||||||
if @profile["nip05"].present? && @profile["nip05"] == @user.address
|
|
||||||
# "Your profile's Nostr address is set to <strong>#{ user_address }</strong>"
|
|
||||||
else
|
|
||||||
# "Your profile's Nostr address is not set to <strong>#{ user_address }</strong> yet"
|
|
||||||
end
|
|
||||||
|
|
||||||
if @profile["lud16"].present? && @profile["lud16"] == @user.address
|
|
||||||
# "Your profile's Lightning address is set to <strong>#{ user_address }</strong>"
|
|
||||||
else
|
|
||||||
# "Your profile's Lightning address is not set to <strong>#{ user_address }</strong> yet"
|
|
||||||
end
|
|
||||||
else
|
|
||||||
# "We could not find a profile for your public key"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
module Settings
|
module Settings
|
||||||
class NostrRelayStatusComponent < ViewComponent::Base
|
class NostrRelayStatusComponent < ViewComponent::Base
|
||||||
def initialize(nip65_event:)
|
def initialize(nip65_event:, relay_url: Setting.nostr_relay_url)
|
||||||
|
@relay_url = relay_url
|
||||||
|
|
||||||
if nip65_event.present?
|
if nip65_event.present?
|
||||||
if relay_urls(nip65_event).any? { |r| r.include?("wss://nostr.kosmos.org") }
|
if relay_urls(nip65_event).include?(@relay_url)
|
||||||
@text = "You have a relay list, and the Kosmos relay is part of it"
|
@text = "You have a relay list, and the Kosmos relay is part of it"
|
||||||
@icon_name = "check-circle"
|
@icon_name = "check-circle"
|
||||||
@icon_color = "emerald-500"
|
@icon_color = "emerald-500"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class SettingsController < ApplicationController
|
|||||||
]
|
]
|
||||||
before_action :set_user, only: [
|
before_action :set_user, only: [
|
||||||
:show, :update, :update_email, :reset_email_password,
|
:show, :update, :update_email, :reset_email_password,
|
||||||
:fetch_nostr_user_metadata
|
:fetch_nostr_metadata
|
||||||
]
|
]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@@ -148,26 +148,45 @@ class SettingsController < ApplicationController
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_nostr_user_metadata
|
def fetch_nostr_metadata
|
||||||
if @user.nostr_pubkey.present?
|
return render plain: "no pubkey", status: :bad_request if @user.nostr_pubkey.blank?
|
||||||
outbox_relay_urls = nil
|
|
||||||
|
|
||||||
# if @nip65_event = NostrManager::DiscoverUserRelays.call(pubkey: @user.nostr_pubkey)
|
cache_key = "nostr_metadata:#{@user.id}"
|
||||||
# relay_tags = @nip65_event["tags"].select{ |t| t[0] == "r" }
|
|
||||||
# outbox_relay_urls = relay_tags&.select{ |t| t[2] != "read" }&.map{ |t| t[1] }
|
|
||||||
# end
|
|
||||||
|
|
||||||
# @profile = NostrManager::DiscoverUserProfile.call(
|
case params[:step]
|
||||||
# pubkey: @user.nostr_pubkey,
|
when "relays"
|
||||||
# relays: outbox_relay_urls
|
nip65_event = NostrManager::DiscoverUserRelays.call(pubkey: @user.nostr_pubkey)
|
||||||
# )
|
write_relays = nip65_event ? extract_write_relays(nip65_event) : []
|
||||||
@profile = {"content"=>"{\"name\":\"jimmy\",\"picture\":\"https://storage.kosmos.org/jimmy/public/shares/241028-1117-tony.jpg\",\"banner\":\"https://storage.kosmos.org/raucao/public/shares/240604-1517-1500x500.jpg\",\"nip05\":\"jimmy@kosmos.org\",\"lud16\":\"jimmy@kosmos.org\",\"pubkey\":\"07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3\",\"display_name\":\"Jimmy\",\"displayName\":\"Jimmy\",\"about\":\"I don't exist. Follow at your own peril.\"}", "created_at"=>1730114246, "id"=>"6b15b1308a61ee837bd3b50319978314650e435891c259f4ea499f819f35a4f6", "kind"=>0, "pubkey"=>"07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3", "sig"=>"4f681f4b95646bbf88a6eae9ca92c0f2ce5effecfa017556a23490f91a99243aedf81d956ee2466ed64fecb9a03b6b89cd80ff116df0178830977e203867d7ae", "tags"=>[]}
|
|
||||||
# @profile = {"content"=>"{\"name\":\"jimmy\",\"nip05\":\"jimmy@kosmos.org\",\"lud16\":\"jimmy@kosmos.org\",\"pubkey\":\"07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3\",\"display_name\":\"Jimmy\",\"displayName\":\"Jimmy\",\"about\":\"I don't exist. Follow at your own peril.\"}", "created_at"=>1730114246, "id"=>"6b15b1308a61ee837bd3b50319978314650e435891c259f4ea499f819f35a4f6", "kind"=>0, "pubkey"=>"07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3", "sig"=>"4f681f4b95646bbf88a6eae9ca92c0f2ce5effecfa017556a23490f91a99243aedf81d956ee2466ed64fecb9a03b6b89cd80ff116df0178830977e203867d7ae", "tags"=>[]}
|
if nip65_event && write_relays.any? && stale?(nip65_event)
|
||||||
else
|
auth_event = NostrManager::DiscoverUserRelays.call(
|
||||||
@relays, @profile = [nil, nil]
|
pubkey: @user.nostr_pubkey, relays: write_relays
|
||||||
|
)
|
||||||
|
nip65_event = auth_event if auth_event
|
||||||
|
write_relays = extract_write_relays(nip65_event) if auth_event
|
||||||
end
|
end
|
||||||
|
|
||||||
render partial: 'nostr_user_metadata'
|
Rails.cache.write("#{cache_key}:nip65", nip65_event, expires_in: 5.minutes)
|
||||||
|
Rails.cache.write("#{cache_key}:write_relays", write_relays, expires_in: 5.minutes)
|
||||||
|
render json: { next: "profile" }
|
||||||
|
|
||||||
|
when "profile"
|
||||||
|
write_relays = Rails.cache.read("#{cache_key}:write_relays") || []
|
||||||
|
relays = write_relays.any? ? write_relays : nil
|
||||||
|
profile = NostrManager::DiscoverUserProfile.call(
|
||||||
|
pubkey: @user.nostr_pubkey, relays: relays
|
||||||
|
)
|
||||||
|
Rails.cache.write("#{cache_key}:profile", profile, expires_in: 5.minutes)
|
||||||
|
render json: { next: "render" }
|
||||||
|
|
||||||
|
when "render", nil
|
||||||
|
@nip65_event = Rails.cache.read("#{cache_key}:nip65")
|
||||||
|
@profile = Rails.cache.read("#{cache_key}:profile")
|
||||||
|
render partial: "nostr_metadata", formats: :html
|
||||||
|
|
||||||
|
else
|
||||||
|
render plain: "invalid step", status: :bad_request
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -176,6 +195,16 @@ class SettingsController < ApplicationController
|
|||||||
@current_section = :settings
|
@current_section = :settings
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def extract_write_relays(nip65_event)
|
||||||
|
nip65_event["tags"]
|
||||||
|
.select { |t| t[0] == "r" && t[2] != "read" }
|
||||||
|
.map { |t| t[1] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def stale?(event, threshold = 30.minutes)
|
||||||
|
Time.at(event["created_at"]) < threshold.ago
|
||||||
|
end
|
||||||
|
|
||||||
def set_settings_section
|
def set_settings_section
|
||||||
@settings_section = params[:section]
|
@settings_section = params[:section]
|
||||||
allowed_sections = [
|
allowed_sections = [
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [ "status", "loading", "content" ]
|
||||||
|
static values = { url: String }
|
||||||
|
|
||||||
|
connect () {
|
||||||
|
this.runStep("relays")
|
||||||
|
}
|
||||||
|
|
||||||
|
async runStep (step) {
|
||||||
|
const messages = {
|
||||||
|
relays: "Looking up your relay list…",
|
||||||
|
profile: "Fetching your profile…",
|
||||||
|
blossom: "Fetching your media server list…",
|
||||||
|
render: null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages[step]) {
|
||||||
|
this.statusTarget.textContent = messages[step]
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers = (step === "render")
|
||||||
|
? { "Accept": "text/html" }
|
||||||
|
: { "Accept": "application/json" }
|
||||||
|
|
||||||
|
const res = await fetch(`${this.urlValue}?step=${step}`, { headers })
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === "render") {
|
||||||
|
const html = await res.text()
|
||||||
|
this.loadingTarget.classList.add("hidden")
|
||||||
|
this.contentTarget.classList.remove("hidden")
|
||||||
|
this.contentTarget.innerHTML = html
|
||||||
|
} else {
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.next) {
|
||||||
|
this.runStep(data.next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Nostr metadata fetch failed:", error.message)
|
||||||
|
this.statusTarget.textContent = "Something went wrong. Please reload."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
module NostrManager
|
module NostrManager
|
||||||
class DiscoverUserRelays < NostrManagerService
|
class DiscoverUserRelays < NostrManagerService
|
||||||
def initialize(pubkey:)
|
def initialize(pubkey:, relays: nil)
|
||||||
@pubkey = pubkey
|
@pubkey = pubkey
|
||||||
@relays = Setting.nostr_discovery_relays
|
@relays = relays.present? ? relays : Setting.nostr_discovery_relays
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
|||||||
@@ -88,8 +88,15 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<% if current_user.nostr_pubkey.present? %>
|
<% if current_user.nostr_pubkey.present? %>
|
||||||
<%= turbo_frame_tag "nostr_user_metadata", src: nostr_user_metadata_settings_path do %>
|
<div data-controller="nostr-metadata"
|
||||||
<p>Loading...</p>
|
data-nostr-metadata-url-value="<%= nostr_metadata_settings_path %>">
|
||||||
<% end %>
|
<div data-nostr-metadata-target="loading"
|
||||||
|
class="flex flex-col items-center justify-center py-16">
|
||||||
|
<div class="mb-4 h-12 w-12"></div>
|
||||||
|
<p data-nostr-metadata-target="status"
|
||||||
|
class="text-gray-500 animate-pulse">Loading…</p>
|
||||||
|
</div>
|
||||||
|
<div data-nostr-metadata-target="content" class="hidden"></div>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<section class="">
|
||||||
|
<h3>Profile</h3>
|
||||||
|
<%= render Settings::NostrProfileStatusComponent.new(
|
||||||
|
profile_event: @profile,
|
||||||
|
user_address: current_user.address
|
||||||
|
) %>
|
||||||
|
<div class="mt-8" data-controller="modal" data-action="keydown.esc->modal#close">
|
||||||
|
<button data-action="click->modal#open" class="btn-md btn-blue w-full sm:w-auto">
|
||||||
|
Edit profile
|
||||||
|
</button>
|
||||||
|
<%= render ModalComponent.new(show_close_button: false) do %>
|
||||||
|
<%= render Settings::NostrEditProfileComponent.new(
|
||||||
|
user: current_user,
|
||||||
|
profile_event: @profile
|
||||||
|
) %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8 sm:mb-12">
|
||||||
|
<h3>Relays</h3>
|
||||||
|
<%= render Settings::NostrRelayStatusComponent.new(
|
||||||
|
nip65_event: @nip65_event
|
||||||
|
) %>
|
||||||
|
</section>
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<%= turbo_frame_tag "nostr_user_metadata" do %>
|
|
||||||
<section>
|
|
||||||
<h3>Relays</h3>
|
|
||||||
<%= render Settings::NostrRelayStatusComponent.new(
|
|
||||||
nip65_event: @nip65_event
|
|
||||||
) %>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h3>Profile</h3>
|
|
||||||
<%= render Settings::NostrProfileStatusComponent.new(
|
|
||||||
profile_event: @profile,
|
|
||||||
user_address: current_user.address
|
|
||||||
) %>
|
|
||||||
<div class="mt-8" data-controller="modal" data-action="keydown.esc->modal#close">
|
|
||||||
<button data-action="click->modal#open" class="btn-md btn-blue w-full sm:w-auto">
|
|
||||||
Edit profile
|
|
||||||
</button>
|
|
||||||
<%= render ModalComponent.new(show_close_button: false) do %>
|
|
||||||
<%= render Settings::NostrEditProfileComponent.new(
|
|
||||||
user: current_user,
|
|
||||||
profile_event: @profile
|
|
||||||
) %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<% end %>
|
|
||||||
+1
-1
@@ -68,7 +68,7 @@ Rails.application.routes.draw do
|
|||||||
post 'reset_email_password'
|
post 'reset_email_password'
|
||||||
post 'set_nostr_pubkey'
|
post 'set_nostr_pubkey'
|
||||||
delete 'nostr_pubkey', to: 'settings#remove_nostr_pubkey'
|
delete 'nostr_pubkey', to: 'settings#remove_nostr_pubkey'
|
||||||
get 'fetch_nostr_user_metadata', as: 'nostr_user_metadata'
|
get 'fetch_nostr_metadata', as: 'nostr_metadata'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user