From 9da91404fce8e2c8dee3ba30923dcb5ce31c0b19 Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Mon, 2 Jun 2025 21:07:08 +0200 Subject: [PATCH 01/33] Remove JS for bookmarklet into Stimulus JS controllers I've successfully refactored the bookmarklet JavaScript functionality from inline scripts to Stimulus Rails controllers: 1. Created bookmarklet_add_controller.js - Handles the main bookmark saving functionality with Nostr signing 2. Created bookmarklet_debug_controller.js - Handles the Nostr extension debugging functionality 3. Created bookmarklet_success_controller.js - Simple controller for the success page close button 4. Updated view files: - add.html.erb - Removed all inline JavaScript, added Stimulus data attributes - debug.html.erb - Removed all inline JavaScript, added Stimulus data attributes - success.html.erb - Updated close button to use Stimulus The refactoring maintains all the original functionality while following Rails best practices by moving JavaScript code into proper Stimulus controllers. The controllers handle: - Nostr extension detection - Form submission with/without Nostr signing - Event preparation and signing - Server communication - Success/error handling - Auto-close functionality --- .../controllers/bookmarklet_add_controller.js | 440 ++++++++++++++++ .../bookmarklet_debug_controller.js | 158 ++++++ .../bookmarklet_success_controller.js | 12 + app/views/bookmarklet/add.html.erb | 468 +----------------- app/views/bookmarklet/debug.html.erb | 185 +------ app/views/bookmarklet/success.html.erb | 4 +- 6 files changed, 632 insertions(+), 635 deletions(-) create mode 100644 app/javascript/controllers/bookmarklet_add_controller.js create mode 100644 app/javascript/controllers/bookmarklet_debug_controller.js create mode 100644 app/javascript/controllers/bookmarklet_success_controller.js diff --git a/app/javascript/controllers/bookmarklet_add_controller.js b/app/javascript/controllers/bookmarklet_add_controller.js new file mode 100644 index 0000000..b6f0fd8 --- /dev/null +++ b/app/javascript/controllers/bookmarklet_add_controller.js @@ -0,0 +1,440 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["status", "form", "title", "url", "description", "submitButton", "formError"] + + connect() { + console.log("Bookmarklet add controller connected") + + // Initialize state + this.formSubmitting = false + + // Check if we're in a popup window + this.isPopup = new URLSearchParams(window.location.search).get('popup') === 'true' + console.log('đŸĒŸ Running in popup mode:', this.isPopup) + + // Initialize Nostr detection + this.initializeNostrDetection() + } + + initializeNostrDetection() { + // Global variables to track Nostr readiness + window.nostrReady = false + window.nostrExtensionFound = false + + let checkCount = 0 + const maxChecks = 10 // Try for 10 seconds + const checkInterval = 1000 // Check every second + + const checkForNostr = () => { + checkCount++ + console.log(`Nostr check attempt ${checkCount}/${maxChecks} in bookmarklet popup`) + + // Debug: Log what's available in window + console.log('window.nostr exists:', !!window.nostr) + + if (window.nostr) { + console.log('window.nostr type:', typeof window.nostr) + console.log('window.nostr object keys:', Object.keys(window.nostr)) + console.log('getPublicKey available:', typeof window.nostr.getPublicKey) + console.log('signEvent available:', typeof window.nostr.signEvent) + + const hasPubkey = typeof window.nostr.getPublicKey === 'function' + const hasSignEvent = typeof window.nostr.signEvent === 'function' + + if (hasPubkey && hasSignEvent) { + // Success! Nostr is ready + window.nostrReady = true + window.nostrExtensionFound = true + + this.statusTarget.textContent = "Nostr extension detected ✓" + this.statusTarget.className = "nostr-status nostr-status-connected" + console.log('✓ Nostr extension fully detected and ready in bookmarklet popup') + + // Try to get pubkey to verify it's really working + window.nostr.getPublicKey() + .then(pubkey => { + this.statusTarget.textContent = `Nostr extension ready: ${pubkey.substring(0, 8)}...` + console.log('✓ Nostr pubkey retrieved successfully in popup:', pubkey) + }) + .catch(err => { + console.log('Note: Error getting pubkey (user may need to approve):', err.message) + // Don't change status - extension is still ready, user just needs to approve + }) + + return // Stop checking + } else { + let missingMethods = [] + if (!hasPubkey) missingMethods.push("getPublicKey") + if (!hasSignEvent) missingMethods.push("signEvent") + + this.statusTarget.textContent = `Nostr extension missing: ${missingMethods.join(", ")}` + this.statusTarget.className = "nostr-status nostr-status-error" + console.log('✗ Nostr extension found but missing methods:', missingMethods) + return // Stop checking + } + } else if (checkCount >= maxChecks) { + // We've tried enough times, give up + window.nostrReady = false + window.nostrExtensionFound = false + + this.statusTarget.textContent = "No Nostr extension detected. Bookmark will be saved without signing." + this.statusTarget.className = "nostr-status nostr-status-disconnected" + console.log('✗ No Nostr extension found after', maxChecks, 'seconds in popup') + return + } else { + // Still checking + this.statusTarget.textContent = `Checking for Nostr extension... (${checkCount}/${maxChecks})` + console.log(`âŗ Nostr not found yet in popup, continuing to check... (${checkCount}/${maxChecks})`) + setTimeout(checkForNostr, checkInterval) + } + } + + // Start checking immediately + checkForNostr() + } + + async handleSubmit(event) { + console.log('đŸŽ¯ EMBEDDED JS: Form submit intercepted') + + // Prevent default form submission + event.preventDefault() + + // Prevent multiple submissions + if (this.formSubmitting) { + console.log('Form already submitting, ignoring duplicate') + return + } + + this.formSubmitting = true + + // Disable submit button + if (this.hasSubmitButtonTarget) { + this.submitButtonTarget.disabled = true + this.submitButtonTarget.textContent = 'Saving...' + } + + try { + // Get form data + const title = this.titleTarget.value + const url = this.urlTarget.value + const description = this.hasDescriptionTarget ? this.descriptionTarget.value : '' + + console.log('📝 Form data:', { title, url, description }) + + // Update status + if (this.hasStatusTarget) { + this.statusTarget.textContent = "Processing bookmark..." + this.statusTarget.className = "nostr-status" + } + + // Check Nostr readiness + console.log('🔍 Checking Nostr readiness...') + console.log(' window.nostrReady:', window.nostrReady) + console.log(' window.nostr exists:', !!window.nostr) + + // Try Nostr signing if available + let nostrSuccess = false + + if (window.nostrReady === true && window.nostr && + typeof window.nostr.getPublicKey === 'function' && + typeof window.nostr.signEvent === 'function') { + try { + console.log('🔐 Attempting Nostr signing...') + if (this.hasStatusTarget) { + this.statusTarget.textContent = "Attempting Nostr signing..." + } + + nostrSuccess = await this.attemptNostrSigning(title, url, description) + console.log('đŸŽ¯ CRITICAL: Nostr signing result:', nostrSuccess) + } catch (error) { + console.error('❌ Error in Nostr signing:', error) + if (this.hasStatusTarget) { + this.statusTarget.textContent = `Nostr error: ${error.message}. Submitting without signing.` + this.statusTarget.className = "nostr-status nostr-status-error" + } + } + } else { + console.log('❌ Nostr not ready, submitting directly') + if (this.hasStatusTarget) { + this.statusTarget.textContent = "No Nostr extension ready. Submitting directly." + this.statusTarget.className = "nostr-status nostr-status-disconnected" + } + } + + // If Nostr signing didn't work, submit directly + if (!nostrSuccess) { + console.log('📤 Falling back to direct submission') + await this.submitFormDirectly(title, url, description) + } + } catch (error) { + console.error('❌ Unhandled error:', error) + + // Re-enable form + this.formSubmitting = false + if (this.hasSubmitButtonTarget) { + this.submitButtonTarget.disabled = false + this.submitButtonTarget.textContent = 'Save Bookmark' + } + + alert('An error occurred: ' + error.message) + } + } + + async attemptNostrSigning(title, url, description) { + console.log('🔐 Starting Nostr signing attempt') + + try { + // Step 1: Get public key + console.log('🔑 Step 1: Getting public key...') + if (this.hasStatusTarget) { + this.statusTarget.textContent = "Requesting Nostr public key..." + } + + const pubkey = await window.nostr.getPublicKey() + console.log('✅ Got pubkey:', pubkey) + + // Step 2: Prepare event + console.log('📝 Step 2: Preparing event...') + if (this.hasStatusTarget) { + this.statusTarget.textContent = "Preparing Nostr event..." + } + + const event = this.prepareNostrEvent(title, url, description, pubkey) + console.log('✅ Prepared event:', event) + + // Step 3: Sign event + console.log('âœī¸ Step 3: Requesting signature...') + if (this.hasStatusTarget) { + this.statusTarget.textContent = "Requesting Nostr signature..." + } + + const signedEvent = await window.nostr.signEvent(event) + console.log('✅ Got signed event:', signedEvent) + + // Step 4: Validate + if (!signedEvent || !signedEvent.sig || !signedEvent.id) { + console.error('❌ Invalid signed event') + return false + } + + // Step 5: Submit to server + console.log('🚀 Step 5: Submitting to server...') + if (this.hasStatusTarget) { + this.statusTarget.textContent = "Successfully signed! Submitting..." + this.statusTarget.className = "nostr-status nostr-status-connected" + } + + const result = await this.submitToServer(signedEvent, title, url, description, true) + return result + } catch (error) { + console.error('❌ Error in Nostr signing:', error) + throw error + } + } + + prepareNostrEvent(title, url, description, pubkey) { + // Extract d-tag from URL + const urlWithoutScheme = this.extractUrlDTag(url) + console.log('đŸˇī¸ Extracted d-tag:', urlWithoutScheme) + + const now = Math.floor(Date.now() / 1000) + + const tags = [ + ["d", urlWithoutScheme], + ["title", title], + ["published_at", now.toString()] + ] + + // Add hashtags + const hashtags = this.extractHashtags(description) + hashtags.forEach(tag => { + tags.push(["t", tag]) + }) + + return { + kind: 39701, + pubkey: pubkey, + created_at: now, + tags: tags, + content: description + } + } + + extractUrlDTag(url) { + try { + let fullUrl = url + if (!url.match(/^https?:\/\//i)) { + fullUrl = "https://" + url + } + + const urlObj = new URL(fullUrl) + return urlObj.hostname + urlObj.pathname + } catch (e) { + console.error("Error parsing URL:", e) + return url.replace(/^https?:\/\//i, '') + } + } + + extractHashtags(content) { + if (!content) return [] + + const hashtags = [] + const matches = content.match(/(?:\s|^)#([\w\d]+)/g) || [] + + matches.forEach(match => { + const tag = match.trim().substring(1) + if (tag && tag.length > 0) { + hashtags.push(tag) + } + }) + + return hashtags + } + + async submitToServer(signedEvent, title, url, description, nostrSigned = false) { + console.log('📤 Submitting to server with signed event') + console.log('📤 Signed event:', signedEvent) + + const csrfToken = document.querySelector('meta[name="csrf-token"]').content + + const payload = { + signed_event: signedEvent, + bookmark: { + title: title, + url: url, + description: description + }, + popup: this.isPopup ? true : undefined + } + + console.log('📤 Full payload:', JSON.stringify(payload, null, 2)) + + try { + const actionUrl = this.formTarget.getAttribute('action') + const response = await fetch(actionUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify(payload) + }) + + console.log('đŸ“Ĩ Server response status:', response.status) + + if (!response.ok) { + const errorData = await response.json() + console.error('❌ Server error:', errorData) + throw new Error(errorData.errors ? errorData.errors.join(', ') : 'Server error') + } + + const data = await response.json() + console.log('✅ Server success:', data) + + // Show success message and auto-close + this.showSuccessAndClose('Your bookmark has been successfully saved and signed with Nostr.', true) + + return true + } catch (error) { + console.error('❌ Error submitting to server:', error) + alert('Failed to save bookmark: ' + error.message) + return false + } + } + + async submitFormDirectly(title, url, description) { + console.log('📤 Submitting form directly without Nostr') + + const formData = new FormData(this.formTarget) + formData.append('direct_submission', 'true') + + try { + const actionUrl = this.formTarget.getAttribute('action') + const response = await fetch(actionUrl, { + method: 'POST', + body: formData, + headers: { + 'Accept': 'text/html,application/xhtml+xml' + }, + redirect: 'follow' + }) + + console.log('📤 Direct submission response:', response) + + if (response.ok) { + // Show success message and auto-close + this.showSuccessAndClose('Your bookmark has been successfully saved.', false) + } else { + throw new Error('Server returned an error') + } + } catch (error) { + console.error('❌ Error in direct submission:', error) + alert('Failed to submit form: ' + error.message) + + // Re-enable form + this.formSubmitting = false + if (this.hasSubmitButtonTarget) { + this.submitButtonTarget.disabled = false + this.submitButtonTarget.textContent = 'Save Bookmark' + } + } + } + + showSuccessAndClose(message, nostrSigned) { + console.log('🎉 Showing success message and preparing to close') + + const container = document.getElementById('form-container') + + // Show success message with Nostr status + let nostrStatusHtml = '' + if (nostrSigned) { + nostrStatusHtml = '
✓ Bookmark signed with Nostr and will be published to relays
' + } else { + nostrStatusHtml = '
â„šī¸ Bookmark saved without Nostr signing
' + } + + container.innerHTML = ` +
+

