From 414743556f913ff5c55d556f6e5d94bc13ff4e08 Mon Sep 17 00:00:00 2001 From: Jack Wilson Date: Mon, 25 Apr 2016 20:16:37 -0700 Subject: [PATCH] Twitter retweet agent (#1181) * Agent to retweet all received tweet events * Cache twitter rest client * Update description, remove unused local * Makes context strings more readable, style consistency * to_h is not implemented in ruby 2.0 * In error case, include all agent_ids and event_ids * Adds capability to favorite tweets This restructures the Agent slightly to allow for retweeting and favoriting. It is possible to do both at the same time. - Renames the Agent from TwitterRetweetAgent to TwitterActionAgent. - Specs refactored --- app/concerns/twitter_concern.rb | 2 +- app/models/agents/twitter_action_agent.rb | 74 ++++++++ .../agents/twitter_action_agent_spec.rb | 158 ++++++++++++++++++ 3 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 app/models/agents/twitter_action_agent.rb create mode 100644 spec/models/agents/twitter_action_agent_spec.rb diff --git a/app/concerns/twitter_concern.rb b/app/concerns/twitter_concern.rb index 6fed3e0e..8427b38b 100644 --- a/app/concerns/twitter_concern.rb +++ b/app/concerns/twitter_concern.rb @@ -36,7 +36,7 @@ module TwitterConcern end def twitter - Twitter::REST::Client.new do |config| + @twitter ||= Twitter::REST::Client.new do |config| config.consumer_key = twitter_consumer_key config.consumer_secret = twitter_consumer_secret config.access_token = twitter_oauth_token diff --git a/app/models/agents/twitter_action_agent.rb b/app/models/agents/twitter_action_agent.rb new file mode 100644 index 00000000..c56ff95f --- /dev/null +++ b/app/models/agents/twitter_action_agent.rb @@ -0,0 +1,74 @@ +module Agents + class TwitterActionAgent < Agent + include TwitterConcern + + cannot_be_scheduled! + + description <<-MD + The Twitter Action Agent is able to retweet or favorite tweets from the events it receives. + + #{ twitter_dependencies_missing if dependencies_missing? } + + It expects to consume events generated by twitter agents where the payload is a hash of tweet information. The existing TwitterStreamAgent is one example of a valid event producer for this Agent. + + To be able to use this Agent you need to authenticate with Twitter in the [Services](/services) section first. + + Set `expected_receive_period_in_days` to the maximum amount of time that you'd expect to pass between Events being received by this Agent. + Set `retweet` to either true or false. + Set `favorite` to either true or false. + MD + + def validate_options + unless options['expected_receive_period_in_days'].present? + errors.add(:base, "expected_receive_period_in_days is required") + end + unless retweet? || favorite? + errors.add(:base, "at least one action must be true") + end + end + + def working? + last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? + end + + def default_options + { + 'expected_receive_period_in_days' => '2', + 'favorite' => 'false', + 'retweet' => 'true', + } + end + + def retweet? + boolify(options['retweet']) + end + + def favorite? + boolify(options['favorite']) + end + + def receive(incoming_events) + tweets = tweets_from_events(incoming_events) + + begin + twitter.favorite(tweets) if favorite? + twitter.retweet(tweets) if retweet? + rescue Twitter::Error => e + create_event :payload => { + 'success' => false, + 'error' => e.message, + 'tweets' => Hash[tweets.map { |t| [t.id, t.text] }], + 'agent_ids' => incoming_events.map(&:agent_id), + 'event_ids' => incoming_events.map(&:id) + } + end + end + + def tweets_from_events(events) + events.map do |e| + Twitter::Tweet.new(id: e.payload["id"], text: e.payload["text"]) + end + end + end +end + diff --git a/spec/models/agents/twitter_action_agent_spec.rb b/spec/models/agents/twitter_action_agent_spec.rb new file mode 100644 index 00000000..fd29ef15 --- /dev/null +++ b/spec/models/agents/twitter_action_agent_spec.rb @@ -0,0 +1,158 @@ +require 'rails_helper' + +describe Agents::TwitterActionAgent do + describe '#receive' do + before do + @event1 = Event.new + @event1.agent = agents(:bob_twitter_user_agent) + @event1.payload = { id: 123, text: 'So awesome.. gotta retweet' } + @event1.save! + @tweet1 = Twitter::Tweet.new( + id: @event1.payload[:id], + text: @event1.payload[:text] + ) + + @event2 = Event.new + @event2.agent = agents(:bob_twitter_user_agent) + @event2.payload = { id: 456, text: 'Something Justin Bieber said' } + @event2.save! + @tweet2 = Twitter::Tweet.new( + id: @event2.payload[:id], + text: @event2.payload[:text] + ) + end + + context 'when set up to retweet' do + before do + @agent = build_agent({ + 'expected_receive_period_in_days' => '2', + 'favorite' => 'false', + 'retweet' => 'true', + }) + @agent.save! + end + + context 'when the twitter client succeeds retweeting' do + it 'should retweet the tweets from the payload' do + mock(@agent.twitter).retweet([@tweet1, @tweet2]) + @agent.receive([@event1, @event2]) + end + end + + context 'when the twitter client fails retweeting' do + it 'creates an event with tweet info and the error message' do + stub(@agent.twitter).retweet(anything) { + raise Twitter::Error.new('uh oh') + } + + @agent.receive([@event1, @event2]) + + failure_event = @agent.events.last + expect(failure_event.payload[:error]).to eq('uh oh') + expect(failure_event.payload[:tweets]).to eq( + { + @event1.payload[:id].to_s => @event1.payload[:text], + @event2.payload[:id].to_s => @event2.payload[:text] + } + ) + expect(failure_event.payload[:agent_ids]).to match_array( + [@event1.agent_id, @event2.agent_id] + ) + expect(failure_event.payload[:event_ids]).to match_array( + [@event2.id, @event1.id] + ) + end + end + end + + context 'when set up to favorite' do + before do + @agent = build_agent( + 'expected_receive_period_in_days' => '2', + 'favorite' => 'true', + 'retweet' => 'false', + ) + @agent.save! + end + + context 'when the twitter client succeeds favoriting' do + it 'should favorite the tweets from the payload' do + mock(@agent.twitter).favorite([@tweet1, @tweet2]) + @agent.receive([@event1, @event2]) + end + end + + context 'when the twitter client fails retweeting' do + it 'creates an event with tweet info and the error message' do + stub(@agent.twitter).favorite(anything) { + raise Twitter::Error.new('uh oh') + } + + @agent.receive([@event1, @event2]) + + failure_event = @agent.events.last + expect(failure_event.payload[:error]).to eq('uh oh') + expect(failure_event.payload[:tweets]).to eq( + { + @event1.payload[:id].to_s => @event1.payload[:text], + @event2.payload[:id].to_s => @event2.payload[:text] + } + ) + expect(failure_event.payload[:agent_ids]).to match_array( + [@event1.agent_id, @event2.agent_id] + ) + expect(failure_event.payload[:event_ids]).to match_array( + [@event2.id, @event1.id] + ) + end + end + end + end + + describe "#validate_options" do + context 'when set up to neither favorite or retweet' do + it 'is invalid' do + agent = build_agent( + 'expected_receive_period_in_days' => '2', + 'favorite' => 'false', + 'retweet' => 'false', + ) + + expect(agent).not_to be_valid + end + end + end + + describe '#working?' do + before do + stub.any_instance_of(Twitter::REST::Client).retweet(anything) + end + + it 'checks if events have been received within the expected time period' do + agent = build_agent( + 'expected_receive_period_in_days' => '2', + 'favorite' => 'false', + 'retweet' => 'true', + ) + agent.save! + + expect(agent).not_to be_working # No events received + + described_class.async_receive(agent.id, [events(:bob_website_agent_event)]) + expect(agent.reload).to be_working # Just received events + + two_days_from_now = 2.days.from_now + stub(Time).now { two_days_from_now } + expect(agent.reload).not_to be_working # Too much time has passed + end + end + + def build_agent(options) + described_class.new do |agent| + agent.name = 'twitter stuff' + agent.options = options + agent.service = services(:generic) + agent.user = users(:bob) + end + end +end