Skip to content

Commit fccefaa

Browse files
committed
Refactor feed creation and token internals
1 parent 46af096 commit fccefaa

22 files changed

Lines changed: 461 additions & 460 deletions

Gemfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ source 'https://rubygems.org'
44

55
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
66

7-
gem 'html2rss', '~> 0.18'
8-
# gem 'html2rss', github: 'html2rss/html2rss', branch: :master
7+
# gem 'html2rss', '~> 0.18'
8+
gem 'html2rss', github: 'html2rss/html2rss', branch: 'codex-pr-auto-fallback-pipeline'
99
gem 'html2rss-configs', github: 'html2rss/html2rss-configs'
1010

1111
# Use these instead of the two above (uncomment them) when developing locally:
1212
# gem 'html2rss', path: '../html2rss'
1313
# gem 'html2rss-configs', path: '../html2rss-configs'
1414

15+
gem 'concurrent-ruby'
1516
gem 'parallel'
1617
gem 'rack-cache'
1718
gem 'rack-timeout'
@@ -21,7 +22,6 @@ gem 'zeitwerk'
2122
gem 'puma', require: false
2223

2324
group :development do
24-
gem 'byebug'
2525
gem 'irb', require: false
2626
gem 'rake', require: false
2727
gem 'rubocop', require: false

Gemfile.lock

Lines changed: 73 additions & 70 deletions
Large diffs are not rendered by default.

app/web/api/v1/create_feed.rb

Lines changed: 57 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
# frozen_string_literal: true
22

3-
require 'time'
43
require 'json'
4+
require 'time'
55

66
module Html2rss
77
module Web
88
module Api
99
module V1
1010
##
1111
# Creates stable feed records from authenticated API requests.
12-
module CreateFeed # rubocop:disable Metrics/ModuleLength
12+
module CreateFeed
1313
FEED_ATTRIBUTE_KEYS =
1414
%i[id name url feed_token public_url json_public_url created_at updated_at].freeze
15-
class << self # rubocop:disable Metrics/ClassLength
15+
16+
class << self
1617
# Creates a feed and returns a normalized API success payload.
1718
#
1819
# @param request [Rack::Request] HTTP request with auth context.
1920
# @return [Hash{Symbol=>Object}] API response payload.
20-
def call(request) # rubocop:disable Metrics/MethodLength
21-
params, feed_data = build_feed_from_request(request)
21+
# rubocop:disable Metrics/MethodLength
22+
def call(request)
23+
account = require_account(request)
24+
params = build_create_params(request, account)
25+
feed_data = create_feed(params, account)
26+
2227
emit_create_success(params)
2328
Response.success(response: request.response,
2429
status: 201,
@@ -30,14 +35,10 @@ def call(request) # rubocop:disable Metrics/MethodLength
3035
emit_create_failure(error)
3136
raise
3237
end
38+
# rubocop:enable Metrics/MethodLength
3339

3440
private
3541

36-
# @return [void]
37-
def ensure_auto_source_enabled!
38-
raise Html2rss::Web::AutoSourceDisabledError unless AutoSource.enabled?
39-
end
40-
4142
# @param request [Rack::Request]
4243
# @return [Hash]
4344
def require_account(request)
@@ -47,15 +48,41 @@ def require_account(request)
4748
account
4849
end
4950

50-
# @param params [Hash]
51+
# @param request [Rack::Request]
5152
# @param account [Hash]
5253
# @return [Html2rss::Web::Api::V1::FeedMetadata::CreateParams]
53-
def build_create_params(params, account)
54-
url = validated_url(params['url'], account)
55-
FeedMetadata::CreateParams.new(
56-
url: url,
57-
name: FeedMetadata.site_title_for(url)
58-
)
54+
def build_create_params(request, account)
55+
url = validated_url(request_params(request)['url'], account)
56+
FeedMetadata::CreateParams.new(url:, name: FeedMetadata.site_title_for(url))
57+
end
58+
59+
# @param request [Rack::Request]
60+
# @return [Hash]
61+
def request_params(request)
62+
return request.params unless json_request?(request)
63+
64+
request.GET.merge(parsed_json_body(request))
65+
end
66+
67+
# @param request [Rack::Request]
68+
# @return [Hash]
69+
def parsed_json_body(request)
70+
raw_body = request.body.read
71+
request.body.rewind
72+
return {} if raw_body.strip.empty?
73+
74+
parsed = JSON.parse(raw_body)
75+
raise Html2rss::Web::BadRequestError, 'Invalid JSON payload' unless parsed.is_a?(Hash)
76+
77+
parsed
78+
rescue JSON::ParserError
79+
raise Html2rss::Web::BadRequestError, 'Invalid JSON payload'
80+
end
81+
82+
# @param request [Rack::Request]
83+
# @return [Boolean]
84+
def json_request?(request)
85+
request.env['CONTENT_TYPE'].to_s.include?('application/json')
5986
end
6087

6188
# @param raw_url [String, nil]
@@ -102,49 +129,23 @@ def hostname_input?(url)
102129
}ix.match?(url)
103130
end
104131