Bookmark Saved!

+

${message || 'Your bookmark has been successfully saved to Pinstr.'}

+ ${nostrStatusHtml} +
+

This window will close automatically in 3 seconds...

+
+ +
+
+
+ ` + + // Countdown and auto-close + let countdown = 3 + const countdownElement = document.getElementById('countdown') + + const countdownInterval = setInterval(() => { + countdown-- + if (countdownElement) { + countdownElement.textContent = countdown + } + + if (countdown <= 0) { + clearInterval(countdownInterval) + console.log('⏰ Auto-closing popup window') + + // Only close if we're actually in a popup + if (this.isPopup) { + window.close() + } else { + // If not in popup, redirect to bookmarks page + window.location.href = '/bookmarks' + } + } + }, 1000) + } + + cancelBookmark() { + window.close() + } +} \ No newline at end of file diff --git a/app/javascript/controllers/bookmarklet_debug_controller.js b/app/javascript/controllers/bookmarklet_debug_controller.js new file mode 100644 index 0000000..f3a6b3b --- /dev/null +++ b/app/javascript/controllers/bookmarklet_debug_controller.js @@ -0,0 +1,158 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["statusMessage", "detectionLog", "testResults", "testOutput", "userAgent", "currentUrl", "extensionsList"] + + connect() { + console.log("Bookmarklet debug controller connected") + + // Fill in browser info + this.userAgentTarget.textContent = navigator.userAgent + this.currentUrlTarget.textContent = window.location.href + + // Check for extensions + this.checkExtensions() + + // Start continuous Nostr checking + this.checkCount = 0 + this.continuousCheck() + } + + log(message) { + console.log(message) + const logEntry = document.createElement('div') + logEntry.className = 'log-entry' + logEntry.textContent = new Date().toLocaleTimeString() + ': ' + message + this.detectionLogTarget.appendChild(logEntry) + this.detectionLogTarget.scrollTop = this.detectionLogTarget.scrollHeight + } + + checkExtensions() { + const extensions = [] + + // Check for common extension objects + if (window.nostr) extensions.push('Nostr (window.nostr)') + if (window.webln) extensions.push('WebLN (window.webln)') + if (window.ethereum) extensions.push('Ethereum (window.ethereum)') + if (window.bitcoin) extensions.push('Bitcoin (window.bitcoin)') + + this.extensionsListTarget.textContent = extensions.length > 0 ? extensions.join(', ') : 'None detected' + } + + checkNostr() { + this.log('Starting Nostr check...') + + if (!window.nostr) { + this.statusMessageTarget.textContent = 'No window.nostr object found' + this.statusMessageTarget.className = 'status-error' + this.log('window.nostr is not defined') + return false + } + + this.log('window.nostr found: ' + typeof window.nostr) + this.log('window.nostr object: ' + JSON.stringify(Object.keys(window.nostr))) + + const hasPubkey = typeof window.nostr.getPublicKey === 'function' + const hasSignEvent = typeof window.nostr.signEvent === 'function' + + this.log('getPublicKey method: ' + typeof window.nostr.getPublicKey) + this.log('signEvent method: ' + typeof window.nostr.signEvent) + + if (hasPubkey && hasSignEvent) { + this.statusMessageTarget.textContent = 'Nostr extension detected and ready!' + this.statusMessageTarget.className = 'status-success' + this.log('Nostr extension fully functional') + return true + } else { + const missing = [] + if (!hasPubkey) missing.push('getPublicKey') + if (!hasSignEvent) missing.push('signEvent') + + this.statusMessageTarget.textContent = 'Nostr extension missing methods: ' + missing.join(', ') + this.statusMessageTarget.className = 'status-error' + this.log('Nostr extension missing methods: ' + missing.join(', ')) + return false + } + } + + continuousCheck() { + this.checkCount++ + this.log(`Continuous check #${this.checkCount}`) + + if (this.checkNostr()) { + this.log('Nostr detected, stopping continuous checks') + return + } + + if (this.checkCount < 20) { + setTimeout(() => this.continuousCheck(), 1000) + } else { + this.log('Gave up after 20 attempts') + } + } + + manualCheck() { + this.log('Manual check requested') + this.checkExtensions() + this.checkNostr() + } + + async testPubkey() { + this.testResultsTarget.style.display = 'block' + this.testOutputTarget.textContent = 'Testing getPublicKey...' + + if (!window.nostr || typeof window.nostr.getPublicKey !== 'function') { + this.testOutputTarget.textContent = 'Error: getPublicKey not available' + this.testOutputTarget.className = 'code-block error' + return + } + + try { + const pubkey = await window.nostr.getPublicKey() + this.testOutputTarget.textContent = 'Success: ' + pubkey + this.testOutputTarget.className = 'code-block success' + this.log('getPublicKey test successful: ' + pubkey) + } catch (error) { + this.testOutputTarget.textContent = 'Error: ' + error.message + this.testOutputTarget.className = 'code-block error' + this.log('getPublicKey test failed: ' + error.message) + } + } + + async testSigning() { + this.testResultsTarget.style.display = 'block' + this.testOutputTarget.textContent = 'Testing signEvent...' + + if (!window.nostr || typeof window.nostr.signEvent !== 'function' || typeof window.nostr.getPublicKey !== 'function') { + this.testOutputTarget.textContent = 'Error: Required Nostr methods not available' + this.testOutputTarget.className = 'code-block error' + return + } + + try { + // Get pubkey first + const pubkey = await window.nostr.getPublicKey() + + // Create a test event + const testEvent = { + kind: 1, + pubkey: pubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: 'This is a test event from Pinstr debugging page.' + } + + // Sign it + const signedEvent = await window.nostr.signEvent(testEvent) + + // Display result + this.testOutputTarget.textContent = JSON.stringify(signedEvent, null, 2) + this.testOutputTarget.className = 'code-block success' + this.log('signEvent test successful') + } catch (error) { + this.testOutputTarget.textContent = 'Error: ' + error.message + this.testOutputTarget.className = 'code-block error' + this.log('signEvent test failed: ' + error.message) + } + } +} \ No newline at end of file diff --git a/app/javascript/controllers/bookmarklet_success_controller.js b/app/javascript/controllers/bookmarklet_success_controller.js new file mode 100644 index 0000000..25ddc25 --- /dev/null +++ b/app/javascript/controllers/bookmarklet_success_controller.js @@ -0,0 +1,12 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + closeWindow() { + window.close() + } + + openInNewTab(event) { + // This will be handled by the target="_blank" on the link itself + // But we can add any additional logic here if needed + } +} \ No newline at end of file diff --git a/app/views/bookmarklet/add.html.erb b/app/views/bookmarklet/add.html.erb index cc98fbf..d983e94 100644 --- a/app/views/bookmarklet/add.html.erb +++ b/app/views/bookmarklet/add.html.erb @@ -1,13 +1,13 @@ -
+

Save Bookmark to Pinstr

-
+
Checking for Nostr extension...
- + - <%= form_with(model: @bookmark, url: create_from_bookmarklet_path, id: "bookmark-form") do |form| %> + <%= form_with(model: @bookmark, url: create_from_bookmarklet_path, id: "bookmark-form", data: { "bookmarklet-add-target": "form", action: "submit->bookmarklet-add#handleSubmit" }) do |form| %> <% if @bookmark.errors.any? %>

<%= pluralize(@bookmark.errors.count, "error") %> prohibited this bookmark from being saved:

