From 244b0e46cb2720132e95756ababded0c5566d76f Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Sun, 23 Aug 2026 09:02:46 +0200 Subject: [PATCH] No application code reads ENV, and the wiki has one address again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The API read its environment wherever it happened to need it: `Image` in the model, `RssService` and `WikiService` in class-level constants, the softwares admin page in a sidebar. Two of those wanted the same wiki and disagreed about its name — `RssService::WIKI_URL` against `WikiService::GRAV_URL` — and only the second one is set in docker-compose, so the howtos feed had been linking to the hard-coded default all along. Every `ENV` read is in `config/application.rb` now, as `config.x.site_url`, `config.x.wiki_url` and `config.x.images.container_path`, and the code asks `Rails.configuration.x`. One place says what this app needs from its environment, and a test can override it. `RssService` was three copies of the same twenty-line `RSS::Maker` block. It is `Rss::Feed` plus `Rss::BlogFeed`, `Rss::ReleasesFeed` and `Rss::HowtosFeed`, each of which now only answers what its title, its link and its items are — and the `Time.parse(...) rescue Time.current` modifier, which swallowed everything, is a rescue of ArgumentError and TypeError. `WikiService` becomes `Wiki::Pages` and loses `alias_method :pages, :index`: two names for one method meant the controller and the feeds each called it something different. The admin cookie was a monkey patch — `ApplicationController.class_eval` in an initializer, adding an `after_action` that the three-line controller file gave no hint of. It is a `SyncsAdminCookie` concern the controller includes. And the engine stops reading the host's config keys: the "View on site" link in the softwares admin asked for `Rails.configuration.x.site_url`, which is ours, not its. `c.site_url` is an engine setting now, nil by default, and the link is left out when the host does not set it. Co-Authored-By: Claude Opus 5 (1M context) --- .../api/app/controllers/api/rss_controller.rb | 6 +- .../app/controllers/api/wiki_controller.rb | 2 +- .../app/controllers/application_controller.rb | 2 + .../concerns/syncs_admin_cookie.rb | 17 +++++ apps/api/app/services/rss/blog_feed.rb | 20 +++++ apps/api/app/services/rss/feed.rb | 35 +++++++++ apps/api/app/services/rss/howtos_feed.rb | 20 +++++ apps/api/app/services/rss/releases_feed.rb | 23 ++++++ apps/api/app/services/rss_service.rb | 73 ------------------- apps/api/app/services/wiki/pages.rb | 37 ++++++++++ apps/api/app/services/wiki_service.rb | 31 -------- apps/api/config/application.rb | 4 + apps/api/config/initializers/admin_cookie.rb | 15 ---- apps/api/config/initializers/warp_engine.rb | 2 + .../lib/warp_engine/configuration.rb | 4 +- 15 files changed, 167 insertions(+), 124 deletions(-) create mode 100644 apps/api/app/controllers/concerns/syncs_admin_cookie.rb create mode 100644 apps/api/app/services/rss/blog_feed.rb create mode 100644 apps/api/app/services/rss/feed.rb create mode 100644 apps/api/app/services/rss/howtos_feed.rb create mode 100644 apps/api/app/services/rss/releases_feed.rb delete mode 100644 apps/api/app/services/rss_service.rb create mode 100644 apps/api/app/services/wiki/pages.rb delete mode 100644 apps/api/app/services/wiki_service.rb delete mode 100644 apps/api/config/initializers/admin_cookie.rb diff --git a/apps/api/app/controllers/api/rss_controller.rb b/apps/api/app/controllers/api/rss_controller.rb index f48b444..ddd2dfa 100644 --- a/apps/api/app/controllers/api/rss_controller.rb +++ b/apps/api/app/controllers/api/rss_controller.rb @@ -7,21 +7,21 @@ class Api::RssController < ApiController api :GET, "/api/rss/blog", "Blog RSS feed" returns code: 200, desc: "RSS XML feed of blog posts" def blog - xml = RssService.new.blog_feed + xml = Rss::BlogFeed.new.xml render xml: xml, content_type: "application/rss+xml" end api :GET, "/api/rss/releases", "Software releases RSS feed" returns code: 200, desc: "RSS XML feed of software releases" def releases - xml = RssService.new.releases_feed + xml = Rss::ReleasesFeed.new.xml render xml: xml, content_type: "application/rss+xml" end api :GET, "/api/rss/howtos", "HowTos RSS feed" returns code: 200, desc: "RSS XML feed of tech howtos" def howtos - xml = RssService.new.howtos_feed + xml = Rss::HowtosFeed.new.xml render xml: xml, content_type: "application/rss+xml" end end diff --git a/apps/api/app/controllers/api/wiki_controller.rb b/apps/api/app/controllers/api/wiki_controller.rb index 3789263..fe362ef 100644 --- a/apps/api/app/controllers/api/wiki_controller.rb +++ b/apps/api/app/controllers/api/wiki_controller.rb @@ -28,7 +28,7 @@ class Api::WikiController < ApiController end def index - render json: WikiService.new.index( + render json: Wiki::Pages.new.fetch( tag: params[:tag], limit: params[:limit], body: params[:body] diff --git a/apps/api/app/controllers/application_controller.rb b/apps/api/app/controllers/application_controller.rb index 1c07694..8dba3f4 100644 --- a/apps/api/app/controllers/application_controller.rb +++ b/apps/api/app/controllers/application_controller.rb @@ -1,3 +1,5 @@ class ApplicationController < ActionController::Base + include SyncsAdminCookie + protect_from_forgery with: :exception end diff --git a/apps/api/app/controllers/concerns/syncs_admin_cookie.rb b/apps/api/app/controllers/concerns/syncs_admin_cookie.rb new file mode 100644 index 0000000..388d580 --- /dev/null +++ b/apps/api/app/controllers/concerns/syncs_admin_cookie.rb @@ -0,0 +1,17 @@ +module SyncsAdminCookie + extend ActiveSupport::Concern + + included do + after_action :sync_admin_cookie + end + + private + + def sync_admin_cookie + if current_admin_user + cookies[:is_admin] = { value: "1", httponly: false, same_site: :lax, path: "/" } + elsif cookies[:is_admin] + cookies.delete(:is_admin, path: "/") + end + end +end diff --git a/apps/api/app/services/rss/blog_feed.rb b/apps/api/app/services/rss/blog_feed.rb new file mode 100644 index 0000000..e283db5 --- /dev/null +++ b/apps/api/app/services/rss/blog_feed.rb @@ -0,0 +1,20 @@ +module Rss + class BlogFeed < Feed + private + + def title = "Teletype Games Blog" + def link = "#{site_url}/blog" + def description = "Latest blog posts from Teletype Games" + + def items + Wiki::Pages.new.all(tag: "blog", limit: 30).map do |page| + { + title: page["title"], + link: "#{site_url}/blog/#{page['route']}", + description: page["description"], + published_at: parse_time(page["createdAt"]) + } + end + end + end +end diff --git a/apps/api/app/services/rss/feed.rb b/apps/api/app/services/rss/feed.rb new file mode 100644 index 0000000..c8c2b11 --- /dev/null +++ b/apps/api/app/services/rss/feed.rb @@ -0,0 +1,35 @@ +require "rss" + +module Rss + class Feed + def xml + RSS::Maker.make("2.0") do |maker| + maker.channel.title = title + maker.channel.link = link + maker.channel.description = description + maker.channel.language = "hu" + + items.each do |item| + maker.items.new_item do |rss_item| + rss_item.title = item[:title] + rss_item.link = item[:link] + rss_item.description = item[:description].to_s + rss_item.pubDate = item[:published_at] + end + end + end.to_s + end + + private + + def site_url = Rails.configuration.x.site_url + + def wiki_url = Rails.configuration.x.wiki_url + + def parse_time(value) + Time.parse(value.to_s) + rescue ArgumentError, TypeError + Time.current + end + end +end diff --git a/apps/api/app/services/rss/howtos_feed.rb b/apps/api/app/services/rss/howtos_feed.rb new file mode 100644 index 0000000..d6a9b93 --- /dev/null +++ b/apps/api/app/services/rss/howtos_feed.rb @@ -0,0 +1,20 @@ +module Rss + class HowtosFeed < Feed + private + + def title = "Teletype Games HowTos" + def link = "#{site_url}/howtos" + def description = "Latest tech howtos from Teletype Games" + + def items + Wiki::Pages.new.all(tag: "howto", limit: 30).map do |page| + { + title: page["title"], + link: "#{wiki_url}/#{page['path']}", + description: page["description"], + published_at: parse_time(page["createdAt"]) + } + end + end + end +end diff --git a/apps/api/app/services/rss/releases_feed.rb b/apps/api/app/services/rss/releases_feed.rb new file mode 100644 index 0000000..e845268 --- /dev/null +++ b/apps/api/app/services/rss/releases_feed.rb @@ -0,0 +1,23 @@ +module Rss + class ReleasesFeed < Feed + private + + def title = "Teletype Games Releases" + def link = "#{site_url}/catalog" + def description = "Latest game releases from Teletype Games" + + def items + WarpEngine::Release.includes(:software).order(created_at: :desc).limit(50).filter_map do |release| + software = release.software + next if software.nil? + + { + title: "#{software.title} v#{release.version}", + link: "#{site_url}/catalog/#{software.name}", + description: "#{software.title} #{release.version} released – #{software.desc}", + published_at: release.created_at.to_time + } + end + end + end +end diff --git a/apps/api/app/services/rss_service.rb b/apps/api/app/services/rss_service.rb deleted file mode 100644 index 9bbe087..0000000 --- a/apps/api/app/services/rss_service.rb +++ /dev/null @@ -1,73 +0,0 @@ -require "rss" - -class RssService - SITE_URL = ENV.fetch("SITE_URL", "https://teletypegames.org").freeze - WIKI_URL = ENV.fetch("WIKI_URL", "https://wiki.teletypegames.org").freeze - - def blog_feed - pages = WikiService.new.pages(tag: "blog", limit: 30) - items = pages.fetch("pages", []) - - RSS::Maker.make("2.0") do |maker| - maker.channel.title = "Teletype Games Blog" - maker.channel.link = "#{SITE_URL}/blog" - maker.channel.description = "Latest blog posts from Teletype Games" - maker.channel.language = "hu" - - items.each do |page| - maker.items.new_item do |item| - item.title = page["title"] - item.link = "#{SITE_URL}/blog/#{page["route"]}" - item.description = page["description"].to_s - item.pubDate = Time.parse(page["createdAt"]) rescue Time.current - end - end - end.to_s - end - - def releases_feed - releases = WarpEngine::Release.includes(:software) - .order(created_at: :desc) - .limit(50) - - RSS::Maker.make("2.0") do |maker| - maker.channel.title = "Teletype Games Releases" - maker.channel.link = "#{SITE_URL}/catalog" - maker.channel.description = "Latest game releases from Teletype Games" - maker.channel.language = "hu" - - releases.each do |release| - sw = release.software - next unless sw - - maker.items.new_item do |item| - item.title = "#{sw.title} v#{release.version}" - item.link = "#{SITE_URL}/catalog/#{sw.name}" - item.description = "#{sw.title} #{release.version} released – #{sw.desc}" - item.pubDate = release.created_at.to_time - end - end - end.to_s - end - - def howtos_feed - pages = WikiService.new.pages(tag: "howto", limit: 30) - items = pages.fetch("pages", []) - - RSS::Maker.make("2.0") do |maker| - maker.channel.title = "Teletype Games HowTos" - maker.channel.link = "#{SITE_URL}/howtos" - maker.channel.description = "Latest tech howtos from Teletype Games" - maker.channel.language = "hu" - - items.each do |page| - maker.items.new_item do |item| - item.title = page["title"] - item.link = "#{WIKI_URL}/#{page["path"]}" - item.description = page["description"].to_s - item.pubDate = Time.parse(page["createdAt"]) rescue Time.current - end - end - end.to_s - end -end diff --git a/apps/api/app/services/wiki/pages.rb b/apps/api/app/services/wiki/pages.rb new file mode 100644 index 0000000..53f04d1 --- /dev/null +++ b/apps/api/app/services/wiki/pages.rb @@ -0,0 +1,37 @@ +require "net/http" +require "json" + +module Wiki + class Pages + def fetch(tag:, limit: nil, body: nil) + query = { tag: tag } + query[:limit] = limit if limit.present? + query[:body] = body if body.present? + + uri = URI.parse("#{Rails.configuration.x.wiki_url}/custom/pages.json") + uri.query = URI.encode_www_form(query) + + response = Net::HTTP.start( + uri.host, uri.port, + use_ssl: uri.scheme == "https", + open_timeout: 5, read_timeout: 10 + ) { |http| http.get(uri.request_uri) } + + return empty(tag, "grav responded #{response.code}") unless response.is_a?(Net::HTTPSuccess) + + JSON.parse(response.body) + rescue StandardError => e + empty(tag, e.message) + end + + def all(tag:, limit: nil) + fetch(tag: tag, limit: limit).fetch("pages", []) + end + + private + + def empty(tag, error) + { "tag" => tag, "count" => 0, "pages" => [], "error" => error } + end + end +end diff --git a/apps/api/app/services/wiki_service.rb b/apps/api/app/services/wiki_service.rb deleted file mode 100644 index d9a9ddc..0000000 --- a/apps/api/app/services/wiki_service.rb +++ /dev/null @@ -1,31 +0,0 @@ -require "net/http" -require "json" - -class WikiService - GRAV_URL = ENV.fetch("WIKI_GRAV_URL", "http://localhost:8080").freeze - - def index(tag:, limit: nil, body: nil) - query = { tag: tag } - query[:limit] = limit if limit.present? - query[:body] = body if body.present? - - uri = URI.parse("#{GRAV_URL}/custom/pages.json") - uri.query = URI.encode_www_form(query) - - response = Net::HTTP.start( - uri.host, uri.port, - use_ssl: uri.scheme == "https", - open_timeout: 5, read_timeout: 10 - ) { |http| http.get(uri.request_uri) } - - unless response.is_a?(Net::HTTPSuccess) - return { "tag" => tag, "count" => 0, "pages" => [], "error" => "grav responded #{response.code}" } - end - - JSON.parse(response.body) - rescue StandardError => e - { "tag" => tag, "count" => 0, "pages" => [], "error" => e.message } - end - - alias_method :pages, :index -end diff --git a/apps/api/config/application.rb b/apps/api/config/application.rb index 01834f8..3b0cd57 100644 --- a/apps/api/config/application.rb +++ b/apps/api/config/application.rb @@ -25,5 +25,9 @@ module Api config.action_controller.forgery_protection_origin_check = false config.autoload_lib(ignore: %w[assets tasks]) + + config.x.site_url = ENV.fetch("SITE_URL", "https://teletypegames.org") + config.x.wiki_url = ENV.fetch("WIKI_GRAV_URL", "https://wiki.teletypegames.org") + config.x.images.container_path = ENV.fetch("IMAGE_CONTAINER_PATH", "/images") end end diff --git a/apps/api/config/initializers/admin_cookie.rb b/apps/api/config/initializers/admin_cookie.rb deleted file mode 100644 index 89d3a48..0000000 --- a/apps/api/config/initializers/admin_cookie.rb +++ /dev/null @@ -1,15 +0,0 @@ -Rails.application.config.to_prepare do - ApplicationController.class_eval do - after_action :sync_admin_cookie - - private - - def sync_admin_cookie - if current_admin_user - cookies[:is_admin] = { value: "1", httponly: false, same_site: :lax, path: "/" } - elsif cookies[:is_admin] - cookies.delete(:is_admin, path: "/") - end - end - end -end diff --git a/apps/api/config/initializers/warp_engine.rb b/apps/api/config/initializers/warp_engine.rb index fac7e14..17d45a8 100644 --- a/apps/api/config/initializers/warp_engine.rb +++ b/apps/api/config/initializers/warp_engine.rb @@ -6,6 +6,8 @@ Rails.application.config.to_prepare do c.image_class_name = "Image" + c.site_url = Rails.configuration.x.site_url + c.ci_adapter = WarpEngine::CI::Woodpecker::Adapter.new( url: ENV["WOODPECKER_URL"], api_token: ENV["WOODPECKER_API_TOKEN"], diff --git a/libs/ruby/warp_engine/lib/warp_engine/configuration.rb b/libs/ruby/warp_engine/lib/warp_engine/configuration.rb index 91d41f6..63c2bae 100644 --- a/libs/ruby/warp_engine/lib/warp_engine/configuration.rb +++ b/libs/ruby/warp_engine/lib/warp_engine/configuration.rb @@ -1,7 +1,8 @@ module WarpEngine class Configuration - attr_accessor :file_container_path, + attr_accessor :site_url, + :file_container_path, :update_secret, :application_token_source, :application_token_owner_class, @@ -19,6 +20,7 @@ module WarpEngine :device_code_interval def initialize + @site_url = nil @file_container_path = ENV.fetch("FILE_CONTAINER_PATH", "/softwares") @update_secret = ENV["UPDATE_SECRET"] @application_token_source = :env