105-
# @param feed_data [Hash, Html2rss::Web::Api::V1::FeedMetadata::Metadata]
106-
# @return [Hash{Symbol=>Object}]
107-
def feed_attributes(feed_data)
108-
timestamp = Time.now.iso8601
109-
typed_feed = feed_metadata(feed_data)
110-
typed_feed_attributes(typed_feed, timestamp).slice(*FEED_ATTRIBUTE_KEYS)
111-
end
112-
113-
# @param request [Rack::Request]
114-
# @return [Hash]
115-
def request_params(request)
116-
return request.params unless json_request?(request)
117-
118-
raw_body = request.body.read
119-
request.body.rewind
120-
return request.params if raw_body.strip.empty?
121-
122-
parsed = JSON.parse(raw_body)
123-
raise Html2rss::Web::BadRequestError, 'Invalid JSON payload' unless parsed.is_a?(Hash)
124-
125-
request.params.merge(parsed)
126-
rescue JSON::ParserError
127-
raise Html2rss::Web::BadRequestError, 'Invalid JSON payload'
128-
end
129-
130-
# @param request [Rack::Request]
131-
# @return [Boolean]
132-
def json_request?(request)
133-
content_type = request.env['CONTENT_TYPE'].to_s
134-
content_type.include?('application/json')
135-
end
136-
137-
# @param request [Rack::Request]
138-
# @return [Array<(Html2rss::Web::Api::V1::FeedMetadata::CreateParams, Object)>]
139-
def build_feed_from_request(request)
140-
account = require_account(request)
141-
ensure_auto_source_enabled!
142-
params = build_create_params(request_params(request), account)
132+
# @param params [Html2rss::Web::Api::V1::FeedMetadata::CreateParams]
133+
# @param account [Hash]
134+
# @return [Html2rss::Web::Api::V1::FeedMetadata::Metadata]
135+
def create_feed(params, account)
136+
raise Html2rss::Web::AutoSourceDisabledError unless AutoSource.enabled?
143137

144138
feed_data = AutoSource.create_stable_feed(params.name, params.url, account)
145139
raise Html2rss::Web::InternalServerError, 'Failed to create feed' unless feed_data
146140

147-
[params, feed_data]
141+
feed_data.is_a?(FeedMetadata::Metadata) ? feed_data : FeedMetadata::Metadata.new(**feed_data)
142+
end
143+
144+
# @param feed_data [Html2rss::Web::Api::V1::FeedMetadata::Metadata]
145+
# @return [Hash{Symbol=>Object}]
146+
def feed_attributes(feed_data)
147+
timestamp = Time.now.iso8601
148+
feed_data.to_h.merge(created_at: timestamp, updated_at: timestamp).slice(*FEED_ATTRIBUTE_KEYS)
148149
end
149150

150151
# @param params [Html2rss::Web::Api::V1::FeedMetadata::CreateParams]
@@ -168,21 +169,6 @@ def emit_create_failure(error)
168169
level: :warn
169170
)
170171
end
171-
172-
# @param feed_data [Hash, Html2rss::Web::Api::V1::FeedMetadata::Metadata]
173-
# @return [Html2rss::Web::Api::V1::FeedMetadata::Metadata]
174-
def feed_metadata(feed_data)
175-
return feed_data if feed_data.is_a?(FeedMetadata::Metadata)
176-
177-
FeedMetadata::Metadata.new(**feed_data)
178-
end
179-
180-
# @param typed_feed [Html2rss::Web::Api::V1::FeedMetadata::Metadata]
181-
# @param timestamp [String]
182-
# @return [Hash{Symbol=>Object}]
183-
def typed_feed_attributes(typed_feed, timestamp)
184-
typed_feed.to_h.merge(created_at: timestamp, updated_at: timestamp)
185-
end
186172
end
187173
end
188174
end

app/web/boot.rb

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,33 @@ module Web
77
##
88
# Boot helpers for code loading and runtime setup.
99
module Boot
10+
@mutex = Mutex.new
11+
@setup = false
12+
@loader = nil
13+
1014
class << self
1115
# @param reloadable [Boolean]
1216
# @return [Zeitwerk::Loader]
1317
def setup!(reloadable: false)
14-
return loader if setup?
18+
@mutex.synchronize do
19+
return @loader if @setup
1520

