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
|
||||
class NostrEditProfileComponent < ViewComponent::Base
|
||||
def initialize(user:, profile_event:)
|
||||
if profile_event.present?
|
||||
@user = user
|
||||
@profile_event = profile_event
|
||||
@profile = JSON.parse(profile_event["content"])
|
||||
@user = user
|
||||
@profile_event = profile_event
|
||||
@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"]
|
||||
|
||||
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
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
module Settings
|
||||
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 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"
|
||||
@icon_name = "check-circle"
|
||||
@icon_color = "emerald-500"
|
||||
|
||||
@@ -9,7 +9,7 @@ class SettingsController < ApplicationController
|
||||
]
|
||||
before_action :set_user, only: [
|
||||
:show, :update, :update_email, :reset_email_password,
|
||||
:fetch_nostr_user_metadata
|
||||
:fetch_nostr_metadata
|
||||
]
|
||||
|
||||
def index
|
||||
@@ -148,26 +148,45 @@ class SettingsController < ApplicationController
|
||||
}
|
||||
end
|
||||
|
||||
def fetch_nostr_user_metadata
|
||||
if @user.nostr_pubkey.present?
|
||||
outbox_relay_urls = nil
|
||||
def fetch_nostr_metadata
|
||||
return render plain: "no pubkey", status: :bad_request if @user.nostr_pubkey.blank?
|
||||
|
||||
# if @nip65_event = NostrManager::DiscoverUserRelays.call(pubkey: @user.nostr_pubkey)
|
||||
# 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
|
||||
cache_key = "nostr_metadata:#{@user.id}"
|
||||
|
||||
case params[:step]
|
||||
when "relays"
|
||||
nip65_event = NostrManager::DiscoverUserRelays.call(pubkey: @user.nostr_pubkey)
|
||||
write_relays = nip65_event ? extract_write_relays(nip65_event) : []
|
||||
|
||||
if nip65_event && write_relays.any? && stale?(nip65_event)
|
||||
auth_event = NostrManager::DiscoverUserRelays.call(
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# @profile = NostrManager::DiscoverUserProfile.call(
|
||||
# pubkey: @user.nostr_pubkey,
|
||||
# relays: outbox_relay_urls
|
||||
# )
|
||||
@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"=>[]}
|
||||
else
|
||||
@relays, @profile = [nil, nil]
|
||||
render plain: "invalid step", status: :bad_request
|
||||
end
|
||||
|
||||
render partial: 'nostr_user_metadata'
|
||||
end
|
||||
|
||||
private
|
||||
@@ -176,6 +195,16 @@ class SettingsController < ApplicationController
|
||||
@current_section = :settings
|
||||
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
|
||||
@settings_section = params[:section]
|
||||
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
|
||||
class DiscoverUserRelays < NostrManagerService
|
||||
def initialize(pubkey:)
|
||||
def initialize(pubkey:, relays: nil)
|
||||
@pubkey = pubkey
|
||||
@relays = Setting.nostr_discovery_relays
|
||||
@relays = relays.present? ? relays : Setting.nostr_discovery_relays
|
||||
end
|
||||
|
||||
def call
|
||||
|
||||
@@ -88,8 +88,15 @@
|
||||
</section>
|
||||
|
||||
<% if current_user.nostr_pubkey.present? %>
|
||||
<%= turbo_frame_tag "nostr_user_metadata", src: nostr_user_metadata_settings_path do %>
|
||||
<p>Loading...</p>
|
||||
<% end %>
|
||||
<div data-controller="nostr-metadata"
|
||||
data-nostr-metadata-url-value="<%= nostr_metadata_settings_path %>">
|
||||
<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 %>
|
||||
</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 'set_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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user