From 24ffbca370f6957bc9a6c43cb6a1ee55cade7bb8 Mon Sep 17 00:00:00 2001 From: Erik Michaels-Ober Date: Sun, 3 Jun 2012 03:52:12 -0400 Subject: [PATCH] Changes to `Twitter::Client#follow` and `Twitter::Client#unfollow` These methods now accept multiple users as arguments and return an array instead of a `Twitter::User`. Additionally, the `Twitter::Client#follow` now checks to make sure the user isn't already being followed. If you don't wish to perform that check (which requires an extra HTTP request), you can use the new `Twitter::Client#follow!` method instead. Closes sferik/t#54. --- README.md | 8 ++ lib/twitter/client.rb | 53 +++++-- lib/twitter/connection.rb | 2 +- lib/twitter/core_ext/enumerable.rb | 11 ++ lib/twitter/core_ext/hash.rb | 8 ++ lib/twitter/response/parse_json.rb | 20 +-- spec/fixtures/id_list.json | 2 +- .../client/friends_and_followers_spec.rb | 130 ++++++++++++++++-- 8 files changed, 196 insertions(+), 38 deletions(-) create mode 100644 lib/twitter/core_ext/enumerable.rb diff --git a/README.md b/README.md index 9462147e5..24796c28b 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,14 @@ wiki][apps]! The Active Support dependency has been removed! +The `Twitter::Client#follow` and `Twitter::Client#unfollow` methods now accept +multiple users as arguments and return an array instead of a `Twitter::User`. +Additionally, the `Twitter::Client#follow` method now checks to make sure the +user isn't already being followed. If you don't wish to perform that check +(which requires an extra HTTP request), you can use the new +`Twitter::Client#follow!` method instead. **Note**: This may re-send an email +notification to the user, even if they are already being followed. + This version introduces an identity map, which ensures that the same objects only get initialized once: diff --git a/lib/twitter/client.rb b/lib/twitter/client.rb index d7f0ac8c8..d0a94536b 100644 --- a/lib/twitter/client.rb +++ b/lib/twitter/client.rb @@ -3,6 +3,7 @@ require 'twitter/config' require 'twitter/configuration' require 'twitter/connection' +require 'twitter/core_ext/enumerable' require 'twitter/core_ext/hash' require 'twitter/cursor' require 'twitter/direct_message' @@ -655,7 +656,7 @@ def friendship(source, target, options={}) alias :friendship_show :friendship alias :relationship :friendship - # Allows the authenticating user to follow the specified user + # Allows the authenticating user to follow the specified users, unless they are already followed # # @see https://dev.twitter.com/docs/api/1/post/friendships/create # @rate_limited No @@ -667,31 +668,61 @@ def friendship(source, target, options={}) # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid. # @example Follow @sferik # Twitter.follow('sferik') - def follow(user, options={}) - options.merge_user!(user) + def follow(*args) + options = args.last.is_a?(Hash) ? args.pop : {} # Twitter always turns on notifications if the "follow" option is present, even if it's set to false # so only send follow if it's true options.merge!(:follow => true) if options.delete(:follow) - user = post("/1/friendships/create.json", options) - Twitter::User.new(user) + friend_ids = self.friend_ids.ids + user_ids = self.users(args).map(&:id) + (user_ids - friend_ids).threaded_map do |user| + user = post("/1/friendships/create.json", options.merge_user(user)) + Twitter::User.new(user) + end end alias :friendship_create :follow - # Allows the authenticating user to unfollow the specified user + # Allows the authenticating user to follow the specified users + # + # @see https://dev.twitter.com/docs/api/1/post/friendships/create + # @rate_limited No + # @requires_authentication Yes + # @param user [Integer, String, Twitter::User] A Twitter user ID, screen name, or object. + # @param options [Hash] A customizable set of options. + # @option options [Boolean] :follow (false) Enable notifications for the target user. + # @return [Array] The followed users. + # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid. + # @example Follow @sferik + # Twitter.follow!('sferik') + def follow!(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + # Twitter always turns on notifications if the "follow" option is present, even if it's set to false + # so only send follow if it's true + options.merge!(:follow => true) if options.delete(:follow) + args.threaded_map do |user| + user = post("/1/friendships/create.json", options.merge_user(user)) + Twitter::User.new(user) + end + end + alias :friendship_create! :follow! + + # Allows the authenticating user to unfollow the specified users # # @see https://dev.twitter.com/docs/api/1/post/friendships/destroy # @rate_limited No # @requires_authentication Yes # @param user [Integer, String, Twitter::User] A Twitter user ID, screen name, or object. # @param options [Hash] A customizable set of options. - # @return [Twitter::User] The unfollowed user. + # @return [Array] The unfollowed users. # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid. # @example Unfollow @sferik # Twitter.unfollow('sferik') - def unfollow(user, options={}) - options.merge_user!(user) - user = delete("/1/friendships/destroy.json", options) - Twitter::User.new(user) + def unfollow(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + args.threaded_map do |user| + user = delete("/1/friendships/destroy.json", options.merge_user(user)) + Twitter::User.new(user) + end end alias :friendship_destroy :unfollow diff --git a/lib/twitter/connection.rb b/lib/twitter/connection.rb index 43a64ba9f..1e1417eb4 100644 --- a/lib/twitter/connection.rb +++ b/lib/twitter/connection.rb @@ -25,7 +25,7 @@ def connection(options={}) :ssl => {:verify => false}, :url => options.fetch(:endpoint, endpoint), } - @connection ||=Faraday.new(default_options.deep_merge(connection_options)) do |builder| + @connection ||= Faraday.new(default_options.deep_merge(connection_options)) do |builder| builder.use Twitter::Request::MultipartWithFile builder.use Twitter::Request::TwitterOAuth, credentials if credentials? builder.use Faraday::Request::Multipart diff --git a/lib/twitter/core_ext/enumerable.rb b/lib/twitter/core_ext/enumerable.rb new file mode 100644 index 000000000..e3572549e --- /dev/null +++ b/lib/twitter/core_ext/enumerable.rb @@ -0,0 +1,11 @@ +module Enumerable + + def threaded_map + threads = [] + each do |object| + threads << Thread.new{yield object} + end + threads.map(&:value) + end + +end diff --git a/lib/twitter/core_ext/hash.rb b/lib/twitter/core_ext/hash.rb index 4e0f1f7b9..6b2f32f2e 100644 --- a/lib/twitter/core_ext/hash.rb +++ b/lib/twitter/core_ext/hash.rb @@ -64,6 +64,14 @@ def merge_owner!(user) self end + # Take a user and merge it into the hash with the correct key + # + # @param user[Integer, String, Twitter::User] A Twitter user ID, screen_name, or object. + # @return [Hash] + def merge_user(user, prefix=nil, suffix=nil) + self.dup.merge_user!(user, prefix, suffix) + end + # Take a user and merge it into the hash with the correct key # # @param user[Integer, String, Twitter::User] A Twitter user ID, screen_name, or object. diff --git a/lib/twitter/response/parse_json.rb b/lib/twitter/response/parse_json.rb index b55e66e09..2dfe75869 100644 --- a/lib/twitter/response/parse_json.rb +++ b/lib/twitter/response/parse_json.rb @@ -6,16 +6,16 @@ module Response class ParseJson < Faraday::Response::Middleware def parse(body) - case body - when '' - nil - when 'true' - true - when 'false' - false - else - MultiJson.load(body) - end + case body + when '' + nil + when 'true' + true + when 'false' + false + else + MultiJson.load(body) + end end def on_complete(env) diff --git a/spec/fixtures/id_list.json b/spec/fixtures/id_list.json index 5e50f51ad..a326ccacc 100644 --- a/spec/fixtures/id_list.json +++ b/spec/fixtures/id_list.json @@ -1 +1 @@ -{"previous_cursor_str":"0","next_cursor":0,"ids":[146197851,145833898,90678091,63267186,142867146,18443187,84722702,9607792,18589926,17831966,103213461,6734842,45322951,14517078,70353603,133143291,54368334,127522778,128046728,95206919,128441606,91078264,126052250,8285392,16314440],"previous_cursor":0,"next_cursor_str":"0"} \ No newline at end of file +{"previous_cursor_str":"0","next_cursor":0,"ids":[146197851,145833898,90678091,63267186,142867146,18443187,84722702,9607792,18589926,17831966,103213461,6734842,45322951,14517078,70353603,133143291,54368334,127522778,128046728,95206919,128441606,91078264,126052250,14100886,16314440],"previous_cursor":0,"next_cursor_str":"0"} \ No newline at end of file diff --git a/spec/twitter/client/friends_and_followers_spec.rb b/spec/twitter/client/friends_and_followers_spec.rb index a2446264a..194fb48e3 100644 --- a/spec/twitter/client/friends_and_followers_spec.rb +++ b/spec/twitter/client/friends_and_followers_spec.rb @@ -280,6 +280,102 @@ end describe "#follow" do + context "with :follow => true passed" do + before do + stub_get("/1/friends/ids.json"). + with(:query => {:cursor => "-1"}). + to_return(:body => fixture("id_list.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + stub_get("/1/users/lookup.json"). + with(:query => {:screen_name => "sferik,pengwynn"}). + to_return(:body => fixture("friendships.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + stub_post("/1/friendships/create.json"). + with(:body => {:user_id => "7505382", :follow => "true"}). + to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + end + it "should request the correct resource" do + @client.follow("sferik", "pengwynn", :follow => true) + a_get("/1/friends/ids.json"). + with(:query => {:cursor => "-1"}). + should have_been_made + a_get("/1/users/lookup.json"). + with(:query => {:screen_name => "sferik,pengwynn"}). + should have_been_made + a_post("/1/friendships/create.json"). + with(:body => {:user_id => "7505382", :follow => "true"}). + should have_been_made + end + it "should return the befriended user" do + users = @client.follow("sferik", "pengwynn", :follow => true) + users.should be_an Array + users.first.should be_a Twitter::User + users.first.name.should == "Erik Michaels-Ober" + end + end + context "with :follow => false passed" do + before do + stub_get("/1/friends/ids.json"). + with(:query => {:cursor => "-1"}). + to_return(:body => fixture("id_list.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + stub_get("/1/users/lookup.json"). + with(:query => {:screen_name => "sferik,pengwynn"}). + to_return(:body => fixture("friendships.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + stub_post("/1/friendships/create.json"). + with(:body => {:user_id => "7505382"}). + to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + end + it "should request the correct resource" do + @client.follow("sferik", "pengwynn", :follow => false) + a_get("/1/friends/ids.json"). + with(:query => {:cursor => "-1"}). + should have_been_made + a_get("/1/users/lookup.json"). + with(:query => {:screen_name => "sferik,pengwynn"}). + should have_been_made + a_post("/1/friendships/create.json"). + with(:body => {:user_id => "7505382"}). + should have_been_made + end + it "should return the befriended user" do + users = @client.follow("sferik", "pengwynn", :follow => false) + users.should be_an Array + users.first.should be_a Twitter::User + users.first.name.should == "Erik Michaels-Ober" + end + end + context "without :follow passed" do + before do + stub_get("/1/friends/ids.json"). + with(:query => {:cursor => "-1"}). + to_return(:body => fixture("id_list.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + stub_get("/1/users/lookup.json"). + with(:query => {:screen_name => "sferik,pengwynn"}). + to_return(:body => fixture("friendships.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + stub_post("/1/friendships/create.json"). + with(:body => {:user_id => "7505382"}). + to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + end + it "should request the correct resource" do + @client.follow("sferik", "pengwynn") + a_get("/1/friends/ids.json"). + with(:query => {:cursor => "-1"}). + should have_been_made + a_get("/1/users/lookup.json"). + with(:query => {:screen_name => "sferik,pengwynn"}). + should have_been_made + a_post("/1/friendships/create.json"). + with(:body => {:user_id => "7505382"}). + should have_been_made + end + it "should return the befriended user" do + users = @client.follow("sferik", "pengwynn") + users.should be_an Array + users.first.should be_a Twitter::User + users.first.name.should == "Erik Michaels-Ober" + end + end + end + + describe "#follow!" do context "with :follow => true passed" do before do stub_post("/1/friendships/create.json"). @@ -287,15 +383,16 @@ to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"}) end it "should request the correct resource" do - @client.follow("sferik", :follow => true) + @client.follow!("sferik", :follow => true) a_post("/1/friendships/create.json"). with(:body => {:screen_name => "sferik", :follow => "true"}). should have_been_made end it "should return the befriended user" do - user = @client.follow("sferik", :follow => true) - user.should be_a Twitter::User - user.name.should == "Erik Michaels-Ober" + users = @client.follow!("sferik", :follow => true) + users.should be_an Array + users.first.should be_a Twitter::User + users.first.name.should == "Erik Michaels-Ober" end end context "with :follow => false passed" do @@ -305,15 +402,16 @@ to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"}) end it "should request the correct resource" do - @client.follow("sferik", :follow => false) + @client.follow!("sferik", :follow => false) a_post("/1/friendships/create.json"). with(:body => {:screen_name => "sferik"}). should have_been_made end it "should return the befriended user" do - user = @client.follow("sferik", :follow => false) - user.should be_a Twitter::User - user.name.should == "Erik Michaels-Ober" + users = @client.follow!("sferik", :follow => false) + users.should be_an Array + users.first.should be_a Twitter::User + users.first.name.should == "Erik Michaels-Ober" end end context "without :follow passed" do @@ -323,15 +421,16 @@ to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"}) end it "should request the correct resource" do - @client.follow("sferik") + @client.follow!("sferik") a_post("/1/friendships/create.json"). with(:body => {:screen_name => "sferik"}). should have_been_made end it "should return the befriended user" do - user = @client.follow("sferik") - user.should be_a Twitter::User - user.name.should == "Erik Michaels-Ober" + users = @client.follow!("sferik") + users.should be_an Array + users.first.should be_a Twitter::User + users.first.name.should == "Erik Michaels-Ober" end end end @@ -349,9 +448,10 @@ should have_been_made end it "should return the unfollowed" do - user = @client.friendship_destroy("sferik") - user.should be_a Twitter::User - user.name.should == "Erik Michaels-Ober" + users = @client.friendship_destroy("sferik") + users.should be_an Array + users.first.should be_a Twitter::User + users.first.name.should == "Erik Michaels-Ober" end end