Skip to content

Commit 5f811cc

Browse files
authored
F2: Implement Publisher service (#594)
* Pass a client object to the Publisher instead of the credentials * Define credentials accessors in the feeder config * Integrate downloader service to the API wrapper * Refactor Request class * Accept HTTP client instead of the client params during API wrapper init * Init downloader with HTTP client instead of the client params * Refactor request objects initialization * Use consistent terminology * Extract post publisher from batch publisher * Parameterize HTTP client for the downloader * Reek * Rubocop
1 parent 0a2effe commit 5f811cc

17 files changed

+261
-71
lines changed

app/services/batch_publisher.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Takes a collection of posts, publishes each, updates post status.
2+
# Does not interrupt on publication error.
3+
#
4+
class BatchPublisher
5+
include Logging
6+
7+
attr_reader :posts, :freefeed_client
8+
9+
def initialize(posts:, freefeed_client:)
10+
@posts = posts
11+
@freefeed_client = freefeed_client
12+
end
13+
14+
def publish
15+
logger.info("publishing #{TextHelpers.pluralize(pending_posts.count, "posts")}")
16+
17+
posts.each do |post|
18+
logger.info("publishing post: #{post.id}")
19+
publish_post(post)
20+
end
21+
end
22+
23+
private
24+
25+
def publish_post(post)
26+
post.with_lock do
27+
next unless post.reload.enqueued?
28+
PostPublisher.new(post: post, freefeed_client: freefeed_client).publish
29+
end
30+
end
31+
end

app/services/feed_processor.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,20 @@ def initialize(feeds:)
1414
def perform
1515
feeds.each do |feed|
1616
Importer.new(feed).import
17-
Publisher.new(posts: feed.posts.pending).publish
17+
Publisher.new(posts: feed.posts.pending, freefeed_client: build_freefeed_client).publish
18+
1819
# TBD: Handle errors
1920
end
2021
end
22+
23+
private
24+
25+
def build_freefeed_client
26+
feeder = Rails.configuration.feeder
27+
28+
Freefeed::Client.new(
29+
token: feeder.freefeed_token,
30+
base_url: feeder.freefeed_base_url
31+
)
32+
end
2133
end

app/services/post_publisher.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
class PostPublisher
2+
include Logging
3+
4+
attr_reader :post, :freefeed_client
5+
6+
def initialize(post:, freefeed_client:)
7+
@post = post
8+
@freefeed_client = freefeed_client
9+
end
10+
11+
# TBD :reek:TooManyStatements
12+
def publish
13+
attachment_ids = create_attachments
14+
post_id = create_post(attachment_ids)
15+
post.update(freefeed_post_id: post_id)
16+
create_comments(post_id)
17+
post.success!
18+
logger.info("---> new post URL: #{post.permalink}")
19+
rescue StandardError
20+
post.fail!
21+
# TBD: Report error
22+
end
23+
24+
private
25+
26+
def create_post(attachment_ids)
27+
response = freefeed_client.create_post(
28+
post: {
29+
body: post.text,
30+
attachments: attachment_ids
31+
},
32+
meta: {
33+
feeds: [post.feed.name]
34+
}
35+
)
36+
37+
response.parse.dig("posts", "id")
38+
end
39+
40+
def create_comments(post_id)
41+
post.comments.each do |comment|
42+
freefeed_client.create_comment(
43+
comment: {
44+
body: comment,
45+
postId: post_id
46+
}
47+
)
48+
end
49+
end
50+
51+
def create_attachments
52+
post.attachments.map { |url| create_attachment(url) }
53+
end
54+
55+
def create_attachment(url)
56+
Downloader.call(url) do |io, content_type|
57+
response = freefeed_client.create_attachment(io, content_type: content_type)
58+
response.parse.fetch("attachments").fetch("id")
59+
end
60+
end
61+
end

app/services/publisher.rb

Lines changed: 0 additions & 27 deletions
This file was deleted.

config/feeder.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
shared:
2-
freefeed_base_url: "<%= ENV['FREEFEED_BASE_URL'] || 'https://candy.freefeed.net' %>"
2+
freefeed_base_url: "<%= ENV.fetch('FREEFEED_BASE_URL') { 'https://candy.freefeed.net' } %>"
3+
freefeed_token: "<%= ENV.fetch('FREEFEED_TOKEN') %>"
4+
35
feeds:
46
- name: "xkcd"
57
state: "enabled"

lib/freefeed/authenticated_request.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ class AuthenticatedRequest < Request
33
private
44

55
def headers
6-
super.merge(authorization: "Bearer #{client.token}")
6+
super.merge(authorization: "Bearer #{token}")
77
end
88
end
99
end

lib/freefeed/client.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ class Client
1010

1111
attr_reader :token, :base_url
1212

13-
def initialize(token:, base_url: Freefeed::BASE_URL)
13+
def initialize(token:, base_url: Freefeed::BASE_URL, http_client: nil)
1414
@token = token
1515
@base_url = base_url
16+
@http_client = http_client
17+
end
18+
19+
def http_client
20+
@http_client ||= HTTP.follow(max_hops: 3).timeout(5)
1621
end
1722
end
1823
end

lib/freefeed/downloader.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
module Freefeed
2+
class Downloader
3+
attr_reader :url, :http_client
4+
5+
def initialize(url:, http_client:)
6+
@url = url
7+
@http_client = http_client
8+
end
9+
10+
def call
11+
# TBD: Honeybadger.context(downloader: {url: url})
12+
response = fetch_url
13+
return unless response&.status&.success?
14+
yield build_io_from(response), response.content_type.mime_type
15+
end
16+
17+
private
18+
19+
def build_io_from(response)
20+
StringIO.new.tap do |io|
21+
io.set_encoding(Encoding::BINARY)
22+
io.write(response.body.to_s)
23+
io.rewind
24+
end
25+
end
26+
27+
def fetch_url
28+
http_client.get(url)
29+
rescue StandardError
30+
# TBD: Report download error
31+
nil
32+
end
33+
end
34+
end

lib/freefeed/request.rb

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
module Freefeed
22
class Request
3-
DEFAULT_OPTIONS = {
4-
http_max_hops: 3,
5-
http_timeout_seconds: 5
6-
}.freeze
3+
attr_reader :client, :request_method, :path, :payload
74

8-
attr_reader :client, :request_method, :path, :options
9-
10-
def initialize(client:, request_method:, path:, options: {})
5+
def initialize(client:, request_method:, path:, payload: {})
116
@client = client
127
@request_method = request_method
138
@path = path
14-
@options = options
9+
@payload = payload
1510
end
1611

17-
def call
18-
response = http.headers(headers).public_send(request_method, uri, **request_params)
12+
def perform
13+
response = http_client.headers(headers).public_send(request_method, uri, **payload)
1914
ensure_successful_response(response)
2015
response
2116
end
@@ -25,16 +20,12 @@ def call
2520
def ensure_successful_response(response)
2621
error = Freefeed::Error.for(response)
2722
return unless error
28-
Honeybadger.context(failed_request_params: request_params)
23+
# TBD: Honeybadger.context(failed_request_payload: payload)
2924
raise(error)
3025
end
3126

3227
def uri
33-
@uri ||= URI.parse(client.base_url + path).to_s
34-
end
35-
36-
def request_params
37-
@request_params ||= options.slice(:json, :form, :params, :body)
28+
URI.parse(base_url + path).to_s
3829
end
3930

4031
def headers
@@ -44,12 +35,6 @@ def headers
4435
}
4536
end
4637