16-
loader.enable_reloading if reloadable
17-
loader.setup
18-
@setup = true # rubocop:disable ThreadSafety/ClassInstanceVariable
19-
loader
21+
@loader ||= build_loader
22+
@loader.enable_reloading if reloadable
23+
@loader.setup
24+
@setup = true
25+
@loader
26+
end
2027
end
2128

2229
# @return [Zeitwerk::Loader]
2330
def loader
24-
@loader ||= build_loader # rubocop:disable ThreadSafety/ClassInstanceVariable
31+
@mutex.synchronize { @loader ||= build_loader }
2532
end
2633

2734
# @return [Boolean]
2835
def setup?
29-
# Loader setup happens once during process boot.
30-
# rubocop:disable ThreadSafety/ClassInstanceVariable
31-
@setup == true
32-
# rubocop:enable ThreadSafety/ClassInstanceVariable
36+
@mutex.synchronize { @setup == true }
3337
end
3438

3539
# @return [void]

app/web/config/local_config.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ module Web
1616
# Keeping lookup/defaulting here gives the rest of the app one predictable
1717
# config shape instead of repeating file parsing and fallback logic.
1818
module LocalConfig
19+
@mutex = Mutex.new
20+
@snapshot = nil
21+
1922
##
2023
# raised when the local config wasn't found
2124
class NotFound < RuntimeError; end
@@ -63,9 +66,7 @@ def yaml
6366
##
6467
# @return [Html2rss::Web::ConfigSnapshot::Snapshot]
6568
def snapshot
66-
return @snapshot if @snapshot # rubocop:disable ThreadSafety/ClassInstanceVariable
67-
68-
@snapshot = ConfigSnapshot.load(yaml) # rubocop:disable ThreadSafety/ClassInstanceVariable
69+
@mutex.synchronize { @snapshot ||= ConfigSnapshot.load(yaml) }
6970
rescue KeyError, TypeError, ArgumentError => error
7071
raise InvalidConfig, "Invalid local config: #{error.message}"
7172
end
@@ -74,7 +75,7 @@ def snapshot
7475
# @param reason [String]
7576
# @return [nil]
7677
def reload!(reason: 'manual')
77-
@snapshot = nil # rubocop:disable ThreadSafety/ClassInstanceVariable
78+
@mutex.synchronize { @snapshot = nil }
7879
SecurityLogger.log_cache_lifecycle('local_config', 'reload', reason: reason)
7980
nil
8081
end

app/web/config/runtime_env.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,20 @@ module Web
88
module RuntimeEnv
99
SENSITIVE_KEYS = %w[HTML2RSS_SECRET_KEY HEALTH_CHECK_TOKEN SENTRY_DSN].freeze
1010
BOOT_METADATA_KEYS = %w[BUILD_TAG GIT_SHA RACK_ENV SENTRY_ENABLE_LOGS].freeze
11+
@mutex = Mutex.new
12+
@values = nil
1113

1214
class << self
1315
# @return [void]
1416
def capture!
15-
@values = tracked_env_values.freeze # rubocop:disable ThreadSafety/ClassInstanceVariable
17+
@mutex.synchronize { @values = tracked_env_values.freeze }
1618
scrub_sensitive_env!
1719
nil
1820
end
1921

2022
# @return [void]
2123
def reset!
22-
@values = nil # rubocop:disable ThreadSafety/ClassInstanceVariable
24+
@mutex.synchronize { @values = nil }
2325
end
2426

2527
# @return [String]
@@ -70,8 +72,8 @@ def rack_env
7072
def fetch(key, default = :__missing__)
7173
return ENV.fetch(key) if ENV.key?(key)
7274

73-
values = @values || {} # rubocop:disable ThreadSafety/ClassInstanceVariable
74-
return values.fetch(key) if values.key?(key)
75+
current_values = @mutex.synchronize { @values || {} }
76+
return current_values.fetch(key) if current_values.key?(key)
7577
return default unless default == :__missing__
7678

7779
raise KeyError, "key not found: #{key}"

app/web/domain/auto_source.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ def enabled?
2222
# @param strategy [String, nil]
2323
# @return [Html2rss::Web::Api::V1::FeedMetadata::Metadata, nil]
2424
def create_stable_feed(name, url, token_data, strategy = nil)
25-
return nil unless token_data && FeedAccess.url_allowed_for_username?(token_data[:username], url)
25+
account = AccountManager.get_account_by_username(token_data&.dig(:username))
26+
return nil unless account && UrlValidator.url_allowed?(account, url)
2627

2728
feed_token = Auth.generate_feed_token(token_data[:username], url, strategy: strategy)
2829
return nil unless feed_token

0 commit comments

Comments
 (0)