diff --git a/Envfile b/Envfile index 0d8bfa24b6faa..32391509000d8 100644 --- a/Envfile +++ b/Envfile @@ -33,6 +33,9 @@ variable :REDIS_SIDEKIQ_URL, :String, default: "redis://localhost:6379" # Elasticsearch variable :ELASTICSEARCH_URL, :String, default: "http://localhost:9200" +# env +variable :MEDIUM_DOMIAN, :String, default: "https://medium.com/" + ################################################ ############## 3rd Party Services ############## ################################################ @@ -182,4 +185,5 @@ group :production do # Heroku variable :HEROKU_APP_URL, :String # practicaldev.herokuapp.com + variable :MEDIUM_DOMIAN, :String, default: "https://medium.com/" end diff --git a/Gemfile b/Gemfile index cbf0a9a9b858b..8c949afaabb20 100644 --- a/Gemfile +++ b/Gemfile @@ -64,6 +64,7 @@ gem "oj", "~> 3.10" # JSON parser and object serializer gem "omniauth", "~> 1.9" # A generalized Rack framework for multiple-provider authentication gem "omniauth-github", "~> 1.3" # OmniAuth strategy for GitHub gem "omniauth-twitter", "~> 1.4" # OmniAuth strategy for Twitter +gem "opengraph_parser" gem "pg", "~> 1.2" # Pg is the Ruby interface to the PostgreSQL RDBMS gem "puma", "~> 4.3" # Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server gem "pundit", "~> 2.1" # Object oriented authorization for Rails applications diff --git a/Gemfile.lock b/Gemfile.lock index 2ec15b6efd5a6..97e569efd2a31 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -512,6 +512,9 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack + opengraph_parser (0.2.3) + addressable + nokogiri orm_adapter (0.5.0) os (1.0.1) parallel (1.19.1) @@ -921,6 +924,7 @@ DEPENDENCIES omniauth (~> 1.9) omniauth-github (~> 1.3) omniauth-twitter (~> 1.4) + opengraph_parser parallel_tests (~> 2.31) pg (~> 1.2) pry (~> 0.12) diff --git a/app/models/message.rb b/app/models/message.rb index 4aeb6f1f9014a..4da96c89a7065 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -118,8 +118,11 @@ def append_rich_links(html) #{user.name} ".html_safe + elsif (medium = PreviewMediumService.call(anchor)) + html += medium.rich_link.html_safe end end + html end diff --git a/app/services/preview_medium_service.rb b/app/services/preview_medium_service.rb new file mode 100644 index 0000000000000..0f3804002ed06 --- /dev/null +++ b/app/services/preview_medium_service.rb @@ -0,0 +1,51 @@ +class PreviewMediumService + def initialize(link) + @href = link["href"] + end + + def self.call(link) + new(link).call + end + + def call + return unless valid_url? + + response = send_request + return unless response + + @medium = create_attributes(response) + self + end + + def rich_link + return "" unless valid_url? + + " + #{"
" if medium.image_url.present?} +

#{medium.title}

+

#{medium.description}

" + end + + private + + attr_reader :href, :medium + + def valid_url? + href.include?(ApplicationConfig["MEDIUM_DOMIAN"]) && href.split("/")[4].present? + end + + def send_request + OpenGraph.new(href) + end + + def create_attributes(response) + OpenStruct.new( + url: response.url, + image_url: response.images[0], + title: response.title, + description: response.description, + ) + end +end diff --git a/spec/models/message_spec.rb b/spec/models/message_spec.rb index 94f89f5c05a7d..56d62e77bea16 100644 --- a/spec/models/message_spec.rb +++ b/spec/models/message_spec.rb @@ -7,6 +7,7 @@ let(:chat_channel) { create(:chat_channel) } let(:message) { create(:message, user: user) } let(:random_word) { Faker::Lorem.word } + let(:medium_url) { "https://medium.com/@jpsmithalt/hold-the-line-17231c48ff17" } describe "validations" do context "with automatic validations" do @@ -73,6 +74,23 @@ expect(message.message_html).not_to include("data-content") end + it "creates rich link with non-rich link for medium" do + stub_request(:get, medium_url). + with( + headers: { + "Accept" => "*/*", + "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", + "User-Agent" => "Ruby" + }, + ). + to_return(status: 200, body: "medium article", headers: {}) + + message.message_markdown = medium_url + message.validate! + + expect(message.message_html).to include("data-content") + end + it "creates mention if user exists" do message.message_markdown = "Hello @#{user.username}" message.validate! diff --git a/spec/services/preview_medium_spec_service_spec.rb b/spec/services/preview_medium_spec_service_spec.rb new file mode 100644 index 0000000000000..d2753a9d0564c --- /dev/null +++ b/spec/services/preview_medium_spec_service_spec.rb @@ -0,0 +1,82 @@ +require "rails_helper" + +RSpec.describe PreviewMediumService, type: :service do + describe "service methods" do + let(:valid_medium_url) { "https://medium.com/@sfchronicle/why-mark-zuckerberg-should-step-down-as-facebook-ceo-795410ef12eb" } + let(:invalid_medium_url) { "https://dev.to/nas5w/first-class-functions-in-javascript-5dj2" } + let(:object) { described_class.call("href" => valid_medium_url) } + + before do + stub_request(:get, valid_medium_url). + with( + headers: { + "Accept" => "*/*", + "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", + "User-Agent" => "Ruby" + }, + ). + to_return(status: 200, body: "medium", headers: {}) + end + + describe "class_methods" do + it ".call" do + expect(object).to be_a described_class + end + end + + describe "instance_methods" do + context "with #call" do + it "return nil without valid_url" do + allow(object).to receive(:valid_url?).and_return(false) + expect(object.call).to be_nil + end + + it "return nil with invalid response" do + allow(object).to receive(:send_request).and_return(nil) + expect(object.call).to be_nil + end + + it "return object with sucess response" do + expect(object.call).to be_a described_class + end + end + + context "with #valid_url" do + it "with valid url" do + expect(object.send(:valid_url?)).to be_truthy + end + + it "with invalid url" do + allow(object).to receive(:href).and_return(invalid_medium_url) + expect(object.send(:valid_url?)).to be_falsy + end + end + + context "with #rich_link" do + it "return with invalid_url" do + allow(object).to receive(:valid_url?).and_return(false) + expect(object.rich_link).to eq("") + end + + it "return with valid_url" do + expect(object.rich_link).to include("sidecar-medium") + expect(object.rich_link).to include("chatchannels__richlink") + end + end + + it "#create_attributes" do + response = OpenStruct.new( + url: valid_medium_url, + images: ["https://miro.medium.com/max/920/0*x6DKkfEFloE-Lgiw.jpg"], + title: "Why Mark Zuckerberg Should Step Down as Facebook CEO", + description: "A shift in the role of CEO Mark Zuckerberg might help address", + ) + res = object.send(:create_attributes, response) + expect(res.url).to eq(response.url) + expect(res.image_url).to eq(response.images[0]) + expect(res.title).to eq(response.title) + expect(res.description).to eq(response.description) + end + end + end +end