diff --git a/.docker/ruby/Dockerfile b/.docker/ruby/Dockerfile index 75c43f1f..af3a8643 100644 --- a/.docker/ruby/Dockerfile +++ b/.docker/ruby/Dockerfile @@ -12,9 +12,10 @@ RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - RUN apt-get update -qq \ && apt-get install -y --no-install-recommends build-essential \ - libpq-dev postgresql-client \ + libpq-dev postgresql-client \ libxml2-dev libxslt1-dev \ libmagickwand-dev imagemagick \ + libidn11-dev \ nodejs \ yarn \ && rm -rf /var/lib/apt/lists/* diff --git a/.github/workflows/retrospring.yml b/.github/workflows/retrospring.yml index b3c05641..7fe00b56 100644 --- a/.github/workflows/retrospring.yml +++ b/.github/workflows/retrospring.yml @@ -52,7 +52,7 @@ jobs: with: node-version: '12' - name: Install dependencies - run: sudo apt update && sudo apt-get install -y libpq-dev libxml2-dev libxslt1-dev libmagickwand-dev imagemagick + run: sudo apt update && sudo apt-get install -y libpq-dev libxml2-dev libxslt1-dev libmagickwand-dev imagemagick libidn11-dev - name: Copy default configuration run: | cp config/database.yml.postgres config/database.yml diff --git a/Gemfile b/Gemfile index 38b52e87..f4ac386d 100644 --- a/Gemfile +++ b/Gemfile @@ -57,6 +57,7 @@ gem 'omniauth-tumblr' # OAuth clients gem 'twitter' +gem 'twitter-text' # To use a more recent Faraday version, a fork of this gem is required. gem 'tumblr_client', git: 'https://github.com/amplifr/tumblr_client' diff --git a/Gemfile.lock b/Gemfile.lock index b75205ba..5e9bf9a5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -251,6 +251,7 @@ GEM concurrent-ruby (~> 1.0) i18n-js (3.6.0) i18n (>= 0.6.6) + idn-ruby (0.1.4) image_processing (1.12.1) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) @@ -534,6 +535,9 @@ GEM multipart-post (~> 2.0) naught (~> 1.0) simple_oauth (~> 0.3.0) + twitter-text (3.1.0) + idn-ruby + unf (~> 0.1.0) tzinfo (1.2.9) thread_safe (~> 0.1) uglifier (4.2.0) @@ -632,6 +636,7 @@ DEPENDENCIES tumblr_client! turbolinks (~> 2.5.3) twitter + twitter-text uglifier (>= 1.3.0) web-console (< 4.0.0) webpacker (~> 5.2) diff --git a/app/controllers/ajax/answer_controller.rb b/app/controllers/ajax/answer_controller.rb index 37bc1ec0..7c5b48ea 100644 --- a/app/controllers/ajax/answer_controller.rb +++ b/app/controllers/ajax/answer_controller.rb @@ -48,7 +48,9 @@ class Ajax::AnswerController < AjaxController end services = JSON.parse params[:share] - ShareWorker.perform_async(current_user.id, answer.id, services) + services.each do |service| + ShareWorker.perform_async(current_user.id, answer.id, service) + end @response[:status] = :okay diff --git a/app/models/services/twitter.rb b/app/models/services/twitter.rb index 0726fe97..0e090bcd 100644 --- a/app/models/services/twitter.rb +++ b/app/models/services/twitter.rb @@ -27,16 +27,30 @@ class Services::Twitter < Service end def prepare_tweet(answer) - # TODO: improve this. question_content = twitter_markdown answer.question.content.gsub(/\@(\w+)/, '\1') + original_question_length = question_content.length answer_content = twitter_markdown answer.content + original_answer_length = answer_content.length answer_url = show_user_answer_url( id: answer.id, username: answer.user.screen_name, host: APP_CONFIG['hostname'], protocol: (APP_CONFIG['https'] ? :https : :http) ) - "#{question_content[0..122]}#{'…' if question_content.length > 123}" \ - " — #{answer_content[0..123]}#{'…' if answer_content.length > 124} #{answer_url}" + + parsed_tweet = { :valid => false } + tweet_text = "" + + until parsed_tweet[:valid] + tweet_text = "#{question_content[0..122]}#{'…' if original_question_length > [123, question_content.length].min}" \ + " — #{answer_content[0..123]}#{'…' if original_answer_length > [124, answer_content.length].min} #{answer_url}" + + parsed_tweet = Twitter::TwitterText::Validation::parse_tweet(tweet_text) + + question_content = question_content[0..-2] + answer_content = answer_content[0..-2] + end + + tweet_text end end diff --git a/app/workers/share_worker.rb b/app/workers/share_worker.rb index fc4eb5b9..d0964b3c 100644 --- a/app/workers/share_worker.rb +++ b/app/workers/share_worker.rb @@ -1,19 +1,19 @@ class ShareWorker include Sidekiq::Worker - sidekiq_options queue: :share, retry: false + sidekiq_options queue: :share, retry: 5 # @param user_id [Integer] the user id # @param answer_id [Integer] the user id - # @param services [Array] array containing strings - def perform(user_id, answer_id, services) - User.find(user_id).services.each do |service| - begin - service.post(Answer.find(answer_id)) if services.include? service.provider - rescue => e - logger.info "failed to post answer #{answer_id} to #{service.provider} for user #{user_id}: #{e.message}" - NewRelic::Agent.notice_error(e) - end - end + # @param service [String] the service to post to + def perform(user_id, answer_id, service) + service_type = "Services::#{service.camelize}" + user_service = User.find(user_id).services.find_by(type: service_type) + + user_service.post(Answer.find(answer_id)) + rescue => e + logger.info "failed to post answer #{answer_id} to #{service.provider} for user #{user_id}: #{e.message}" + NewRelic::Agent.notice_error(e) + raise end end diff --git a/spec/controllers/ajax/answer_controller_spec.rb b/spec/controllers/ajax/answer_controller_spec.rb index b234a41b..c73e901f 100644 --- a/spec/controllers/ajax/answer_controller_spec.rb +++ b/spec/controllers/ajax/answer_controller_spec.rb @@ -27,7 +27,9 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do it "enqueues a job for sharing the answer to social networks" do subject - expect(ShareWorker).to have_enqueued_sidekiq_job(user.id, Answer.last.id, shared_services) + shared_services.each do |service| + expect(ShareWorker).to have_enqueued_sidekiq_job(user.id, Answer.last.id, service) + end end include_examples "returns the expected response" diff --git a/spec/models/services/twitter_spec.rb b/spec/models/services/twitter_spec.rb new file mode 100644 index 00000000..a177993d --- /dev/null +++ b/spec/models/services/twitter_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Services::Twitter do + describe "#post" do + let(:user) { FactoryBot.create(:user) } + let(:service) { Services::Twitter.create(user: user) } + let(:answer) { FactoryBot.create(:answer, user: user, + content: 'a' * 255, + question_content: 'q' * 255) } + let(:twitter_client) { instance_double(Twitter::REST::Client) } + + before do + allow(Twitter::REST::Client).to receive(:new).and_return(twitter_client) + allow(twitter_client).to receive(:update!) + stub_const("APP_CONFIG", { + 'hostname' => 'example.com', + 'https' => true, + 'items_per_page' => 5, + 'sharing' => { + 'twitter' => { + 'consumer_key' => 'AAA', + } + } + }) + end + + it "posts a shortened tweet" do + service.post(answer) + + expect(twitter_client).to have_received(:update!).with("#{'q' * 123}… — #{'a' * 124}… https://example.com/#{user.screen_name}/a/#{answer.id}") + end + + it "posts an un-shortened tweet" do + answer.question.content = 'Why are raccoons so good?' + answer.question.save! + answer.content = 'Because they are good cunes.' + answer.save! + + service.post(answer) + + expect(twitter_client).to have_received(:update!).with("#{answer.question.content} — #{answer.content} https://example.com/#{user.screen_name}/a/#{answer.id}") + end + end +end \ No newline at end of file