Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Userstream support #40

Merged
merged 10 commits into from

4 participants

@coreyhaines

I added rudimentary twitter userstream support for Client, including a proc for separately receiving direct messages.
I also added a way to receive the raw hashes that come from the streaming api.
I also added support for providing an #on_timeline_status for consistency with the other client methods. The existing way of passing a block for regular timeline statuses is still supported.
Cleaned up some code
Updated README

@chad

Rad!

@coreyhaines

I made a screencast to show the api
http://screencast.com/t/DAywY28Ln

@chad

Also rad!!!!

README.md
((4 lines not shown))
+Using the Twitter Userstream
+----------------------------
+
+Using the Twitter userstream works similarly to the regular usage, except you use the userstream method.
+
+ # Use 'userstream' to get message from your stream
+ TweetStream::Client.new.userstream do |status|
+ puts "#{status.text}"
+ end
+
+You also can listen to direct messages by supplying an on_direct_message proc
+
+ client = TweetStream::Client.new
+ client.on_direct_message do |direct_message|
+ puts "#{direct_message.text}"
+ end

#userstream and #on_direct_message sound very different, but behave similarly. The first one sounds like it returns the stream (perhaps as a static array or as an IO reader), whereas the second sounds like you're registering a callback for an event handler. Either could be a reasonable way of describing what's happening (I prefer the event handler style), but above all, it would be good to be consistent.

That's a good point. #userstream behaves like the other twitter streaming api calls. You also are given an on_timeline_status hook method. I'll update the README to make it more clear.

Thanks for the feedback.

Updated the README

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@coreyhaines

Have pushed an update to the README to be more clear about using method hooks to receive the statuses. Thanks for the feedback @jamesarosen

@jamesarosen jamesarosen commented on the diff
spec/tweetstream/direct_message_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe TweetStream::DirectMessage do
+ it 'modifies the :sender key into a TweetStream::User object called #user' do
+ @status = TweetStream::DirectMessage.new({:sender => {:screen_name => 'bob'}})
+ @status.user.is_a?(TweetStream::User).should be_true

I usually see this assertion written as @status.user.should be_kind_of(TweetStream::User) or ... be_a(TweetStream::User). Is there a reason you chose to write it the way you did? Do you find the magic of be_foo hampers spec maintainability?

This was copied down from the other spec, so I was keeping it consistent with their style.
Although, I did break convention by not using 'should' in the example description. :)

Consistency is a perfectly fine reason :)

@stve Owner
stve added a note

I think I prefer @status.user.should be_kind_of(TweetStream::User) but appreciate you keeping it this way for consistency. I hadn't really noticed it to be honest, but now that I have, I'll probably change to it to the more familiar convention.

By the way, I do like what Corey has done here with it 'modifies...' instead of it 'should modify...'. should makes me think that it would in an ideal world, but it doesn't now, which is not the case (at least if the test is passing ;) ).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@jamesarosen

If every API change pull request came with a screencast of how to use it, the world would be a much happier place. Way to raise the bar, @coreyhaines :)

@coreyhaines

Thanks. With Jing it is super easy. And, I love doing screencasts like this.

@coreyhaines

Now to just see if we can get it merged in. I would wait until after the release, since adding a big feature like this to a release candidate isn't wise. But, maybe we can get a quick one-two version release.

@stve
Owner

Thanks for the pull request @coreyhaines. I will happily include this in the next release candidate for version 1.1.0. I just released 1.1.0.rc1 a few days ago, primarily because the OAuth configuration changes broke compatibility with older versions.

@stve stve merged commit 40ce000 into tweetstream:master
@coreyhaines