@@ -21,17 +21,17 @@
<%= form.label :title %> - <%= form.text_field :title, required: true, id: "bookmark-title" %> + <%= form.text_field :title, required: true, id: "bookmark-title", data: { "bookmarklet-add-target": "title" } %>
<%= form.label :url %> - <%= form.text_field :url, required: true, id: "bookmark-url" %> + <%= form.text_field :url, required: true, id: "bookmark-url", data: { "bookmarklet-add-target": "url" } %>
<%= form.label :description %> - <%= form.text_area :description, id: "bookmark-description" %> + <%= form.text_area :description, id: "bookmark-description", data: { "bookmarklet-add-target": "description" } %>
<% if @in_popup %> @@ -39,8 +39,8 @@ <% end %>
- - <%= form.submit "Save Bookmark", class: "btn btn-primary", id: "submit-button" %> + + <%= form.submit "Save Bookmark", class: "btn btn-primary", id: "submit-button", data: { "bookmarklet-add-target": "submitButton" } %>
<% end %> @@ -52,456 +52,6 @@
- - - diff --git a/app/views/bookmarklet/debug.html.erb b/app/views/bookmarklet/debug.html.erb index 76af2f0..801214f 100644 --- a/app/views/bookmarklet/debug.html.erb +++ b/app/views/bookmarklet/debug.html.erb @@ -1,47 +1,51 @@ -
-

Nostr Extension Debug

+
+

Nostr Extension Debug

-
-
Real-time Nostr Detection
-
-
-

Checking...

-
+
+
Real-time Nostr Detection
+
+
+

Checking...

+
- - - +
+ + + +
-
-
Browser Information
-
- - - - - - - - - - - - - - - -
User Agent
URL
Extensions Detected
+
+
Browser Information
+
+
+ + + + + + + + + + + + + + + +
User Agent
URL
Extensions Detected
+
-
-
Test Results
-
+
+
Test Results
+
diff --git a/app/views/bookmarklet/instructions.html.erb b/app/views/bookmarklet/instructions.html.erb index f837d19..3dcaf20 100644 --- a/app/views/bookmarklet/instructions.html.erb +++ b/app/views/bookmarklet/instructions.html.erb @@ -1,123 +1,86 @@ -
-
-
-

Pinstr Bookmarklet

-

The Pinstr bookmarklet allows you to save webpages to your Pinstr account directly from your browser.

-
+
+
+

Pinstr Bookmarklet

+

The Pinstr bookmarklet allows you to save webpages to your Pinstr account directly from your browser.

-
-
-

Installation

-
- - Save to Pinstr - -

- (Drag this link to your bookmarks bar) -

-
- -
-

How to Install:

-
    -
  1. Make sure your browser's bookmarks bar is visible. (In most browsers, you can press Ctrl+Shift+B or Cmd+Shift+B to toggle the bookmarks bar)
  2. -
  3. Click and drag the "Save to Pinstr" button above to your bookmarks bar.
  4. -
  5. The bookmarklet is now installed and ready to use!
  6. -
-
+
+

Installation

+
+ + Save to Pinstr + +

+ (Drag this link to your bookmarks bar) +

+
+ +
+

How to Install:

+
    +
  1. Make sure your browser's bookmarks bar is visible. (In most browsers, you can press Ctrl+Shift+B or Cmd+Shift+B to toggle the bookmarks bar)
  2. +
  3. Click and drag the "Save to Pinstr" button above to your bookmarks bar.
  4. +
  5. The bookmarklet is now installed and ready to use!
  6. +
-
-
-

How to Use

-
-
    -
  1. Navigate to any webpage you want to save as a bookmark.
  2. -
  3. Click the "Save to Pinstr" bookmarklet in your bookmarks bar.
  4. -
  5. A popup window will appear with the current page's URL and title pre-filled.
  6. -
  7. If you've selected text on the page before clicking the bookmarklet, it will be included in the description field.
  8. -
  9. Review and edit the bookmark details if needed.
  10. -
  11. Click "Save Bookmark" to save it to your Pinstr account.
  12. -
  13. If you have a Nostr extension installed (like nos2x or Alby), you'll be prompted to sign the bookmark event for publishing to Nostr.
  14. -
  15. After saving, you can close the popup or continue browsing.
  16. -
-
+
+

How to Use

+
+
    +
  1. Navigate to any webpage you want to save as a bookmark.
  2. +
  3. Click the "Save to Pinstr" bookmarklet in your bookmarks bar.
  4. +
  5. A popup window will appear with the current page's URL and title pre-filled.
  6. +
  7. If you've selected text on the page before clicking the bookmarklet, it will be included in the description field.
  8. +
  9. Review and edit the bookmark details if needed.
  10. +
  11. Click "Save Bookmark" to save it to your Pinstr account.
  12. +
  13. If you have a Nostr extension installed (like nos2x or Alby), you'll be prompted to sign the bookmark event for publishing to Nostr.
  14. +
  15. After saving, you can close the popup or continue browsing.
  16. +
-
-
-

Nostr Integration

-
-

The Pinstr bookmarklet works best with a Nostr browser extension installed:

-
    -
  • Alby - Browser extension that includes Nostr capabilities
  • -
  • nos2x - Dedicated Nostr signer extension
  • -
  • Nostr Commander - For iOS/macOS users
  • -
- -

These extensions allow the bookmarklet to create a properly signed Nostr event (NIP-B0) for your bookmark, which can then be published to Nostr relays.

- -
- Having trouble with Nostr signing? Check out our debugging page to verify your Nostr extension is working correctly. -
+
+

Nostr Integration

+
+

The Pinstr bookmarklet works best with a Nostr browser extension installed:

+
    +
  • Alby - Browser extension that includes Nostr capabilities
  • +
  • nos2x - Dedicated Nostr signer extension
  • +
  • Nostr Commander - For iOS/macOS users
  • +
+ +

These extensions allow the bookmarklet to create a properly signed Nostr event (NIP-B0) for your bookmark, which can then be published to Nostr relays.

+ +
+ Having trouble with Nostr signing? Check out our debugging page to verify your Nostr extension is working correctly.
-
-
-

Troubleshooting

-
-

Common Issues:

-
    -
  • Popup Blocked: If your browser blocks the popup, you'll need to allow popups from your Pinstr domain.
  • -
  • No Nostr Extension: If you don't have a Nostr extension installed, you can still save bookmarks, but they won't be published to the Nostr network.
  • -
  • Bookmark Not Saving: Make sure you're logged into your Pinstr account in the same browser.
  • -
  • Bookmarklet Not Working: Try reinstalling the bookmarklet by dragging it to your bookmarks bar again.
  • -
  • Nostr Signing Fails: Check if your Nostr extension is properly configured and has permissions for the Pinstr site.
  • -
-
+
+

Troubleshooting

+
+

Common Issues:

+
    +
  • Popup Blocked: If your browser blocks the popup, you'll need to allow popups from your Pinstr domain.
  • +
  • No Nostr Extension: If you don't have a Nostr extension installed, you can still save bookmarks, but they won't be published to the Nostr network.
  • +
  • Bookmark Not Saving: Make sure you're logged into your Pinstr account in the same browser.
  • +
  • Bookmarklet Not Working: Try reinstalling the bookmarklet by dragging it to your bookmarks bar again.
  • +
  • Nostr Signing Fails: Check if your Nostr extension is properly configured and has permissions for the Pinstr site.
  • +
-
-
- - + diff --git a/app/views/bookmarklet/success.html.erb b/app/views/bookmarklet/success.html.erb index 19d27bc..6f39d9b 100644 --- a/app/views/bookmarklet/success.html.erb +++ b/app/views/bookmarklet/success.html.erb @@ -1,58 +1,30 @@ -
-

Bookmark Saved!

-

Your bookmark has been successfully saved to Pinstr.

+
+

Bookmark Saved!

+

Your bookmark has been successfully saved to Pinstr.

<% if params[:nostr_signed] == 'true' %> -
- ✓ Bookmark was signed with Nostr and will be published to relays. +
+ ✓ Bookmark was signed with Nostr and will be published to relays.
<% elsif params[:nostr_signed] == 'false' %> -
- â„šī¸ Bookmark was saved without Nostr signing. +
+ â„šī¸ Bookmark was saved without Nostr signing.
<% end %> -
- - View All Bookmarks +
+ + View All Bookmarks
<% if params[:nostr_signed] == 'false' %> -
-

+

+

Want to use Nostr for your bookmarks? - Set up Nostr integration -

+ Set up Nostr integration +

