From 215a438e56ee0852282b07b8efe7f7a6edc16442 Mon Sep 17 00:00:00 2001 From: robert Date: Fri, 20 Mar 2026 12:00:07 -0400 Subject: [PATCH] Renaming plugin. --- README.md | 26 ++++++------- .../bookmark_url/articles_controller.rb | 10 ++--- .../discourse/initializers/bookmark-url.js | 38 +++++++++---------- assets/stylesheets/bookmark-url.scss | 20 +++++----- config/locales/client.en.yml | 2 +- config/settings.yml | 12 +++--- lib/bookmark_url/article_extractor.rb | 10 ++--- plugin.rb | 20 +++++----- 8 files changed, 69 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 7e1d412..eb38c23 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# discourse-url-to-article +# discourse-bookmark-url A Discourse plugin that detects when a URL is pasted into the **topic title** field and offers to scrape the page, extracting the article content (à la browser Reader Mode) and populating the **composer body** with a clean Markdown rendering. @@ -25,7 +25,7 @@ hooks: - exec: cd: $home/plugins cmd: - - git clone https://code.draft13.com/robert/discourse-url-to-article.git + - git clone https://code.draft13.com/robert/discourse-bookmark-url.git ``` Then rebuild: `./launcher rebuild app` @@ -36,12 +36,12 @@ Then rebuild: `./launcher rebuild app` | Setting | Default | Description | |---|---|---| -| `url_to_article_enabled` | `true` | Enable/disable the plugin | -| `url_to_article_auto_populate` | `false` | Populate body automatically without button click | -| `url_to_article_max_content_length` | `50000` | Max chars extracted from a page | -| `url_to_article_fetch_timeout` | `10` | Seconds before HTTP fetch times out | -| `url_to_article_allowed_domains` | *(blank = all)* | Comma-separated domain allowlist | -| `url_to_article_blocked_domains` | `localhost,127.0.0.1,…` | SSRF blocklist | +| `bookmark_url_enabled` | `true` | Enable/disable the plugin | +| `bookmark_url_auto_populate` | `false` | Populate body automatically without button click | +| `bookmark_url_max_content_length` | `50000` | Max chars extracted from a page | +| `bookmark_url_fetch_timeout` | `10` | Seconds before HTTP fetch times out | +| `bookmark_url_allowed_domains` | *(blank = all)* | Comma-separated domain allowlist | +| `bookmark_url_blocked_domains` | `localhost,127.0.0.1,…` | SSRF blocklist | --- @@ -49,15 +49,15 @@ Then rebuild: `./launcher rebuild app` ### Frontend (Ember.js) -`initializers/url-to-article.js` hooks into the `composer-editor` component and observes the `composer.model.title` property via Ember's observer system. When the title matches a bare URL pattern: +`initializers/bookmark-url.js` hooks into the `composer-editor` component and observes the `composer.model.title` property via Ember's observer system. When the title matches a bare URL pattern: 1. A dismissible bar appears above the editor offering to import the article. -2. On click (or automatically if `auto_populate` is on), it POSTs to `/url-to-article/extract`. +2. On click (or automatically if `auto_populate` is on), it POSTs to `/bookmark-url/extract`. 3. The response populates `composer.model.reply` (body) and optionally updates the title. ### Backend (Ruby) -`ArticleExtractor` in `lib/url_to_article/article_extractor.rb`: +`ArticleExtractor` in `lib/bookmark_url/article_extractor.rb`: 1. **Fetches** the HTML via `Net::HTTP` with a browser-like User-Agent (follows one redirect). 2. **Extracts metadata** from Open Graph / Twitter Card / standard `` tags. @@ -67,7 +67,7 @@ Then rebuild: `./launcher rebuild app` ### Security -- Only authenticated users can call `/url-to-article/extract`. +- Only authenticated users can call `/bookmark-url/extract`. - Only `http`/`https` schemes are allowed. - Configurable domain blocklist (loopback/private addresses blocked by default). - Optional allowlist to restrict to specific domains. @@ -97,7 +97,7 @@ Full article text in Markdown... ### Custom extraction logic -Subclass or monkey-patch `UrlToArticle::ArticleExtractor` in a separate plugin to add site-specific selectors or post-processing. +Subclass or monkey-patch `BookmarkUrl::ArticleExtractor` in a separate plugin to add site-specific selectors or post-processing. ### Paywall / JS-rendered sites diff --git a/app/controllers/bookmark_url/articles_controller.rb b/app/controllers/bookmark_url/articles_controller.rb index 992accc..584b59e 100644 --- a/app/controllers/bookmark_url/articles_controller.rb +++ b/app/controllers/bookmark_url/articles_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module UrlToArticle +module BookmarkUrl class ArticlesController < ::ApplicationController requires_login before_action :ensure_enabled! @@ -18,14 +18,14 @@ module UrlToArticle url: result.url, } rescue => e - Rails.logger.warn("[url-to-article] Extraction failed for #{@url}: #{e.message}") + Rails.logger.warn("[bookmark-url] Extraction failed for #{@url}: #{e.message}") render json: { error: "Could not extract article: #{e.message}" }, status: :unprocessable_entity end private def ensure_enabled! - raise Discourse::NotFound unless SiteSetting.url_to_article_enabled + raise Discourse::NotFound unless SiteSetting.bookmark_url_enabled end def validate_url! @@ -42,7 +42,7 @@ module UrlToArticle end # SSRF protection — block private/loopback addresses - blocked_domains = SiteSetting.url_to_article_blocked_domains + blocked_domains = SiteSetting.bookmark_url_blocked_domains .split(",").map(&:strip).reject(&:empty?) if blocked_domains.any? { |d| uri.host&.include?(d) } @@ -50,7 +50,7 @@ module UrlToArticle end # Optionally enforce an allowlist - allowed_domains = SiteSetting.url_to_article_allowed_domains + allowed_domains = SiteSetting.bookmark_url_allowed_domains .split(",").map(&:strip).reject(&:empty?) if allowed_domains.any? && !allowed_domains.any? { |d| uri.host&.end_with?(d) } diff --git a/assets/javascripts/discourse/initializers/bookmark-url.js b/assets/javascripts/discourse/initializers/bookmark-url.js index aa38d2e..711df23 100644 --- a/assets/javascripts/discourse/initializers/bookmark-url.js +++ b/assets/javascripts/discourse/initializers/bookmark-url.js @@ -17,12 +17,12 @@ const STRINGS = { }; export default apiInitializer("1.8.0", (api) => { - if (!api.container.lookup("site-settings:main").url_to_article_enabled) { + if (!api.container.lookup("site-settings:main").bookmark_url_enabled) { return; } api.modifyClass("component:composer-editor", { - pluginId: "url-to-article", + pluginId: "bookmark-url", didInsertElement() { this._super(...arguments); @@ -75,30 +75,30 @@ export default apiInitializer("1.8.0", (api) => { this._hideArticleBar(); const bar = document.createElement("div"); - bar.className = "url-to-article-bar"; + bar.className = "bookmark-url-bar"; bar.innerHTML = ` - 📄 - ${STRINGS.bar_label} - - - `; - bar.querySelector(".url-to-article-btn").addEventListener("click", () => { + bar.querySelector(".bookmark-url-btn").addEventListener("click", () => { this._fetchAndPopulate(url); }); - bar.querySelector(".url-to-article-onebox-btn").addEventListener("click", () => { + bar.querySelector(".bookmark-url-onebox-btn").addEventListener("click", () => { this._hideArticleBar(); this._commitUrlToModel(); }); - bar.querySelector(".url-to-article-dismiss").addEventListener("click", () => { + bar.querySelector(".bookmark-url-dismiss").addEventListener("click", () => { this._hideArticleBar(); this._pendingUrl = null; }); @@ -113,21 +113,21 @@ export default apiInitializer("1.8.0", (api) => { _hideArticleBar() { this.element - ?.querySelectorAll(".url-to-article-bar") + ?.querySelectorAll(".bookmark-url-bar") .forEach((el) => el.remove()); }, _setStatus(message, type = "info") { - const bar = this.element?.querySelector(".url-to-article-bar"); + const bar = this.element?.querySelector(".bookmark-url-bar"); if (!bar) return; - let status = bar.querySelector(".url-to-article-status"); + let status = bar.querySelector(".bookmark-url-status"); if (!status) { status = document.createElement("span"); - status.className = "url-to-article-status"; + status.className = "bookmark-url-status"; bar.appendChild(status); } status.textContent = message; - status.className = `url-to-article-status url-to-article-status--${type}`; + status.className = `bookmark-url-status bookmark-url-status--${type}`; }, // Release the URL into Ember's data-binding so Discourse handles it normally @@ -143,8 +143,8 @@ export default apiInitializer("1.8.0", (api) => { // ---- Fetch & populate --------------------------------------------- async _fetchAndPopulate(url) { - const bar = this.element?.querySelector(".url-to-article-bar"); - const btn = bar?.querySelector(".url-to-article-btn"); + const bar = this.element?.querySelector(".bookmark-url-bar"); + const btn = bar?.querySelector(".bookmark-url-btn"); if (btn) { btn.disabled = true; @@ -153,7 +153,7 @@ export default apiInitializer("1.8.0", (api) => { this._setStatus(STRINGS.fetching, "info"); try { - const data = await ajax("/url-to-article/extract", { + const data = await ajax("/bookmark-url/extract", { type: "POST", data: { url }, }); diff --git a/assets/stylesheets/bookmark-url.scss b/assets/stylesheets/bookmark-url.scss index 24e9ad4..f0732bc 100644 --- a/assets/stylesheets/bookmark-url.scss +++ b/assets/stylesheets/bookmark-url.scss @@ -1,6 +1,6 @@ -/* URL-to-Article plugin styles */ +/* Bookmark URL plugin styles */ -.url-to-article-bar { +.bookmark-url-bar { display: flex; align-items: center; gap: 0.5rem; @@ -18,39 +18,39 @@ order: -1; } -.url-to-article-icon { +.bookmark-url-icon { font-size: 1.1em; flex-shrink: 0; } -.url-to-article-label { +.bookmark-url-label { flex: 1; min-width: 8rem; color: var(--primary-medium); font-weight: 500; } -.url-to-article-btn { +.bookmark-url-btn { flex-shrink: 0; } -.url-to-article-dismiss { +.bookmark-url-dismiss { flex-shrink: 0; padding: 0.25rem 0.5rem !important; color: var(--primary-medium) !important; } -.url-to-article-status { +.bookmark-url-status { font-style: italic; font-size: var(--font-down-1); - &.url-to-article-status--info { + &.bookmark-url-status--info { color: var(--tertiary); } - &.url-to-article-status--success { + &.bookmark-url-status--success { color: var(--success); } - &.url-to-article-status--error { + &.bookmark-url-status--error { color: var(--danger); } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 153e8a9..6d84ea3 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1,5 +1,5 @@ en: - url_to_article: + bookmark_url: bar_label: "URL detected — import as article?" fetch_button: "Import Article" onebox_button: "Use Onebox" diff --git a/config/settings.yml b/config/settings.yml index d2d6e4c..1993378 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -1,31 +1,31 @@ plugins: - url_to_article_enabled: + bookmark_url_enabled: default: true client: true type: bool - url_to_article_auto_populate: + bookmark_url_auto_populate: default: false client: true type: bool description: "Automatically populate the body when a URL is detected in the title (no button click needed)" - url_to_article_max_content_length: + bookmark_url_max_content_length: default: 50000 type: integer description: "Maximum number of characters to extract from a page" - url_to_article_fetch_timeout: + bookmark_url_fetch_timeout: default: 10 type: integer description: "Seconds to wait when fetching a URL" - url_to_article_allowed_domains: + bookmark_url_allowed_domains: default: "" type: string description: "Comma-separated list of allowed domains. Leave blank to allow all." - url_to_article_blocked_domains: + bookmark_url_blocked_domains: default: "localhost,127.0.0.1,0.0.0.0,::1" type: string description: "Comma-separated list of blocked domains (SSRF protection)" diff --git a/lib/bookmark_url/article_extractor.rb b/lib/bookmark_url/article_extractor.rb index d671399..d5b863d 100644 --- a/lib/bookmark_url/article_extractor.rb +++ b/lib/bookmark_url/article_extractor.rb @@ -7,7 +7,7 @@ require "net/http" require "uri" require "timeout" -module UrlToArticle +module BookmarkUrl class ArticleExtractor NOISE_SELECTORS = %w[ script style noscript iframe nav footer header @@ -80,7 +80,7 @@ module UrlToArticle # ------------------------------------------------------------------ # def fetch_html - Timeout.timeout(SiteSetting.url_to_article_fetch_timeout) do + Timeout.timeout(SiteSetting.bookmark_url_fetch_timeout) do response = do_get(@uri) if response.is_a?(Net::HTTPRedirection) && response["location"] @@ -100,10 +100,10 @@ module UrlToArticle http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == "https" http.open_timeout = 5 - http.read_timeout = SiteSetting.url_to_article_fetch_timeout + http.read_timeout = SiteSetting.bookmark_url_fetch_timeout req = Net::HTTP::Get.new(uri.request_uri) - req["User-Agent"] = "Mozilla/5.0 (compatible; Discourse/url-to-article)" + req["User-Agent"] = "Mozilla/5.0 (compatible; Discourse/bookmark-url)" req["Accept"] = "text/html,application/xhtml+xml" req["Accept-Language"] = "en-US,en;q=0.9" @@ -335,7 +335,7 @@ module UrlToArticle # ------------------------------------------------------------------ # def truncate(text) - max = SiteSetting.url_to_article_max_content_length + max = SiteSetting.bookmark_url_max_content_length return text if text.length <= max text[0...max] + "\n\n*[Content truncated — visit the original article for the full text.]*" end diff --git a/plugin.rb b/plugin.rb index 37462f3..0e8f33b 100644 --- a/plugin.rb +++ b/plugin.rb @@ -1,32 +1,32 @@ # frozen_string_literal: true -# name: discourse-url-to-article +# name: discourse-bookmark-url # about: Scrapes a URL pasted into the topic title and populates the composer body with the article content # version: 0.1.0 # authors: Robert Johnson -# url: https://code.draft13.com/robert/discourse-url-to-article +# url: https://code.draft13.com/robert/discourse-bookmark-url -enabled_site_setting :url_to_article_enabled +enabled_site_setting :bookmark_url_enabled after_initialize do - require_relative "lib/url_to_article/article_extractor" + require_relative "lib/bookmark_url/article_extractor" - module ::UrlToArticle - PLUGIN_NAME = "discourse-url-to-article" + module ::BookmarkUrl + PLUGIN_NAME = "discourse-bookmark-url" class Engine < ::Rails::Engine engine_name PLUGIN_NAME - isolate_namespace UrlToArticle + isolate_namespace BookmarkUrl end end - require_relative "app/controllers/url_to_article/articles_controller" + require_relative "app/controllers/bookmark_url/articles_controller" - UrlToArticle::Engine.routes.draw do + BookmarkUrl::Engine.routes.draw do post "/extract" => "articles#extract" end Discourse::Application.routes.append do - mount UrlToArticle::Engine, at: "/url-to-article" + mount BookmarkUrl::Engine, at: "/bookmark-url" end end