Awesome, thanks, Steve! Will be good to point my Gemfile to the official release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
27 README.md
@@ -51,6 +51,33 @@ user ids:
The methods available to TweetStream::Client will be kept in parity
with the methods available on the Streaming API wiki page.
+Using the Twitter Userstream
+----------------------------
+
+Using the Twitter userstream works similarly to the regular streaming, except you use the userstream method.
+
+ # Use 'userstream' to get message from your stream
+ TweetStream::Client.new.userstream do |status|
+ puts status.text
+ end
+
+
+You also can use method hooks for both regular timeline statuses and direct messages.
+
+ client = TweetStream::Client.new
+
+ client.on_direct_message do |direct_message|
+ puts "direct message"
+ puts direct_message.text
+ end
+
+ client.on_timeline_status do |status|
+ puts "timeline status"
+ puts status.text
+ end
+
+ client.userstream
+
Configuration and Changes in 1.1.0
----------------------------------
View
3  lib/tweetstream.rb
@@ -2,6 +2,7 @@
require 'tweetstream/client'
require 'tweetstream/hash'
require 'tweetstream/status'
+require 'tweetstream/direct_message'
require 'tweetstream/user'
require 'tweetstream/daemon'
@@ -39,4 +40,4 @@ def self.method_missing(method, *args, &block)
def self.respond_to?(method)
client.respond_to?(method) || super
end
-end
+end
View
139 lib/tweetstream/client.rb
@@ -122,6 +122,11 @@ def filter(query_params = {}, &block)
start('statuses/filter', query_params.merge(:method => :post), &block)
end
+ # Make a call to the userstream api for currently authenticated user
+ def userstream(&block)
+ start('', :extra_stream_parameters => {:host => "userstream.twitter.com", :path => "/2/user.json"}, &block)
+ end
+
# Set a Proc to be run when a deletion notice is received
# from the Twitter stream. For example:
#
@@ -186,6 +191,69 @@ def on_error(&block)
end
end
+ # Set a Proc to be run when a direct message is encountered in the
+ # processing of the stream.
+ #
+ # @client = TweetStream::Client.new('user','pass')
+ # @client.on_direct_message do |direct_message|
+ # # do something with the direct message
+ # end
+ #
+ # Block must take one argument: the direct message.
+ # If no block is given, it will return the currently set
+ # direct message proc. When a block is given, the TweetStream::Client
+ # object is returned to allow for chaining.
+ def on_direct_message(&block)
+ if block_given?
+ @on_direct_message = block
+ self
+ else
+ @on_direct_message
+ end
+ end
+
+ # Set a Proc to be run whenever anything is encountered in the
+ # processing of the stream.
+ #
+ # @client = TweetStream::Client.new('user','pass')
+ # @client.on_anything do |status|
+ # # do something with the status
+ # end
+ #
+ # Block can take one or two arguments. |status (, client)|
+ # If no block is given, it will return the currently set
+ # timeline status proc. When a block is given, the TweetStream::Client
+ # object is returned to allow for chaining.
+ def on_anything(&block)
+ if block_given?
+ @on_anything = block
+ self
+ else
+ @on_anything
+ end
+ end
+
+ # Set a Proc to be run when a regular timeline message is encountered in the
+ # processing of the stream.
+ #
+ # @client = TweetStream::Client.new('user','pass')
+ # @client.on_timeline_message do |status|
+ # # do something with the status
+ # end
+ #
+ # Block can take one or two arguments. |status (, client)|
+ # If no block is given, it will return the currently set
+ # timeline status proc. When a block is given, the TweetStream::Client
+ # object is returned to allow for chaining.
+ def on_timeline_status(&block)
+ if block_given?
+ @on_timeline_status = block
+ self
+ else
+ @on_timeline_status
+ end
+ end
+
# Set a Proc to be run when connection established.
# Called in EventMachine::Connection#post_init
#
@@ -209,22 +277,27 @@ def start(path, query_parameters = {}, &block) #:nodoc:
limit_proc = query_parameters.delete(:limit) || self.on_limit
error_proc = query_parameters.delete(:error) || self.on_error
inited_proc = query_parameters.delete(:inited) || self.on_inited
+ direct_message_proc = query_parameters.delete(:direct_message) || self.on_direct_message
+ timeline_status_proc = query_parameters.delete(:timeline_status) || self.on_timeline_status
+ anything_proc = query_parameters.delete(:anything) || self.on_anything
params = normalize_filter_parameters(query_parameters)
+ extra_stream_parameters = query_parameters.delete(:extra_stream_parameters) || {}
+
uri = method == :get ? build_uri(path, params) : build_uri(path)
- EventMachine::run {
- stream_params = {
- :path => uri,
- :method => method.to_s.upcase,
- :user_agent => user_agent,
- :on_inited => inited_proc,
- :filters => params.delete(:track),
- :params => params,
- :ssl => true
- }.merge(auth_params)
+ stream_params = {
+ :path => uri,
+ :method => method.to_s.upcase,
+ :user_agent => user_agent,
+ :on_inited => inited_proc,
+ :filters => params.delete(:track),
+ :params => params,
+ :ssl => true
+ }.merge(auth_params).merge(extra_stream_parameters)
+ EventMachine::run {
@stream = Twitter::JSONStream.connect(stream_params)
@stream.each_item do |item|
begin
@@ -240,23 +313,31 @@ def start(path, query_parameters = {}, &block) #:nodoc:
end
hash = TweetStream::Hash.new(raw_hash)
-
if hash[:delete] && hash[:delete][:status]
delete_proc.call(hash[:delete][:status][:id], hash[:delete][:status][:user_id]) if delete_proc.is_a?(Proc)
elsif hash[:limit] && hash[:limit][:track]
limit_proc.call(hash[:limit][:track]) if limit_proc.is_a?(Proc)
+
+ elsif hash[:direct_message]
+ yield_message_to direct_message_proc, TweetStream::DirectMessage.new(hash[:direct_message])
+
elsif hash[:text] && hash[:user]
@last_status = TweetStream::Status.new(hash)
-
- # Give the block the option to receive either one
- # or two arguments, depending on its arity.
- case block.arity
- when 1
- yield @last_status
- when 2
- yield @last_status, self
+ yield_message_to timeline_status_proc, @last_status
+
+ if block_given?
+ # Give the block the option to receive either one
+ # or two arguments, depending on its arity.
+ case block.arity
+ when 1
+ yield @last_status
+ when 2
+ yield @last_status, self
+ end
end
end
+
+ yield_message_to anything_proc, hash
end
@stream.on_error do |message|
@@ -292,14 +373,11 @@ def build_query_parameters(query)
def build_post_body(query) #:nodoc:
return '' unless query && query.is_a?(::Hash) && query.size > 0
- pairs = []
- query.each_pair do |k,v|
+ query.map do |k, v|
v = v.flatten.collect { |q| q.to_s }.join(',') if v.is_a?(Array)
- pairs << "#{k.to_s}=#{CGI.escape(v.to_s)}"
- end
-
- pairs.join('&')
+ "#{k.to_s}=#{CGI.escape(v.to_s)}"
+ end.join('&')
end
def normalize_filter_parameters(query_parameters = {})
@@ -326,5 +404,16 @@ def auth_params
}
end
end
+
+ def yield_message_to(procedure, message)
+ if procedure.is_a?(Proc)
+ case procedure.arity
+ when 1
+ procedure.call(message)
+ when 2
+ procedure.call(message, self)
+ end
+ end
+ end
end
end
View
6 lib/tweetstream/direct_message.rb
@@ -0,0 +1,6 @@
+class TweetStream::DirectMessage < TweetStream::Hash
+ def initialize(hash)
+ super
+ self[:user] = self[:sender] = TweetStream::User.new(self[:sender])
+ end
+end
View
2  lib/tweetstream/status.rb
@@ -5,7 +5,7 @@ def initialize(hash)
super
self[:user] = TweetStream::User.new(self[:user])
end
-
+
def id
self[:id] || super
end
View
1  spec/data/direct_messages.json
@@ -0,0 +1 @@
+{"direct_message":{"created_at":"Sat Sep 24 18:59:38 +0000 2011", "id_str":"4227325281", "sender_screen_name":"coreyhaines", "sender":{"name":"Corey Haines", "profile_sidebar_fill_color":"DAECF4", "profile_sidebar_border_color":"C6E2EE", "profile_background_tile":false, "profile_image_url":"http://a0.twimg.com/profile_images/1508969901/Photo_on_2011-08-22_at_19.15__3_normal.jpg", "created_at":"Sun Dec 23 18:11:29 +0000 2007", "location":"Chicago, IL", "follow_request_sent":false, "id_str":"11458102", "is_translator":false, "profile_link_color":"1F98C7", "default_profile":false, "favourites_count":122, "contributors_enabled":false, "url":"http://www.coreyhaines.com", "id":11458102, "profile_image_url_https":"https://si0.twimg.com/profile_images/1508969901/Photo_on_2011-08-22_at_19.15__3_normal.jpg", "utc_offset":-21600, "profile_use_background_image":true, "listed_count":593, "lang":"en", "followers_count":5764, "protected":false, "profile_text_color":"663B12", "notifications":false, "description":"Software Journeyman, Coderetreat Facilitator, Cofounder of MercuryApp.com, Awesome....\r\nI make magic!", "verified":false, "profile_background_color":"C6E2EE", "geo_enabled":false, "profile_background_image_url_https":"https://si0.twimg.com/images/themes/theme2/bg.gif", "time_zone":"Central Time (US & Canada)", "profile_background_image_url":"http://a1.twimg.com/images/themes/theme2/bg.gif", "default_profile_image":false, "friends_count":423, "statuses_count":35950, "following":false, "screen_name":"coreyhaines", "show_all_inline_media":false}, "recipient_screen_name":"coreyhainestest", "text":"waddup gain", "id":4227325281, "recipient":{"name":"Corey's Test Account", "profile_sidebar_fill_color":"DDEEF6", "profile_sidebar_border_color":"C0DEED", "profile_background_tile":false, "profile_image_url":"http://a2.twimg.com/sticky/default_profile_images/default_profile_3_normal.png", "created_at":"Sat Sep 24 13:04:56 +0000 2011", "location":null, "follow_request_sent":false, "id_str":"379145826", "is_translator":false, "profile_link_color":"0084B4", "default_profile":true, "favourites_count":0, "contributors_enabled":false, "url":null, "id":379145826, "profile_image_url_https":"https://si0.twimg.com/sticky/default_profile_images/default_profile_3_normal.png", "utc_offset":null, "profile_use_background_image":true, "listed_count":0, "lang":"en", "followers_count":1, "protected":false, "profile_text_color":"333333", "notifications":false, "description":null, "verified":false, "profile_background_color":"C0DEED", "geo_enabled":false, "profile_background_image_url_https":"https://si0.twimg.com/images/themes/theme1/bg.png", "time_zone":null, "profile_background_image_url":"http://a0.twimg.com/images/themes/theme1/bg.png", "default_profile_image":true, "friends_count":1, "statuses_count":21, "following":true, "screen_name":"coreyhainestest", "show_all_inline_media":false}, "recipient_id":379145826, "sender_id":11458102}}
View
10 spec/spec_helper.rb
@@ -24,3 +24,13 @@ def sample_tweets
@tweets
end
end
+
+def sample_direct_messages
+ return @direct_messages if @direct_messages
+
+ @direct_messages = []
+ Yajl::Parser.parse(File.open(File.dirname(__FILE__) + '/data/direct_messages.json', 'r')) do |hash|
+ @direct_messages << hash
+ end
+ @direct_messages
+end
View
89 spec/tweetstream/client_spec.rb
@@ -1,4 +1,4 @@
-require File.dirname(__FILE__) + '/../spec_helper'
+require 'spec_helper'
describe TweetStream::Client do
before(:each) do
@@ -123,6 +123,81 @@
end.track('abc')
end
+ context "using on_anything" do
+ it "yields the raw hash" do
+ hash = {:id => 1234}
+ @stream.should_receive(:each_item).and_yield(hash.to_json)
+ yielded_hash = nil
+ @client.on_anything do |hash|
+ yielded_hash = hash
+ end.track('abc')
+ yielded_hash.should_not be_nil
+ yielded_hash.id.should == 1234
+ end
+ it 'yields itself if block has an arity of 2' do
+ hash = {:id => 1234}
+ @stream.should_receive(:each_item).and_yield(hash.to_json)
+ yielded_client = nil
+ @client.on_anything do |_, client|
+ yielded_client = client
+ end.track('abc')
+ yielded_client.should_not be_nil
+ yielded_client.should == @client
+ end
+ end
+
+ context 'using on_timeline_status' do
+ it 'yields a Status' do
+ tweet = sample_tweets[0]
+ tweet[:id] = 123
+ tweet[:user][:screen_name] = 'monkey'
+ tweet[:text] = "Oo oo aa aa"
+ @stream.should_receive(:each_item).and_yield(tweet.to_json)
+ yielded_status = nil
+ @client.on_timeline_status do |status|
+ yielded_status = status
+ end.track('abc')
+ yielded_status.should_not be_nil
+ yielded_status[:id].should == 123
+ yielded_status.user.screen_name.should == 'monkey'
+ yielded_status.text.should == 'Oo oo aa aa'
+ end
+ it 'yields itself if block has an arity of 2' do
+ @stream.should_receive(:each_item).and_yield(sample_tweets[0].to_json)
+ yielded_client = nil
+ @client.on_timeline_status do |_, client|
+ yielded_client = client
+ end.track('abc')
+ yielded_client.should_not be_nil
+ yielded_client.should == @client
+ end
+ end
+
+ context 'using on_direct_message' do
+ it 'yields a DirectMessage' do
+ direct_message = sample_direct_messages[0]
+ direct_message["direct_message"]["id"] = 1234
+ direct_message["direct_message"]["sender"]["screen_name"] = "coder"
+ @stream.should_receive(:each_item).and_yield(direct_message.to_json)
+ yielded_dm = nil
+ @client.on_direct_message do |dm|
+ yielded_dm = dm
+ end.userstream
+ yielded_dm.should_not be_nil
+ yielded_dm.id.should == 1234
+ yielded_dm.user.screen_name.should == "coder"
+ end
+
+ it 'yields itself if block has an arity of 2' do
+ @stream.should_receive(:each_item).and_yield(sample_direct_messages[0].to_json)
+ yielded_client = nil
+ @client.on_direct_message do |_, client|
+ yielded_client = client
+ end.userstream
+ yielded_client.should == @client
+ end
+ end
+
it 'should call on_error if a non-hash response is received' do
@stream.should_receive(:each_item).and_yield('["favorited"]')
@client.on_error do |message|
@@ -287,6 +362,18 @@
@client.track('monday')
end
+
+ context "when calling #userstream" do
+ it "sends the userstream host" do
+ Twitter::JSONStream.should_receive(:connect).with(hash_including(:host => "userstream.twitter.com")).and_return(@stream)
+ @client.userstream
+ end
+
+ it "uses the userstream uri" do
+ Twitter::JSONStream.should_receive(:connect).with(hash_including(:path => "/2/user.json")).and_return(@stream)
+ @client.userstream
+ end
+ end
end
end
View
21 spec/tweetstream/direct_message_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe TweetStream::DirectMessage do
+ it 'modifies the :sender key into a TweetStream::User object called #user' do
+ @status = TweetStream::DirectMessage.new({:sender => {:screen_name => 'bob'}})
+ @status.user.is_a?(TweetStream::User).should be_true

I usually see this assertion written as @status.user.should be_kind_of(TweetStream::User) or ... be_a(TweetStream::User). Is there a reason you chose to write it the way you did? Do you find the magic of be_foo hampers spec maintainability?

This was copied down from the other spec, so I was keeping it consistent with their style.
Although, I did break convention by not using 'should' in the example description. :)