<% end %>
- + diff --git a/app/views/layouts/bookmarklet.html.erb b/app/views/layouts/bookmarklet.html.erb index 31c89fd..1b760bc 100644 --- a/app/views/layouts/bookmarklet.html.erb +++ b/app/views/layouts/bookmarklet.html.erb @@ -6,76 +6,11 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> - <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> + From 4855954ff6772559bc4daf11c34590f1cf3bb97b Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Wed, 4 Jun 2025 22:41:32 +0200 Subject: [PATCH 12/33] Change success message after bookmarklet success I've successfully updated the bookmarklet to use a proper Rails view instead of building HTML strings in JavaScript. Here's what I changed: Key improvements: 1. Updated JavaScript controller (bookmarklet_add_controller.js:336-339): Both submitToServer and submitFormDirectly methods now redirect to the Rails success view using window.location.href = data.redirect_url 2. Enhanced success view (success.html.erb:15-19): Added conditional countdown display that only shows when in popup mode 3. Updated success controller (bookmarklet_success_controller.js:6-41): Added auto-countdown functionality that automatically closes the popup window after 3 seconds 4. Removed outdated code: Eliminated the showSuccessAndClose method that was building HTML strings Benefits: - Consistent styling using your existing Tailwind CSS classes - Proper Rails view rendering with server-side logic - Maintains auto-close functionality for popup windows - Better separation of concerns (styling in views, logic in controllers) - Easier to maintain and update styling The bookmarklet will now redirect to a properly styled Rails page that shows the success message with appropriate Nostr status indicators and automatically closes the popup window after 3 seconds. --- .../controllers/bookmarklet_add_controller.js | 88 ++++++------------- .../bookmarklet_success_controller.js | 40 +++++++-- app/views/bookmarklet/success.html.erb | 8 +- 3 files changed, 69 insertions(+), 67 deletions(-) diff --git a/app/javascript/controllers/bookmarklet_add_controller.js b/app/javascript/controllers/bookmarklet_add_controller.js index b31ca30..9cbd3e1 100644 --- a/app/javascript/controllers/bookmarklet_add_controller.js +++ b/app/javascript/controllers/bookmarklet_add_controller.js @@ -333,8 +333,10 @@ export default class extends Controller { const data = await response.json() console.log('✅ Server success:', data) - // Show success message and auto-close - this.showSuccessAndClose('Your bookmark has been successfully saved and signed with Nostr.', true) + // Redirect to the Rails success view + if (data.redirect_url) { + window.location.href = data.redirect_url + } return true } catch (error) { @@ -347,27 +349,42 @@ export default class extends Controller { async submitFormDirectly(title, url, description) { console.log('📤 Submitting form directly without Nostr') - const formData = new FormData(this.formTarget) - formData.append('direct_submission', 'true') + const csrfToken = document.querySelector('meta[name="csrf-token"]').content + + const payload = { + bookmark: { + title: title, + url: url, + description: description + }, + popup: this.isPopup ? true : undefined + } try { const actionUrl = this.formTarget.getAttribute('action') const response = await fetch(actionUrl, { method: 'POST', - body: formData, headers: { - 'Accept': 'text/html,application/xhtml+xml' + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-CSRF-Token': csrfToken }, - redirect: 'follow' + body: JSON.stringify(payload) }) console.log('📤 Direct submission response:', response) if (response.ok) { - // Show success message and auto-close - this.showSuccessAndClose('Your bookmark has been successfully saved.', false) + const data = await response.json() + console.log('✅ Direct submission success:', data) + + // Redirect to the Rails success view + if (data.redirect_url) { + window.location.href = data.redirect_url + } } else { - throw new Error('Server returned an error') + const errorData = await response.json() + throw new Error(errorData.errors ? errorData.errors.join(', ') : 'Server error') } } catch (error) { console.error('❌ Error in direct submission:', error) @@ -382,57 +399,6 @@ export default class extends Controller { } } - showSuccessAndClose(message, nostrSigned) { - console.log('🎉 Showing success message and preparing to close') - - const container = document.getElementById('form-container') - - // Show success message with Nostr status - let nostrStatusHtml = '' - if (nostrSigned) { - nostrStatusHtml = '
✓ Bookmark signed with Nostr and will be published to relays
' - } else { - nostrStatusHtml = '
â„šī¸ Bookmark saved without Nostr signing
' - } - - container.innerHTML = ` -
-

Bookmark Saved!

-

${message || 'Your bookmark has been successfully saved to Pinstr.'}

- ${nostrStatusHtml} -
-

This window will close automatically in 3 seconds...

-
- -
-
-
- ` - - // Countdown and auto-close - let countdown = 3 - const countdownElement = document.getElementById('countdown') - - const countdownInterval = setInterval(() => { - countdown-- - if (countdownElement) { - countdownElement.textContent = countdown - } - - if (countdown <= 0) { - clearInterval(countdownInterval) - console.log('⏰ Auto-closing popup window') - - // Only close if we're actually in a popup - if (this.isPopup) { - window.close() - } else { - // If not in popup, redirect to bookmarks page - window.location.href = '/bookmarks' - } - } - }, 1000) - } cancelBookmark() { window.close() diff --git a/app/javascript/controllers/bookmarklet_success_controller.js b/app/javascript/controllers/bookmarklet_success_controller.js index 25ddc25..144590c 100644 --- a/app/javascript/controllers/bookmarklet_success_controller.js +++ b/app/javascript/controllers/bookmarklet_success_controller.js @@ -1,12 +1,42 @@ import { Controller } from "@hotwired/stimulus" export default class extends Controller { - closeWindow() { - window.close() + static targets = ["countdown"] + + connect() { + console.log("Bookmarklet success controller connected") + + // Check if we're in a popup window and start countdown + this.isPopup = new URLSearchParams(window.location.search).get('popup') === 'true' + + if (this.isPopup && this.hasCountdownTarget) { + this.startCountdown() + } } - openInNewTab(event) { - // This will be handled by the target="_blank" on the link itself - // But we can add any additional logic here if needed + startCountdown() { + let countdown = 3 + + const countdownInterval = setInterval(() => { + countdown-- + if (this.hasCountdownTarget) { + this.countdownTarget.textContent = countdown + } + + if (countdown <= 0) { + clearInterval(countdownInterval) + console.log('⏰ Auto-closing popup window') + this.closeWindow() + } + }, 1000) + } + + closeWindow() { + if (this.isPopup) { + window.close() + } else { + // If not in popup, redirect to bookmarks page + window.location.href = '/bookmarks' + } } } \ No newline at end of file diff --git a/app/views/bookmarklet/success.html.erb b/app/views/bookmarklet/success.html.erb index 6f39d9b..6b26af6 100644 --- a/app/views/bookmarklet/success.html.erb +++ b/app/views/bookmarklet/success.html.erb @@ -12,8 +12,14 @@
<% end %> + <% if @in_popup %> +
+

This window will close automatically in 3 seconds...

+
+ <% end %> +
- + View All Bookmarks
From 0086baaffa48e0ed28241494834115f55aa6cc69 Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Wed, 4 Jun 2025 22:53:27 +0200 Subject: [PATCH 13/33] Improve url validations Key Changes: 1. Uses URI::DEFAULT_PARSER.make_regexp - Rails' built-in URL validation regex 2. Leverages ActiveModel validations - More Rails-idiomatic approach 3. Cleaner separation of concerns - Base URL format validation + custom business rules 4. Better error handling - Uses Rails validation error system Benefits: - More reliable - Uses battle-tested Rails validation patterns - Maintainable - Less custom regex logic to maintain - Extensible - Easy to add more validation rules using Rails patterns - Consistent - Follows Rails conventions your team already knows The validation now properly catches malformed URLs like "https://http" while leveraging Rails' robust URL validation foundation instead of reinventing the wheel. --- app/services/url_service.rb | 77 ++++++++++++++++++++++++++----- spec/services/url_service_spec.rb | 15 ++++++ 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/app/services/url_service.rb b/app/services/url_service.rb index 48df926..8bfa91e 100644 --- a/app/services/url_service.rb +++ b/app/services/url_service.rb @@ -1,4 +1,5 @@ require 'uri' +require 'active_model' class UrlService # Normalizes a URL to ensure consistent format @@ -73,22 +74,74 @@ def self.canonicalize(url, keep_params: false) canonical end - # Validates if a string is a valid URL + # Validates if a string is a valid URL using Rails' built-in validation def self.valid?(url) return false if url.blank? - # Try to normalize first - normalized = normalize(url) - - begin - uri = URI.parse(normalized) - # Check if it has a scheme and host - return false unless uri.scheme && uri.host && !uri.host.empty? - return true if uri.scheme =~ /\Ahttps?\z/i - false - rescue URI::InvalidURIError - false + # Create a temporary model to use Rails' URL validation + validator_class = Class.new do + include ActiveModel::Model + include ActiveModel::Attributes + include ActiveModel::Validations + + attribute :url, :string + validates :url, format: { + with: URI::DEFAULT_PARSER.make_regexp(%w[http https]), + message: 'is not a valid URL' + } + + # Additional custom validation + validate :validate_url_structure + + private + + def validate_url_structure + return if url.blank? + + begin + uri = URI.parse(url) + + # Must be HTTP or HTTPS + unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS) + errors.add(:url, 'must use http or https protocol') + return + end + + # Must have a valid host + unless uri.host&.present? + errors.add(:url, 'must have a valid host') + return + end + + host = uri.host.downcase.strip + + # Reject obviously invalid hosts + if %w[http https ftp].include?(host) + errors.add(:url, 'host cannot be a protocol name') + return + end + + if host.match?(/\A\.+\z/) + errors.add(:url, 'host cannot be only dots') + return + end + + # Require domain structure (except localhost) or valid IP + ip_pattern = /\A(\d{1,3}\.){3}\d{1,3}\z/ + unless host == 'localhost' || host.include?('.') || host.match?(ip_pattern) + errors.add(:url, 'host must be a valid domain or IP address') + end + + rescue URI::InvalidURIError + errors.add(:url, 'is malformed') + end + end end + + # Try to normalize first, then validate + normalized = normalize(url) + validator = validator_class.new(url: normalized) + validator.valid? end # Determines if two URLs are equivalent after canonicalization diff --git a/spec/services/url_service_spec.rb b/spec/services/url_service_spec.rb index be48447..c398f8d 100644 --- a/spec/services/url_service_spec.rb +++ b/spec/services/url_service_spec.rb @@ -61,6 +61,21 @@ expect(UrlService.valid?('not a url')).to be false expect(UrlService.valid?('http://')).to be false end + + it 'returns false for malformed protocol URLs' do + expect(UrlService.valid?('https://http')).to be false + expect(UrlService.valid?('http://https')).to be false + expect(UrlService.valid?('ftp://http')).to be false + expect(UrlService.valid?('https://ftp')).to be false + end + + it 'returns false for URLs with invalid host patterns' do + expect(UrlService.valid?('https://')).to be false + expect(UrlService.valid?('https://.')).to be false + expect(UrlService.valid?('https://..')).to be false + expect(UrlService.valid?('https://...')).to be false + expect(UrlService.valid?('https://com')).to be false + end end describe '.equivalent?' do From 4968cdf8b85cc4f7074cafee71337bdb334860ad Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Wed, 18 Jun 2025 19:34:18 +0200 Subject: [PATCH 14/33] Configure Kamal for deploy --- .kamal/secrets | 4 ++- config/database.yml | 3 ++ config/deploy.yml | 70 +++++++++++++++++++-------------------------- config/init.sql | 1 + 4 files changed, 37 insertions(+), 41 deletions(-) create mode 100644 config/init.sql diff --git a/.kamal/secrets b/.kamal/secrets index 9a771a3..202f8ca 100644 --- a/.kamal/secrets +++ b/.kamal/secrets @@ -12,6 +12,8 @@ # Grab the registry password from ENV KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD - +RAILS_MASTER_KEY=$(cat config/master.key) +POSTGRES_PASSWORD=$POSTGRES_PASSWORD +DATABASE_URL=$DATABASE_URL # Improve security by using a password manager. Never check config/master.key into git! RAILS_MASTER_KEY=$(cat config/master.key) diff --git a/config/database.yml b/config/database.yml index 2f0fc61..44c565d 100644 --- a/config/database.yml +++ b/config/database.yml @@ -22,6 +22,7 @@ default: &default development: <<: *default + url: <%= ENV['DEVELOPMENT_DATABASE_URL'] %> database: pinstr_development # The specified database role being used to connect to PostgreSQL. @@ -56,6 +57,7 @@ development: # Do not set this db to the same as development or production. test: <<: *default + url: <%= ENV['TEST_DATABASE_URL'] %> database: pinstr_test # As with config/credentials.yml, you never want to store sensitive information, @@ -81,6 +83,7 @@ test: production: primary: &primary_production <<: *default + url: <%= ENV['DATABASE_URL'] %> database: pinstr_production username: pinstr password: <%= ENV["PINSTR_DATABASE_PASSWORD"] %> diff --git a/config/deploy.yml b/config/deploy.yml index 72214c5..9401e69 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -1,45 +1,31 @@ -# Name of your application. Used to uniquely configure containers. service: pinstr -# Name of the container image. -image: your-user/pinstr +image: neudabei/pinstr -# Deploy to these servers. servers: web: - - 192.168.0.1 - # job: - # hosts: - # - 192.168.0.1 - # cmd: bin/jobs - -# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. -# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. -# -# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. + - 167.71.173.177 + proxy: ssl: true - host: app.example.com + host: pinstr.co -# Credentials for your image host. registry: - # Specify the registry server, if you're not using Docker Hub - # server: registry.digitalocean.com / ghcr.io / ... - username: your-user - - # Always use an access token rather than real password when possible. + username: neudabei password: - KAMAL_REGISTRY_PASSWORD -# Inject ENV variables into containers (secrets come from .kamal/secrets). env: secret: + - KAMAL_REGISTRY_PASSWORD - RAILS_MASTER_KEY + - POSTGRES_PASSWORD + - DATABASE_URL clear: # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. # When you start using multiple servers, you should split out job processing to a dedicated machine. SOLID_QUEUE_IN_PUMA: true - + DB_HOST: 167.71.173.177 # Set number of processes dedicated to Solid Queue (default: 1) # JOB_CONCURRENCY: 3 @@ -76,7 +62,9 @@ asset_path: /rails/public/assets # Configure the image builder. builder: arch: amd64 - + secrets: + - KAMAL_REGISTRY_PASSWORD + - RAILS_MASTER_KEY # # Build image via remote server (useful for faster amd64 builds on arm64 computers) # remote: ssh://docker@docker-builder-server # @@ -92,22 +80,24 @@ builder: # user: app # Use accessory services (secrets come from .kamal/secrets). -# accessories: -# db: -# image: mysql:8.0 -# host: 192.168.0.2 -# # Change to 3306 to expose port to the world instead of just local network. -# port: "127.0.0.1:3306:3306" -# env: -# clear: -# MYSQL_ROOT_HOST: '%' -# secret: -# - MYSQL_ROOT_PASSWORD -# files: -# - config/mysql/production.cnf:/etc/mysql/my.cnf -# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql -# directories: -# - data:/var/lib/mysql +accessories: + postgres: + image: postgres:15 + host: 167.71.173.177 + port: 5432 + env: + clear: + DB_HOST: "pinstr-postgres" + POSTGRES_USER: "pinstr" + POSTGRES_DB: "pinstr_production" + secret: + - POSTGRES_PASSWORD + - DATABASE_URL + files: + - config/init.sql:/docker-entrypoint-initdb.d/setup.sql + directories: + - data:/var/lib/postgresql/data + # redis: # image: redis:7.0 # host: 192.168.0.2 diff --git a/config/init.sql b/config/init.sql new file mode 100644 index 0000000..c1f3a2b --- /dev/null +++ b/config/init.sql @@ -0,0 +1 @@ +CREATE DATABASE pinstr_production; From ba48fb75298326005eb8299a497734191bd22c3e Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Thu, 26 Jun 2025 21:29:01 +0200 Subject: [PATCH 15/33] Provide DATABASE_URL to all DBs The hope is that this fixes the issues on production with solid queue. --- config/database.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/database.yml b/config/database.yml index 44c565d..45a435c 100644 --- a/config/database.yml +++ b/config/database.yml @@ -89,13 +89,16 @@ production: password: <%= ENV["PINSTR_DATABASE_PASSWORD"] %> cache: <<: *primary_production + url: <%= ENV['DATABASE_URL'] %> database: pinstr_production_cache migrations_paths: db/cache_migrate queue: <<: *primary_production + url: <%= ENV['DATABASE_URL'] %> database: pinstr_production_queue migrations_paths: db/queue_migrate cable: <<: *primary_production + url: <%= ENV['DATABASE_URL'] %> database: pinstr_production_cable migrations_paths: db/cable_migrate From 7ee8e652832ea261aa1deb999d3cd451dca96d56 Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Thu, 26 Jun 2025 21:36:48 +0200 Subject: [PATCH 16/33] Upgrade to Rails 8.0.2 --- Gemfile | 2 +- Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 76ba9ae..c44b37f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "https://rubygems.org" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" -gem "rails", "~> 8.0.0" +gem "rails", "~> 8.0.2" # The modern asset pipeline for Rails [https://github.com/rails/propshaft] gem "propshaft" # Use postgresql as the database for Active Record diff --git a/Gemfile.lock b/Gemfile.lock index 8677616..a775c71 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -473,7 +473,7 @@ DEPENDENCIES propshaft pry puma (>= 5.0) - rails (~> 8.0.0) + rails (~> 8.0.2) rspec-rails (~> 7.0) rubocop-rails-omakase solid_cable From 8175913b646e97f93e15ca7051371d6ed3d7cefa Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Fri, 27 Jun 2025 11:53:43 +0200 Subject: [PATCH 17/33] Upgrade Kamal and use nostr_ruby fork --- Gemfile | 4 ++-- Gemfile.lock | 50 ++++++++++++++++++++++++++++---------------------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/Gemfile b/Gemfile index c44b37f..b0c5761 100644 --- a/Gemfile +++ b/Gemfile @@ -32,7 +32,7 @@ gem "solid_cable" gem "bootsnap", require: false # Deploy this application anywhere as a Docker container [https://kamal-deploy.org] -gem "kamal", require: false +gem "kamal", ">= 2.7", require: false # Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/] gem "thruster", require: false @@ -40,7 +40,7 @@ gem "thruster", require: false # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] # gem "image_processing", "~> 1.2" -gem "nostr_ruby" +gem 'nostr_ruby', git: 'https://github.com/zeroxbob/nostr-ruby.git' gem "websocket-client-simple" group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index a775c71..d9041ce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,15 @@ +GIT + remote: https://github.com/zeroxbob/nostr-ruby.git + revision: d83273332ee991620481379b078141b1738ba6b6 + specs: + nostr_ruby (0.3.0) + base64 (>= 0.2.0) + bech32 (~> 1.4.0) + bip-schnorr (~> 0.4.0) + faye-websocket (~> 0.11) + json (~> 2.6.2) + unicode-emoji (~> 3.3.1) + GEM remote: https://rubygems.org/ specs: @@ -75,7 +87,7 @@ GEM addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) ast (2.4.3) - base64 (0.1.2) + base64 (0.3.0) bcrypt_pbkdf (1.1.1) bcrypt_pbkdf (1.1.1-arm64-darwin) bcrypt_pbkdf (1.1.1-x86_64-darwin) @@ -113,7 +125,7 @@ GEM irb (~> 1.10) reline (>= 0.3.8) diff-lcs (1.6.1) - dotenv (2.8.1) + dotenv (3.1.8) drb (2.2.1) dry-configurable (1.3.0) dry-core (~> 1.1) @@ -145,7 +157,7 @@ GEM dry-logic (~> 1.4) zeitwerk (~> 2.6) ecdsa (1.2.0) - ed25519 (1.3.0) + ed25519 (1.4.0) erubi (1.13.1) et-orbi (1.2.11) tzinfo @@ -157,9 +169,9 @@ GEM json (~> 2.0) mime-types (~> 3.4) rack (~> 3.1) - faye-websocket (0.11.3) + faye-websocket (0.11.4) eventmachine (>= 0.12.0) - websocket-driver (>= 0.5.1) + websocket-driver (>= 0.5.1, < 0.8.0) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -180,16 +192,17 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) json (2.6.3) - kamal (1.3.0) + kamal (2.7.0) activesupport (>= 7.0) + base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) concurrent-ruby (~> 1.2) - dotenv (~> 2.8) - ed25519 (~> 1.2) - net-ssh (~> 7.0) - sshkit (~> 1.21) - thor (~> 1.2) - zeitwerk (~> 2.5) + dotenv (~> 3.1) + ed25519 (~> 1.4) + net-ssh (~> 7.3) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) language_server-protocol (3.17.0.4) lint_roller (1.1.0) logger (1.7.0) @@ -243,14 +256,7 @@ GEM racc (~> 1.4) nokogiri (1.18.7-x86_64-linux-musl) racc (~> 1.4) - nostr_ruby (0.3.0) - base64 (~> 0.1.1) - bech32 (~> 1.4.0) - bip-schnorr (~> 0.4.0) - faye-websocket (~> 0.11) - json (~> 2.6.2) - unicode-emoji (~> 3.3.1) - ostruct (0.6.1) + ostruct (0.6.2) parallel (1.27.0) parser (3.3.8.0) ast (~> 2.4.1) @@ -467,8 +473,8 @@ DEPENDENCIES debug importmap-rails jbuilder - kamal - nostr_ruby + kamal (>= 2.7) + nostr_ruby! pg (~> 1.1) propshaft pry From e12b37646355e79f923ed00f7f422bef7bc6dd9b Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Fri, 27 Jun 2025 12:31:47 +0200 Subject: [PATCH 18/33] Add DB password to queue DBs --- config/database.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/database.yml b/config/database.yml index 45a435c..76fb4bf 100644 --- a/config/database.yml +++ b/config/database.yml @@ -92,13 +92,16 @@ production: url: <%= ENV['DATABASE_URL'] %> database: pinstr_production_cache migrations_paths: db/cache_migrate + password: <%= ENV["PINSTR_DATABASE_PASSWORD"] %> queue: <<: *primary_production url: <%= ENV['DATABASE_URL'] %> database: pinstr_production_queue migrations_paths: db/queue_migrate + password: <%= ENV["PINSTR_DATABASE_PASSWORD"] %> cable: <<: *primary_production url: <%= ENV['DATABASE_URL'] %> database: pinstr_production_cable migrations_paths: db/cable_migrate + password: <%= ENV["PINSTR_DATABASE_PASSWORD"] %> From 4e94c704603d8ad67afb96b23dc9cd4b20c9cd06 Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Fri, 27 Jun 2025 17:33:37 +0200 Subject: [PATCH 19/33] Create DBs for cache, queue and cable --- config/database.yml | 6 ------ config/init.sql | 5 ++++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/config/database.yml b/config/database.yml index 76fb4bf..44c565d 100644 --- a/config/database.yml +++ b/config/database.yml @@ -89,19 +89,13 @@ production: password: <%= ENV["PINSTR_DATABASE_PASSWORD"] %> cache: <<: *primary_production - url: <%= ENV['DATABASE_URL'] %> database: pinstr_production_cache migrations_paths: db/cache_migrate - password: <%= ENV["PINSTR_DATABASE_PASSWORD"] %> queue: <<: *primary_production - url: <%= ENV['DATABASE_URL'] %> database: pinstr_production_queue migrations_paths: db/queue_migrate - password: <%= ENV["PINSTR_DATABASE_PASSWORD"] %> cable: <<: *primary_production - url: <%= ENV['DATABASE_URL'] %> database: pinstr_production_cable migrations_paths: db/cable_migrate - password: <%= ENV["PINSTR_DATABASE_PASSWORD"] %> diff --git a/config/init.sql b/config/init.sql index c1f3a2b..684815e 100644 --- a/config/init.sql +++ b/config/init.sql @@ -1 +1,4 @@ -CREATE DATABASE pinstr_production; +CREATE DATABASE IF NOT EXISTS `pinstr_production`; +CREATE DATABASE IF NOT EXISTS `pinstr_production_cache`; +CREATE DATABASE IF NOT EXISTS `pinstr_production_queue`; +CREATE DATABASE IF NOT EXISTS `pinstr_production_cable`; From e61fb630ea92a72cc98fad709f399978982a84f6 Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Fri, 27 Jun 2025 23:14:28 +0200 Subject: [PATCH 20/33] Use correct syntax for DB creation Your init.sql file uses MySQL syntax (CREATE DATABASE IF NOT EXISTS) but you're using PostgreSQL. PostgreSQL doesn't support the IF NOT EXISTS clause for CREATE DATABASE statements. --- config/init.sql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/init.sql b/config/init.sql index 684815e..437ee4f 100644 --- a/config/init.sql +++ b/config/init.sql @@ -1,4 +1,4 @@ -CREATE DATABASE IF NOT EXISTS `pinstr_production`; -CREATE DATABASE IF NOT EXISTS `pinstr_production_cache`; -CREATE DATABASE IF NOT EXISTS `pinstr_production_queue`; -CREATE DATABASE IF NOT EXISTS `pinstr_production_cable`; +CREATE DATABASE pinstr_production; +CREATE DATABASE pinstr_production_cache; +CREATE DATABASE pinstr_production_queue; +CREATE DATABASE pinstr_production_cable; From 120793f2426f46e59e1cbdcaf879750341ed0b5b Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Fri, 27 Jun 2025 23:30:43 +0200 Subject: [PATCH 21/33] Change DB setup 1. Changed POSTGRES_DB from "pinstr_production" to "postgres" in deploy.yml - this ensures the init.sql runs in the postgres database context 2. Updated init.sql to create all four databases (including pinstr_production) and grant privileges After redeploying with Kamal, all four databases should be created properly. The Solid Queue will then be able to connect to its dedicated pinstr_production_queue database. --- config/deploy.yml | 2 +- config/init-databases.sh | 21 +++++++++++++++++++++ config/init.sql | 7 +++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 config/init-databases.sh diff --git a/config/deploy.yml b/config/deploy.yml index 9401e69..04de061 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -89,7 +89,7 @@ accessories: clear: DB_HOST: "pinstr-postgres" POSTGRES_USER: "pinstr" - POSTGRES_DB: "pinstr_production" + POSTGRES_DB: "postgres" # Start with postgres DB to allow creating other databases secret: - POSTGRES_PASSWORD - DATABASE_URL diff --git a/config/init-databases.sh b/config/init-databases.sh new file mode 100644 index 0000000..18e67b8 --- /dev/null +++ b/config/init-databases.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -e + +# Wait for PostgreSQL to be ready +until PGPASSWORD=$POSTGRES_PASSWORD psql -h localhost -U "$POSTGRES_USER" -d postgres -c '\q'; do + >&2 echo "Postgres is unavailable - sleeping" + sleep 1 +done + +# Create databases +PGPASSWORD=$POSTGRES_PASSWORD psql -v ON_ERROR_STOP=1 -h localhost -U "$POSTGRES_USER" -d postgres <<-EOSQL + SELECT 'CREATE DATABASE pinstr_production' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'pinstr_production')\gexec + SELECT 'CREATE DATABASE pinstr_production_cache' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'pinstr_production_cache')\gexec + SELECT 'CREATE DATABASE pinstr_production_queue' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'pinstr_production_queue')\gexec + SELECT 'CREATE DATABASE pinstr_production_cable' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'pinstr_production_cable')\gexec + + GRANT ALL PRIVILEGES ON DATABASE pinstr_production TO $POSTGRES_USER; + GRANT ALL PRIVILEGES ON DATABASE pinstr_production_cache TO $POSTGRES_USER; + GRANT ALL PRIVILEGES ON DATABASE pinstr_production_queue TO $POSTGRES_USER; + GRANT ALL PRIVILEGES ON DATABASE pinstr_production_cable TO $POSTGRES_USER; +EOSQL \ No newline at end of file diff --git a/config/init.sql b/config/init.sql index 437ee4f..4d3590c 100644 --- a/config/init.sql +++ b/config/init.sql @@ -1,4 +1,11 @@ +-- Create all databases from postgres database CREATE DATABASE pinstr_production; CREATE DATABASE pinstr_production_cache; CREATE DATABASE pinstr_production_queue; CREATE DATABASE pinstr_production_cable; + +-- Grant all privileges to the pinstr user on all databases +GRANT ALL PRIVILEGES ON DATABASE pinstr_production TO pinstr; +GRANT ALL PRIVILEGES ON DATABASE pinstr_production_cache TO pinstr; +GRANT ALL PRIVILEGES ON DATABASE pinstr_production_queue TO pinstr; +GRANT ALL PRIVILEGES ON DATABASE pinstr_production_cable TO pinstr; From 30f1bebf67dd6b3d55fb1efb30dee0acadf2e42e Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Sun, 29 Jun 2025 13:07:08 +0200 Subject: [PATCH 22/33] Change DB settings for solid queue on Kamal This allows proper running of migrations and interacting with the solid queue db. --- config/database.yml | 4 ++-- config/deploy.yml | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/config/database.yml b/config/database.yml index 44c565d..539f9d1 100644 --- a/config/database.yml +++ b/config/database.yml @@ -83,10 +83,10 @@ test: production: primary: &primary_production <<: *default - url: <%= ENV['DATABASE_URL'] %> + host: <%= ENV["DB_HOST"] %> database: pinstr_production username: pinstr - password: <%= ENV["PINSTR_DATABASE_PASSWORD"] %> + password: <%= ENV["POSTGRES_PASSWORD"] %> cache: <<: *primary_production database: pinstr_production_cache diff --git a/config/deploy.yml b/config/deploy.yml index 04de061..7fd9bcd 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -89,7 +89,6 @@ accessories: clear: DB_HOST: "pinstr-postgres" POSTGRES_USER: "pinstr" - POSTGRES_DB: "postgres" # Start with postgres DB to allow creating other databases secret: - POSTGRES_PASSWORD - DATABASE_URL From cb684cc021d3ad45671674bcf62317cb5075b9e4 Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Sun, 29 Jun 2025 21:15:17 +0200 Subject: [PATCH 23/33] Grant permission to compile assets The fix adds the app/assets/builds directory to the ownership change in the Dockerfile, giving the rails user proper permissions to write the compiled Tailwind CSS file. This should resolve the EACCES permission error when running on the production server with Kamal. The updated Dockerfile now grants the rails user write permissions to both public/assets and app/assets/builds directories. This should resolve both permission errors you encountered when compiling assets on the production server. --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 604e394..9d82aa7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,7 +61,8 @@ COPY --from=build /rails /rails # Run and own only the runtime files as a non-root user for security RUN groupadd --system --gid 1000 rails && \ useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ - chown -R rails:rails db log storage tmp + chown -R rails:rails db log storage tmp public/assets app/assets/builds && \ + chmod -R 755 public/assets app/assets/builds USER 1000:1000 # Entrypoint prepares the database. From de7c66d7a725c2ec464d74238211ad01335ec7d3 Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Mon, 30 Jun 2025 16:22:28 +0200 Subject: [PATCH 24/33] Change precompilation command To properly precompile Tailwind assets in production. https://github.com/basecamp/kamal/issues/849 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9d82aa7..16645d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,7 +46,7 @@ COPY . . RUN bundle exec bootsnap precompile app/ lib/ # Precompiling assets for production without requiring secret RAILS_MASTER_KEY -RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile +RUN SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile From aa22b5b475467214da7defd59e016287cb5908df Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Mon, 30 Jun 2025 16:35:19 +0200 Subject: [PATCH 25/33] Improve tailwindcss build This should: 1. Build Tailwind CSS first (rails tailwindcss:build) 2. Then run asset precompilation (rails assets:precompile) 3. Ensure the manifest has the correct hash that matches what the HTML references The explicit Tailwind build step should resolve the hash mismatch issue. --- .claude/settings.local.json | 7 ++++++- Dockerfile | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index bb35714..28983fc 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,7 +5,12 @@ "Bash(npm run lint)", "Bash(npm run:*)", "Bash(rm:*)", - "Bash(bundle:*)" + "Bash(bundle:*)", + "Bash(find:*)", + "Bash(bin/rails:*)", + "Bash(ls:*)", + "Bash(cat:*)", + "WebFetch(domain:pinstr.co)" ], "deny": [] } diff --git a/Dockerfile b/Dockerfile index 16645d1..e835229 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,8 +45,11 @@ COPY . . # Precompile bootsnap code for faster boot times RUN bundle exec bootsnap precompile app/ lib/ +# Build Tailwind CSS first - this needs to happen before assets:precompile +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails tailwindcss:build + # Precompiling assets for production without requiring secret RAILS_MASTER_KEY -RUN SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile From b693a708a531713e6bc5f06910ed0857489f769a Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Tue, 1 Jul 2025 16:05:35 +0200 Subject: [PATCH 26/33] Undo changes to Dockerfile --- Dockerfile | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index e835229..604e394 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,9 +45,6 @@ COPY . . # Precompile bootsnap code for faster boot times RUN bundle exec bootsnap precompile app/ lib/ -# Build Tailwind CSS first - this needs to happen before assets:precompile -RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails tailwindcss:build - # Precompiling assets for production without requiring secret RAILS_MASTER_KEY RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile @@ -64,8 +61,7 @@ COPY --from=build /rails /rails # Run and own only the runtime files as a non-root user for security RUN groupadd --system --gid 1000 rails && \ useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ - chown -R rails:rails db log storage tmp public/assets app/assets/builds && \ - chmod -R 755 public/assets app/assets/builds + chown -R rails:rails db log storage tmp USER 1000:1000 # Entrypoint prepares the database. From 90f0b1709c433dd5ca7abd0912a988f85ea820b0 Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Sun, 6 Jul 2025 13:31:00 +0200 Subject: [PATCH 27/33] Clean up references to nostr extensions --- app/views/bookmarklet/add.html.erb | 2 +- app/views/bookmarklet/instructions.html.erb | 4 ++-- app/views/home/index.html.erb | 22 +-------------------- app/views/sessions/new.html.erb | 11 +++-------- 4 files changed, 7 insertions(+), 32 deletions(-) diff --git a/app/views/bookmarklet/add.html.erb b/app/views/bookmarklet/add.html.erb index 2c4239c..e0858e1 100644 --- a/app/views/bookmarklet/add.html.erb +++ b/app/views/bookmarklet/add.html.erb @@ -46,7 +46,7 @@

- Note: If you have a Nostr extension installed (like nos2x or Alby), you'll be prompted to sign this bookmark. + Note: If you have a Nostr extension installed, you'll be prompted to sign this bookmark. Nostr extension not working?

diff --git a/app/views/bookmarklet/instructions.html.erb b/app/views/bookmarklet/instructions.html.erb index 3dcaf20..1775a7b 100644 --- a/app/views/bookmarklet/instructions.html.erb +++ b/app/views/bookmarklet/instructions.html.erb @@ -35,7 +35,8 @@
  • If you've selected text on the page before clicking the bookmarklet, it will be included in the description field.
  • Review and edit the bookmark details if needed.
  • Click "Save Bookmark" to save it to your Pinstr account.
  • -
  • If you have a Nostr extension installed (like nos2x or Alby), you'll be prompted to sign the bookmark event for publishing to Nostr.
  • +
  • If you have a + Nostr extension installed, you'll be prompted to sign the bookmark event for publishing to Nostr.
  • After saving, you can close the popup or continue browsing.
  • @@ -48,7 +49,6 @@
    • Alby - Browser extension that includes Nostr capabilities
    • nos2x - Dedicated Nostr signer extension
    • -
    • Nostr Commander - For iOS/macOS users

    These extensions allow the bookmarklet to create a properly signed Nostr event (NIP-B0) for your bookmark, which can then be published to Nostr relays.

    diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb index 80998e9..e1e5a54 100644 --- a/app/views/home/index.html.erb +++ b/app/views/home/index.html.erb @@ -43,27 +43,7 @@

    Nostr Integration

    -

    For full functionality, you'll need a Nostr browser extension. We recommend:

    - -
    -
    -

    Alby

    -

    A Bitcoin and Nostr extension with wallet capabilities

    - Get Alby -
    - -
    -

    nos2x

    -

    A lightweight Nostr signer extension

    - Get nos2x -
    - -
    -

    Nostr Connect

    -

    For iOS and macOS users

    - Learn More -
    -
    +

    For full functionality, you'll need a Nostr browser extension.

    Test Your Nostr Extension diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index 6567972..478f959 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -3,12 +3,7 @@

    Checking for Nostr extension...

    @@ -79,4 +74,4 @@ color: #666; font-size: 0.8rem; } - \ No newline at end of file + From e885b600329dad709f36f4486a584ca9a23663e7 Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Sun, 6 Jul 2025 13:36:07 +0200 Subject: [PATCH 28/33] Move debug page outside of bookmarklet controller Also, don't require login to access it. --- app/controllers/bookmarklet_controller.rb | 4 ---- app/controllers/debug_controller.rb | 4 ++++ app/views/bookmarklet/add.html.erb | 2 +- app/views/bookmarklet/instructions.html.erb | 4 ++-- app/views/bookmarklet/success.html.erb | 2 +- .../{bookmarklet/debug.html.erb => debug/index.html.erb} | 0 app/views/home/index.html.erb | 2 +- app/views/sessions/new.html.erb | 2 +- config/routes.rb | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 app/controllers/debug_controller.rb rename app/views/{bookmarklet/debug.html.erb => debug/index.html.erb} (100%) diff --git a/app/controllers/bookmarklet_controller.rb b/app/controllers/bookmarklet_controller.rb index 7ab5ccc..25cd64e 100644 --- a/app/controllers/bookmarklet_controller.rb +++ b/app/controllers/bookmarklet_controller.rb @@ -8,10 +8,6 @@ def instructions # Show bookmarklet installation instructions end - def debug - # Debugging page for Nostr extension integration - end - def add @bookmark = Bookmark.new( url: params[:url], diff --git a/app/controllers/debug_controller.rb b/app/controllers/debug_controller.rb new file mode 100644 index 0000000..65a52e2 --- /dev/null +++ b/app/controllers/debug_controller.rb @@ -0,0 +1,4 @@ +class DebugController < ApplicationController + def index + end +end diff --git a/app/views/bookmarklet/add.html.erb b/app/views/bookmarklet/add.html.erb index e0858e1..b4e45dc 100644 --- a/app/views/bookmarklet/add.html.erb +++ b/app/views/bookmarklet/add.html.erb @@ -47,7 +47,7 @@

    Note: If you have a Nostr extension installed, you'll be prompted to sign this bookmark. - Nostr extension not working? + Nostr extension not working?

    diff --git a/app/views/bookmarklet/instructions.html.erb b/app/views/bookmarklet/instructions.html.erb index 1775a7b..7315957 100644 --- a/app/views/bookmarklet/instructions.html.erb +++ b/app/views/bookmarklet/instructions.html.erb @@ -54,7 +54,7 @@

    These extensions allow the bookmarklet to create a properly signed Nostr event (NIP-B0) for your bookmark, which can then be published to Nostr relays.

    - Having trouble with Nostr signing? Check out our debugging page to verify your Nostr extension is working correctly. + Having trouble with Nostr signing? Check out our debugging page to verify your Nostr extension is working correctly.
    @@ -77,7 +77,7 @@
    diff --git a/app/views/bookmarklet/success.html.erb b/app/views/bookmarklet/success.html.erb index 6b26af6..dc368c6 100644 --- a/app/views/bookmarklet/success.html.erb +++ b/app/views/bookmarklet/success.html.erb @@ -27,7 +27,7 @@

    Want to use Nostr for your bookmarks? - Set up Nostr integration + Set up Nostr integration

    <% end %> diff --git a/app/views/bookmarklet/debug.html.erb b/app/views/debug/index.html.erb similarity index 100% rename from app/views/bookmarklet/debug.html.erb rename to app/views/debug/index.html.erb diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb index e1e5a54..45a390a 100644 --- a/app/views/home/index.html.erb +++ b/app/views/home/index.html.erb @@ -46,5 +46,5 @@

    For full functionality, you'll need a Nostr browser extension.

    diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index 478f959..d7b79d8 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -15,7 +15,7 @@
    -

    If you're having trouble logging in, check our <%= link_to 'debug page', debug_bookmarklet_path, class: 'text-indigo-600 hover:text-indigo-800 hover:underline' %>

    +

    If you're having trouble logging in, check our <%= link_to 'debug page', debug_index_path, class: 'text-indigo-600 hover:text-indigo-800 hover:underline' %>

    diff --git a/config/routes.rb b/config/routes.rb index 16aa158..a4ef938 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,8 +19,8 @@ post "/bookmarklet", to: "bookmarklet#create", as: :create_from_bookmarklet get "/bookmarklet/instructions", to: "bookmarklet#instructions", as: :bookmarklet_instructions get "/bookmarklet/success", to: "bookmarklet#success", as: :success_bookmarklet - get "/bookmarklet/debug", to: "bookmarklet#debug", as: :debug_bookmarklet resources :sessions resources :bookmarks + resources :debug, only: :index end From 915ae92a42bfe54667b423b3de7cf8150396ec02 Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Sun, 6 Jul 2025 13:40:00 +0200 Subject: [PATCH 29/33] Move instructions outside of bookmarklet controller It should not be required to login to access this resource. --- app/controllers/bookmarklet_controller.rb | 4 ---- app/controllers/instructions_controller.rb | 4 ++++ app/views/debug/index.html.erb | 2 +- app/views/home/index.html.erb | 2 +- .../instructions.html.erb => instructions/index.html.erb} | 0 config/routes.rb | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 app/controllers/instructions_controller.rb rename app/views/{bookmarklet/instructions.html.erb => instructions/index.html.erb} (100%) diff --git a/app/controllers/bookmarklet_controller.rb b/app/controllers/bookmarklet_controller.rb index 25cd64e..2f2bff5 100644 --- a/app/controllers/bookmarklet_controller.rb +++ b/app/controllers/bookmarklet_controller.rb @@ -4,10 +4,6 @@ class BookmarkletController < ApplicationController skip_before_action :verify_authenticity_token, only: [:create] before_action :authenticate_user!, unless: -> { Rails.env.test? || current_user } - def instructions - # Show bookmarklet installation instructions - end - def add @bookmark = Bookmark.new( url: params[:url], diff --git a/app/controllers/instructions_controller.rb b/app/controllers/instructions_controller.rb new file mode 100644 index 0000000..809da46 --- /dev/null +++ b/app/controllers/instructions_controller.rb @@ -0,0 +1,4 @@ +class InstructionsController < ApplicationController + def index + end +end diff --git a/app/views/debug/index.html.erb b/app/views/debug/index.html.erb index 801214f..eae02f7 100644 --- a/app/views/debug/index.html.erb +++ b/app/views/debug/index.html.erb @@ -52,7 +52,7 @@
    diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb index 45a390a..35ace0c 100644 --- a/app/views/home/index.html.erb +++ b/app/views/home/index.html.erb @@ -36,7 +36,7 @@ diff --git a/app/views/bookmarklet/instructions.html.erb b/app/views/instructions/index.html.erb similarity index 100% rename from app/views/bookmarklet/instructions.html.erb rename to app/views/instructions/index.html.erb diff --git a/config/routes.rb b/config/routes.rb index a4ef938..5556207 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -17,10 +17,10 @@ # Bookmarklet routes get "/bookmarklet", to: "bookmarklet#add", as: :bookmarklet post "/bookmarklet", to: "bookmarklet#create", as: :create_from_bookmarklet - get "/bookmarklet/instructions", to: "bookmarklet#instructions", as: :bookmarklet_instructions get "/bookmarklet/success", to: "bookmarklet#success", as: :success_bookmarklet resources :sessions resources :bookmarks resources :debug, only: :index + resources :instructions, only: :index end From 9db2ee306e2c306a166df919813cb8e7cd095ab4 Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Sun, 6 Jul 2025 13:54:55 +0200 Subject: [PATCH 30/33] Update README --- README.md | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 7db80e4..d51666c 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,8 @@ # README -This README would normally document whatever steps are necessary to get the -application up and running. +A Nostr app to bookmark URLs. -Things you may want to cover: +- The user signs in and authenticates through their Nostr extension. +- Any bookmark that is added is broadcast to one or multiple Nostr relays. +- Bookmarks are sent as Nostr notes following the format defined in NIP-B0. -* Ruby version - -* System dependencies - -* Configuration - -* Database creation - -* Database initialization - -* How to run the test suite - -* Services (job queues, cache servers, search engines, etc.) - -* Deployment instructions - -* ... From 035ea9bbc6bbc78eca2c668a1274fa1221e9e7e5 Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Sun, 6 Jul 2025 14:26:41 +0200 Subject: [PATCH 31/33] Run bundle install with updated Ruby version 3.4.4 --- Gemfile.lock | 2 -- 1 file changed, 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d9041ce..cf31fd1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -89,8 +89,6 @@ GEM ast (2.4.3) base64 (0.3.0) bcrypt_pbkdf (1.1.1) - bcrypt_pbkdf (1.1.1-arm64-darwin) - bcrypt_pbkdf (1.1.1-x86_64-darwin) bech32 (1.4.2) thor (>= 1.1.0) benchmark (0.4.0) From 1a59ed662850da8305a623738145b621b72973b6 Mon Sep 17 00:00:00 2001 From: zeroxbob Date: Sun, 6 Jul 2025 14:28:02 +0200 Subject: [PATCH 32/33] Update bootsnap The Bootsnap precompile step was haning on kamal deploy. Using this forked Bootsnap version fixes the issues as discussed here: https://github.com/Shopify/bootsnap/issues/495#issuecomment-2770358158 --- Gemfile | 2 +- Gemfile.lock | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index b0c5761..5602f3d 100644 --- a/Gemfile +++ b/Gemfile @@ -29,7 +29,7 @@ gem "solid_queue" gem "solid_cable" # Reduces boot times through caching; required in config/boot.rb -gem "bootsnap", require: false +gem 'bootsnap', require: false, git: 'https://github.com/midnight-wonderer/ruby-bootsnap.git', branch: 'bug/test-forks-before-usage' # Deploy this application anywhere as a Docker container [https://kamal-deploy.org] gem "kamal", ">= 2.7", require: false diff --git a/Gemfile.lock b/Gemfile.lock index cf31fd1..3cc8eda 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,11 @@ +GIT + remote: https://github.com/midnight-wonderer/ruby-bootsnap.git + revision: 0e0df3395d5ee25713cb47e843236fcaded209f1 + branch: bug/test-forks-before-usage + specs: + bootsnap (1.18.4) + msgpack (~> 1.2) + GIT remote: https://github.com/zeroxbob/nostr-ruby.git revision: d83273332ee991620481379b078141b1738ba6b6 @@ -96,8 +104,6 @@ GEM bindex (0.8.1) bip-schnorr (0.4.0) ecdsa (~> 1.2.0) - bootsnap (1.18.4) - msgpack (~> 1.2) brakeman (7.0.2) racc builder (3.3.0) @@ -464,7 +470,7 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES - bootsnap + bootsnap! brakeman capybara capybara-playwright-driver From 52ad7f2e5b6bded6e051855753739020a66bf2ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 05:31:30 +0000 Subject: [PATCH 33/33] Bump tailwindcss-rails from 4.2.3 to 4.4.0 Bumps [tailwindcss-rails](https://github.com/rails/tailwindcss-rails) from 4.2.3 to 4.4.0. - [Release notes](https://github.com/rails/tailwindcss-rails/releases) - [Changelog](https://github.com/rails/tailwindcss-rails/blob/main/CHANGELOG.md) - [Commits](https://github.com/rails/tailwindcss-rails/compare/v4.2.3...v4.4.0) --- updated-dependencies: - dependency-name: tailwindcss-rails dependency-version: 4.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile | 2 +- Gemfile.lock | 74 +++++++++++++++++++++++++++------------------------- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/Gemfile b/Gemfile index 5602f3d..3a264ea 100644 --- a/Gemfile +++ b/Gemfile @@ -68,4 +68,4 @@ group :test do gem "capybara-playwright-driver" end -gem "tailwindcss-rails", "~> 4.2" +gem "tailwindcss-rails", "~> 4.4" diff --git a/Gemfile.lock b/Gemfile.lock index 3cc8eda..90b6c3e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,8 +99,8 @@ GEM bcrypt_pbkdf (1.1.1) bech32 (1.4.2) thor (>= 1.1.0) - benchmark (0.4.0) - bigdecimal (3.1.9) + benchmark (0.5.0) + bigdecimal (3.3.1) bindex (0.8.1) bip-schnorr (0.4.0) ecdsa (~> 1.2.0) @@ -122,7 +122,7 @@ GEM playwright-ruby-client (>= 1.16.0) coderay (1.1.3) concurrent-ruby (1.3.5) - connection_pool (2.5.1) + connection_pool (2.5.4) crass (1.0.6) date (3.4.1) debug (1.10.0) @@ -130,7 +130,7 @@ GEM reline (>= 0.3.8) diff-lcs (1.6.1) dotenv (3.1.8) - drb (2.2.1) + drb (2.2.3) dry-configurable (1.3.0) dry-core (~> 1.1) zeitwerk (~> 2.6) @@ -162,6 +162,7 @@ GEM zeitwerk (~> 2.6) ecdsa (1.2.0) ed25519 (1.4.0) + erb (5.1.3) erubi (1.13.1) et-orbi (1.2.11) tzinfo @@ -187,7 +188,7 @@ GEM actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) - io-console (0.8.0) + io-console (0.8.1) irb (1.15.2) pp (>= 0.6.0) rdoc (>= 4.0.0) @@ -210,7 +211,7 @@ GEM language_server-protocol (3.17.0.4) lint_roller (1.1.0) logger (1.7.0) - loofah (2.24.0) + loofah (2.24.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -226,7 +227,7 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2025.0429) mini_mime (1.1.5) - minitest (5.25.5) + minitest (5.26.0) msgpack (1.8.0) mutex_m (0.3.0) net-imap (0.5.6) @@ -244,21 +245,21 @@ GEM net-protocol net-ssh (7.3.0) nio4r (2.7.4) - nokogiri (1.18.7-aarch64-linux-gnu) + nokogiri (1.18.10-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.7-aarch64-linux-musl) + nokogiri (1.18.10-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.18.7-arm-linux-gnu) + nokogiri (1.18.10-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.18.7-arm-linux-musl) + nokogiri (1.18.10-arm-linux-musl) racc (~> 1.4) - nokogiri (1.18.7-arm64-darwin) + nokogiri (1.18.10-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.7-x86_64-darwin) + nokogiri (1.18.10-x86_64-darwin) racc (~> 1.4) - nokogiri (1.18.7-x86_64-linux-gnu) + nokogiri (1.18.10-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.7-x86_64-linux-musl) + nokogiri (1.18.10-x86_64-linux-musl) racc (~> 1.4) ostruct (0.6.2) parallel (1.27.0) @@ -269,7 +270,7 @@ GEM playwright-ruby-client (1.51.0) concurrent-ruby (>= 1.1.6) mime-types (>= 3.0) - pp (0.6.2) + pp (0.6.3) prettyprint prettyprint (0.2.0) prism (1.4.0) @@ -281,7 +282,7 @@ GEM pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) - psych (5.2.3) + psych (5.2.6) date stringio public_suffix (6.0.1) @@ -289,8 +290,8 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (3.1.13) - rack-session (2.1.0) + rack (3.2.3) + rack-session (2.1.1) base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.2.0) @@ -311,7 +312,7 @@ GEM activesupport (= 8.0.2) bundler (>= 1.15.0) railties (= 8.0.2) - rails-dom-testing (2.2.0) + rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) @@ -327,11 +328,13 @@ GEM thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.2.1) - rdoc (6.13.1) + rake (13.3.0) + rdoc (6.15.0) + erb psych (>= 4.0.0) + tsort regexp_parser (2.10.0) - reline (0.6.1) + reline (0.6.2) io-console (~> 0.5) rspec-core (3.13.3) rspec-support (~> 3.13.0) @@ -405,17 +408,17 @@ GEM ostruct stimulus-rails (1.3.4) railties (>= 6.0.0) - stringio (3.1.6) - tailwindcss-rails (4.2.3) + stringio (3.1.7) + tailwindcss-rails (4.4.0) railties (>= 7.0.0) tailwindcss-ruby (~> 4.0) - tailwindcss-ruby (4.1.8) - tailwindcss-ruby (4.1.8-aarch64-linux-gnu) - tailwindcss-ruby (4.1.8-aarch64-linux-musl) - tailwindcss-ruby (4.1.8-arm64-darwin) - tailwindcss-ruby (4.1.8-x86_64-darwin) - tailwindcss-ruby (4.1.8-x86_64-linux-gnu) - tailwindcss-ruby (4.1.8-x86_64-linux-musl) + tailwindcss-ruby (4.1.16) + tailwindcss-ruby (4.1.16-aarch64-linux-gnu) + tailwindcss-ruby (4.1.16-aarch64-linux-musl) + tailwindcss-ruby (4.1.16-arm64-darwin) + tailwindcss-ruby (4.1.16-x86_64-darwin) + tailwindcss-ruby (4.1.16-x86_64-linux-gnu) + tailwindcss-ruby (4.1.16-x86_64-linux-musl) thor (1.3.2) thruster (0.1.12) thruster (0.1.12-aarch64-linux) @@ -427,6 +430,7 @@ GEM rack (>= 2.0) rails (>= 7.1.0) timeout (0.4.3) + tsort (0.2.0) turbo-rails (2.0.13) actionpack (>= 7.1.0) railties (>= 7.1.0) @@ -436,7 +440,7 @@ GEM unicode-emoji (3.3.2) unicode-version (~> 1.0) unicode-version (1.5.0) - uri (1.0.3) + uri (1.0.4) useragent (0.16.11) web-console (4.2.1) actionview (>= 6.0.0) @@ -455,7 +459,7 @@ GEM websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.2) + zeitwerk (2.7.3) PLATFORMS aarch64-linux @@ -490,7 +494,7 @@ DEPENDENCIES solid_cache solid_queue stimulus-rails - tailwindcss-rails (~> 4.2) + tailwindcss-rails (~> 4.4) thruster tidewave turbo-rails