47-
def http
48-
HTTP.follow(max_hops: option(:http_max_hops)).timeout(option(:http_timeout_seconds))
49-
end
50-
51-
def option(option_name)
52-
options.fetch(option_name) { DEFAULT_OPTIONS.fetch(option_name) }
53-
end
38+
delegate :token, :base_url, :http_client, to: :client, private: true
5439
end
5540
end

lib/freefeed/utils.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
module Freefeed
22
module Utils
3-
def authenticated_request(request_method, path, params = {})
3+
def authenticated_request(request_method, path, payload = {})
44
AuthenticatedRequest.new(
55
client: self,
66
request_method: request_method,
77
path: path,
8-
options: params
9-
).call
8+
payload: payload
9+
).perform
1010
end
1111

12-
def request(request_method, path, params = {})
12+
def request(request_method, path, payload = {})
1313
Request.new(
1414
client: self,
1515
request_method: request_method,
1616
path: path,
17-
options: params
18-
).call
17+
payload: payload
18+
).perform
1919
end
2020
end
2121
end

lib/freefeed/v1/attachments.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,15 @@ module Attachments
44
# @param [String, Pathname, IO] source could by a file path
55
# or an IO object
66
def create_attachment(source, content_type: nil)
7-
options = {form: {file: file(source, content_type)}}
8-
authenticated_request(:post, "/v1/attachments", options)
7+
payload = {form: {file: file(source, content_type)}}
8+
authenticated_request(:post, "/v1/attachments", payload)
9+
end
10+
11+
def create_attachment_from(url:)
12+
::Freefeed::Downloader.new(url: url, http_client: http_client).call do |io, content_type|
13+
response = create_attachment(io, content_type: content_type)
14+
response.parse.fetch("attachments").fetch("id")
15+
end
916
end
1017

1118
private

lib/freefeed/v2/timelines.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ def likes_timeline(username, offset: 0)
3131
private
3232

3333
def request_timeline(path, offset)
34-
params = offset.positive? ? {json: {offset: offset}} : {}
35-
authenticated_request(:get, "/v2/timelines/#{path}", params)
34+
payload = offset.positive? ? {json: {offset: offset}} : {}
35+
authenticated_request(:get, "/v2/timelines/#{path}", payload)
3636
end
3737
end
3838
end
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
require "rails_helper"
2+
3+
RSpec.describe Freefeed::AuthenticatedRequest do
4+
let(:client) do
5+
Freefeed::Client.new(
6+
token: "TEST_TOKEN",
7+
base_url: "https://example.com",
8+
http_client: HTTP
9+
)
10+
end
11+
12+
describe "#perform" do
13+
context "when the response is successful" do
14+
it "makes a HTTP request without raising an error" do
15+
stub_request(:post, "https://example.com/test")
16+
.with(
17+
body: {payload: "data"}.to_json,
18+
headers: {
19+
"Authorization" => "Bearer TEST_TOKEN",
20+
"Content-Type" => "application/json; charset=utf-8",
21+
"User-Agent" => "feeder"
22+
}
23+
)
24+
.to_return(
25+
status: 200,
26+
headers: {"Content-Type" => "application/json"},
27+
body: {"response" => "payload"}.to_json
28+
)
29+
30+
request = described_class.new(
31+
client: client,
32+
request_method: :post,
33+
path: "/test",
34+
payload: {json: {"payload" => "data"}}
35+
)
36+
37+
actual = request.perform.parse
38+
expected = {"response" => "payload"}
39+
40+
assert_equal expected, actual
41+
end
42+
end
43+
end
44+
end

spec/lib/freefeed/downloader_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
require "rails_helper"

0 commit comments

Comments
 (0)