Consistency is a perfectly fine reason :)

@stve Owner
stve added a note

I think I prefer @status.user.should be_kind_of(TweetStream::User) but appreciate you keeping it this way for consistency. I hadn't really noticed it to be honest, but now that I have, I'll probably change to it to the more familiar convention.

By the way, I do like what Corey has done here with it 'modifies...' instead of it 'should modify...'. should makes me think that it would in an ideal world, but it doesn't now, which is not the case (at least if the test is passing ;) ).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ @status.user.screen_name.should == 'bob'
+ end
+
+ it 'transforms the sender into a TweetStream::User object called #sender' do
+ @status = TweetStream::DirectMessage.new({:sender => {:screen_name => 'bob'}})
+ @status.sender.is_a?(TweetStream::User).should be_true
+ @status.sender.screen_name.should == 'bob'
+ end
+
+ it 'overrides the #id method for itself and the user' do
+ @status = TweetStream::DirectMessage.new({:id => 123, :sender => {:id => 345}})
+ @status.id.should == 123
+ @status.user.id.should == 345
+ end
+end
View
8 spec/tweetstream/status_spec.rb
@@ -1,14 +1,14 @@
-require File.dirname(__FILE__) + '/../spec_helper'
+require 'spec_helper'
describe TweetStream::Status do
it 'should modify the :user key into a TweetStream::User object' do
- @status = TweetStream::Status.new(:user => {:screen_name => 'bob'})
+ @status = TweetStream::Status.new({:user => {:screen_name => 'bob'}})
@status.user.is_a?(TweetStream::User).should be_true
@status.user.screen_name.should == 'bob'
end
-
+
it 'should override the #id method for itself and the user' do
- @status = TweetStream::Status.new(:id => 123, :user => {:id => 345})
+ @status = TweetStream::Status.new({:id => 123, :user => {:id => 345}})
@status.id.should == 123
@status.user.id.should == 345
end
Something went wrong with that request. Please try again.