jcrossley-twitter-search 0.5.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,32 @@
1
+ h2. 0.5.8 (June 30, 2009)
2
+
3
+ * subclass Tweets and Trends from Array. (Dan Croak)
4
+ * change test suite to use cURL'd JSON fixtures & Fakeweb for just HTTP calls
5
+ for more surgical tests. (Dan Croak)
6
+
7
+ h2. 0.5.7 (June 30, 2009)
8
+
9
+ * improved Fakeweb usage. (Matt Jankowski)
10
+ * account for possible 403 response, plus user-friendly message. (Matt Jankowski)
11
+
12
+ h2. 0.5.6 (June 6, 2009)
13
+
14
+ * When Twitter returns a 404, raise a TwitterSearch::SearchServerError instead
15
+ of a JSON parse error. (Luke Francl)
16
+
17
+ h2. 0.5.5 (May 18, 2009)
18
+
19
+ * raise error when query contains :near or :within:. (Dan Croak)
20
+
21
+ h2. 0.5.4 (May 16, 2009)
22
+
23
+ * Fixed bug in Twitter trends. (Dan Croak)
24
+ * Refactored test suite. Now organized by function, uses redgreen, began moving toward using JSON instead of YAML for fixtures. (Dan Croak)
25
+ * Exposed Shoulda Macros to apps that want to use them. (Dan Croak)
26
+
27
+ h2. 0.5.3 (May 15, 2009)
28
+
29
+ * Added Twitter trends. (Matt Sanford)
30
+ * Added overdue attribution for Luke Francl, Matt Sanford, Alejandro Crosa, Danny Burkes, Don Brown, & HotFusionMan.
31
+ * Added CHANGELOG. (Dan Croak)
32
+
@@ -0,0 +1,112 @@
1
+ # A Twitter Search client for Ruby.
2
+
3
+ Access the Twitter Search API from your Ruby code.
4
+
5
+ ## Usage
6
+
7
+ Install the gem.
8
+
9
+ sudo gem install dancroak-twitter-search -s http://gems.github.com
10
+
11
+ Require the gem.
12
+
13
+ require 'twitter_search'
14
+
15
+ Set up a TwitterSearch::Client. Name your client (a.k.a. 'user agent') to something meaningful, such as your app's name. This helps Twitter Search answer any questions about your use of the API.
16
+
17
+ client = TwitterSearch::Client.new('thunderthimble')
18
+
19
+ ### Search
20
+
21
+ Request tweets by calling the query method of your client. It takes either a String or a Hash of arguments.
22
+
23
+ tweets = client.query('twitter search')
24
+
25
+ The String form uses the default Twitter Search behavior, which in this example finds tweets containing both "twitter" and "search". It is identical to the more verbose, explicit version:
26
+
27
+ tweets = client.query(:q => 'twitter search')
28
+
29
+ Use the Twitter Search API's query operators with the :q key to access a variety of behavior.
30
+
31
+ ### Trends
32
+
33
+ Request the current trending topics by calling the trends method of your client. It takes an optional Hash of arguments.
34
+
35
+ trends = client.trends
36
+
37
+ The only supported option currently is exclude_hashtags to return trends that are not hashtags only.
38
+
39
+ trends = client.trends(:exclude_hashtags => true)
40
+
41
+ ## Search Operators
42
+
43
+ The following operator examples find tweets...
44
+
45
+ * <a href="http://search.twitter.com/search?q=twitter+search">:q => 'twitter search'</a> - containing both "twitter" and "search". This is the default operator.
46
+ * <a href="http://search.twitter.com/search?q=%22happy+hour%22">:q => '<b>"</b>happy hour<b>"</b>'</a> - containing the exact phrase "happy hour".
47
+ * <a href="http://search.twitter.com/search?q=obama+OR+hillary">:q => 'obama <b>OR</b> hillary'</a> - containing either "obama" or "hillary" (or both).
48
+ * <a href="http://search.twitter.com/search?q=beer+-root">:q => 'beer <b>-</b>root'</a> - containing "beer" but not "root".
49
+ * <a href="http://search.twitter.com/search?q=%23haiku">:q => '<b>#</b>haiku</a>' - containing the hashtag "haiku".
50
+ * <a href="http://search.twitter.com/search?q=from%3Aalexiskold">:q => '<b>from:</b>alexiskold'</a> - sent from person "alexiskold".
51
+ * <a href="http://search.twitter.com/search?q=to%3Atechcrunch">:q => '<b>to:</b>techcrunch</a>' - sent to person "techcrunch".
52
+ * <a href="http://search.twitter.com/search?q=%40mashable">:q => '<b>@</b>mashable</a>' - referencing person "mashable".
53
+ * <a href="http://search.twitter.com/search?q=superhero+since%3A2008-05-01">:q => 'superhero <b>since:</b>2008-05-01'</a> - containing "superhero" and sent since date "2008-05-01" (year-month-day).
54
+ * <a href="http://search.twitter.com/search?q=ftw+until%3A2008-05-03">:q => 'ftw <b>until:</b>2008-05-03'</a> - containing "ftw" and sent up to date "2008-05-03".
55
+ * <a href="http://search.twitter.com/search?q=movie+-scary+%3A%29">:q => 'movie -scary <b>:)</b>'</a> - containing "movie", but not "scary", and with a positive attitude.
56
+ * <a href="http://search.twitter.com/search?q=flight+%3A%28">:q => 'flight <b>:(</b>'</a> - containing "flight" and with a negative attitude.
57
+ * <a href="http://search.twitter.com/search?q=traffic+%3F">:q => 'traffic <b>?</b>'</a> - containing "traffic" and asking a question.
58
+ * <a href="http://search.twitter.com/search?q=hilarious+filter%3Alinks">:q => 'hilarious <b>filter:links</b>'</a> - containing "hilarious" and linking to URLs.
59
+
60
+ ### Foreign Languages
61
+
62
+ The Twitter Search API supports foreign languages, accessible via the :lang key. Use the [ISO 639-1](http://en.wikipedia.org/wiki/ISO_639-1) codes as the value:
63
+
64
+ tweets = client.query(:q => 'programmé', :lang => 'fr')
65
+
66
+ ### Pagination
67
+
68
+ Alter the number of Tweets returned per page with the :rpp key. Stick with 10, 15, 20, 25, 30, or 50.
69
+
70
+ tweets = client.query(:q => 'Boston Celtics', :rpp => '30')
71
+
72
+ ## Gotchas
73
+
74
+ * Searches are case-insenstive.
75
+ * The "near" operator available in the Twitter Search web interface is not available via the API. You must geocode before making your Twitter Search API call, and use the :geocode key in your request using the pattern lat,lngmi or lat,lngkm:
76
+
77
+ tweets = client.query(:q => 'Pearl Jam', :geocode => '43.4411,-70.9846mi')
78
+
79
+ * Searching for a positive attitude :) returns tweets containing the text :), =), :D, and :-)
80
+
81
+ ## Contributing
82
+
83
+ Get the source and clone it.
84
+
85
+ The test suite uses JSON fixtures which were created from cURL. Usage example:
86
+
87
+ query = { :q => 'rails training' }
88
+ fake_query(query, 'rails_training.json')
89
+ @tweets = TwitterSearch::Client.new.query(query)
90
+
91
+ Then you assert any necessary expectations on @tweets.
92
+
93
+ To create your own JSON fixtures, first get the CGI escaped querystring in irb:
94
+
95
+ require 'lib/twitter_search'
96
+ TwitterSearch::Client.new.sanitize_query({ :q => 'rails training' })
97
+
98
+ Then take the output and append it to a simple cURL, sending the output into your new file:
99
+
100
+ curl -i 'http://search.twitter.com/search.json?q=rails+training' > test/json/rails_training.json
101
+
102
+ ## Contributors
103
+
104
+ Dustin Sallings, Dan Croak, Luke Francl, Matt Jankowski, Matt Sanford, Alejandro Crosa, Danny Burkes, Don Brown, & HotFusionMan.
105
+
106
+ ## Resources
107
+
108
+ * [Official Twitter Search API](http://apiwiki.twitter.com/Twitter-API-Documentation)
109
+
110
+ ## License
111
+
112
+ MIT License, same terms as Ruby.
@@ -0,0 +1,71 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rubygems'
4
+
5
+ test_files_pattern = 'test/*_test.rb'
6
+ Rake::TestTask.new do |t|
7
+ t.libs << 'lib'
8
+ t.pattern = test_files_pattern
9
+ t.verbose = false
10
+ end
11
+
12
+ desc "Run the test suite"
13
+ task :default => :test
14
+
15
+ gem_spec = Gem::Specification.new do |gem_spec|
16
+ gem_spec.name = "jcrossley-twitter-search"
17
+ gem_spec.version = "0.5.8"
18
+ gem_spec.summary = "Ruby client for Twitter Search. Includes trends."
19
+ gem_spec.email = "dcroak@thoughtbot.com"
20
+ gem_spec.homepage = "http://github.com/jcrossley/twitter-search"
21
+ gem_spec.description = "Ruby client for Twitter Search."
22
+ gem_spec.authors = ["Dustin Sallings", "Dan Croak", "Luke Francl", "Matt Jankowski", "Matt Sanford", "Alejandro Crosa", "Danny Burkes", "Don Brown", "HotFusionMan"]
23
+ gem_spec.files = FileList["[A-Z]*", "{lib,shoulda_macros}/**/*"]
24
+ gem_spec.add_dependency('json', '>= 1.1.2')
25
+ end
26
+
27
+ desc "Generate a gemspec file"
28
+ task :gemspec do
29
+ File.open("#{gem_spec.name}.gemspec", 'w') do |f|
30
+ f.write gem_spec.to_yaml
31
+ end
32
+ end
33
+
34
+ require File.expand_path('lib/twitter_search', File.dirname(__FILE__))
35
+ require 'rubygems'
36
+ require 'yaml'
37
+
38
+ namespace :yaml do
39
+ desc "Write Twitter Search results to yaml file so API is not hit every test."
40
+ task :write do
41
+ write_yaml :tweets => 'Obama', :file => 'obama'
42
+ write_yaml :tweets => 'twitter search', :file => 'twitter_search'
43
+ write_yaml :tweets => {:q => 'twitter search'}, :file => 'twitter_search_and'
44
+ write_yaml :tweets => {:q => '"happy hour"'}, :file => 'happy_hour_exact'
45
+ write_yaml :tweets => {:q => 'obama OR hillary'}, :file => 'obama_or_hillary'
46
+ write_yaml :tweets => {:q => 'beer -root'}, :file => 'beer_minus_root'
47
+ write_yaml :tweets => {:q => '#haiku'}, :file => 'hashtag_haiku'
48
+ write_yaml :tweets => {:q => 'from:alexiskold'}, :file => 'from_alexiskold'
49
+ write_yaml :tweets => {:q => 'to:techcrunch'}, :file => 'to_techcrunch'
50
+ write_yaml :tweets => {:q => '@mashable'}, :file => 'reference_mashable'
51
+ write_yaml :tweets => {:q => '"happy hour" near:"san francisco"'}, :file => 'happy_hour_near_sf'
52
+ write_yaml :tweets => {:q => 'near:NYC within:15mi'}, :file => 'within_15mi_nyc'
53
+ write_yaml :tweets => {:q => 'superhero since:2008-05-01'}, :file => 'superhero_since'
54
+ write_yaml :tweets => {:q => 'ftw until:2008-05-03'}, :file => 'ftw_until'
55
+ write_yaml :tweets => {:q => 'movie -scary :)'}, :file => 'movie_positive_tude'
56
+ write_yaml :tweets => {:q => 'flight :('}, :file => 'flight_negative_tude'
57
+ write_yaml :tweets => {:q => 'traffic ?'}, :file => 'traffic_question'
58
+ write_yaml :tweets => {:q => 'hilarious filter:links'}, :file => 'hilarious_links'
59
+ write_yaml :tweets => {:q => 'congratulations', :lang => 'en'}, :file => 'english'
60
+ write_yaml :tweets => {:q => 'با', :lang => 'ar'}, :file => 'arabic'
61
+ write_yaml :tweets => {:q => 'Boston Celtics', :rpp => '30'}, :file => 'results_per_page'
62
+ end
63
+ end
64
+
65
+ def write_yaml(opts = {})
66
+ @client = TwitterSearch::Client.new 'twitter-search'
67
+ tweets = @client.query(opts[:tweets])
68
+ File.open(File.join(File.dirname(__FILE__), 'test', 'yaml', "#{opts[:file]}.yaml"), 'w+') do |file|
69
+ file.puts tweets.to_yaml
70
+ end
71
+ end
@@ -0,0 +1,7 @@
1
+ Add tests for:
2
+
3
+ * since_id: returns tweets with status ids greater than the given id.
4
+ * geocode: returns tweets by users located within a given radius of the given latitude/longitude, where the user's location is taken from their Twitter profile.
5
+ * show_user: when "true", adds "<user>:" to the beginning of the tweet. This is useful for readers that do not display Atom's author field. The default is "false".
6
+ * callback: if supplied, the response will use the JSONP format with a callback of the given name. E.g., http://search.twitter.com/search.json?callback=foo&q=twitter
7
+
@@ -0,0 +1,24 @@
1
+ module TwitterSearch
2
+ class Trend
3
+ VARS = [:query, :name]
4
+ attr_reader *VARS
5
+ attr_reader :exclude_hashtags
6
+
7
+ def initialize(opts)
8
+ @exclude_hashtags = !!opts['exclude_hashtags']
9
+ VARS.each { |each| instance_variable_set "@#{each}", opts[each.to_s] }
10
+ end
11
+ end
12
+
13
+ class Trends < Array
14
+ VARS = [:date]
15
+ attr_reader *VARS
16
+
17
+ def initialize(opts)
18
+ trends = opts('trends').delete
19
+ trends = trends.values.first.collect { |each| Trend.new(each) }
20
+ super(trends)
21
+ VARS.each { |each| instance_variable_set "@#{each}", opts[each.to_s] }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,43 @@
1
+ module TwitterSearch
2
+ class Tweet
3
+ VARS = [:text, :from_user, :to_user, :to_user_id, :id, :iso_language_code, :from_user_id, :created_at, :profile_image_url, :source, :geo ]
4
+ attr_reader *VARS
5
+ attr_reader :language
6
+
7
+ def initialize(opts)
8
+ @language = opts['iso_language_code']
9
+ VARS.each { |each| instance_variable_set "@#{each}", opts[each.to_s] }
10
+ end
11
+
12
+
13
+ def eql?(other)
14
+ self.hash == other.hash
15
+ end
16
+
17
+ def hash
18
+ @id
19
+ end
20
+
21
+ end
22
+
23
+ class Tweets < Array
24
+ VARS = [:since_id, :max_id, :results_per_page, :page, :query, :next_page]
25
+ attr_reader *VARS
26
+
27
+ def initialize(opts)
28
+ results = opts.delete('results') || []
29
+ results.collect! { |each| Tweet.new(each) }
30
+ super(results)
31
+ VARS.each { |each| instance_variable_set "@#{each}", opts[each.to_s] }
32
+ end
33
+
34
+ def has_next_page?
35
+ ! @next_page.nil?
36
+ end
37
+
38
+ def get_next_page
39
+ client = Client.new
40
+ return client.query( CGI.parse( @next_page[1..-1] ) )
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,114 @@
1
+ require 'rubygems'
2
+ require 'net/http'
3
+ require 'json'
4
+ require 'cgi'
5
+
6
+ require File.join(File.dirname(__FILE__), 'tweets')
7
+ require File.join(File.dirname(__FILE__), 'trends')
8
+
9
+ module TwitterSearch
10
+ class SearchOperatorError < ArgumentError
11
+ end
12
+ class SearchServerError < RuntimeError
13
+ end
14
+ class RateLimitError < SearchServerError
15
+ attr_reader :retry_after
16
+ def initialize(retry_after)
17
+ super "Twitter has rate limited your IP address; wait #{retry_after} seconds before retry."
18
+ @retry_after = retry_after.to_i
19
+ end
20
+ end
21
+
22
+ class Client
23
+ TWITTER_SEARCH_API_URL = 'http://search.twitter.com/search.json'
24
+ TWITTER_TRENDS_API_URL = 'http://search.twitter.com/trends/current.json'
25
+ DEFAULT_TIMEOUT = 5
26
+
27
+ attr_accessor :agent
28
+ attr_accessor :timeout
29
+
30
+ def initialize(agent = 'twitter-search', timeout = DEFAULT_TIMEOUT)
31
+ @agent = agent
32
+ @timeout = timeout
33
+ end
34
+
35
+ def headers
36
+ { "Content-Type" => 'application/json',
37
+ "User-Agent" => @agent }
38
+ end
39
+
40
+ def query(opts = {})
41
+ url = URI.parse(TWITTER_SEARCH_API_URL)
42
+ url.query = sanitize_query(opts)
43
+
44
+ ensure_no_location_operators(url.query)
45
+
46
+ req = Net::HTTP::Get.new(url.path)
47
+ http = Net::HTTP.new(url.host, url.port)
48
+ http.read_timeout = timeout
49
+
50
+ res = http.start { |http|
51
+ http.get("#{url.path}?#{url.query}", headers)
52
+ }
53
+
54
+ if res.code == '404'
55
+ raise TwitterSearch::SearchServerError,
56
+ "Twitter responded with a 404 for your query."
57
+ end
58
+
59
+ if res.code == '420'
60
+ raise TwitterSearch::RateLimitError.new(res['retry-after'])
61
+ end
62
+
63
+ json = res.body
64
+ parsed_json = JSON.parse(json)
65
+
66
+ if parsed_json['error']
67
+ raise TwitterSearch::SearchServerError,
68
+ "Twitter responded with an error body: #{parsed_json['error']}"
69
+ end
70
+
71
+ Tweets.new parsed_json
72
+ end
73
+
74
+ def trends(opts = {})
75
+ url = URI.parse(TWITTER_TRENDS_API_URL)
76
+ if opts['exclude_hashtags']
77
+ url.query = sanitize_query_hash({ :exclude_hashtags => opts['exclude_hashtags'] })
78
+ end
79
+
80
+ req = Net::HTTP::Get.new(url.path)
81
+ http = Net::HTTP.new(url.host, url.port)
82
+ http.read_timeout = timeout
83
+
84
+ json = http.start { |http|
85
+ http.get("#{url.path}?#{url.query}", headers)
86
+ }.body
87
+
88
+ Trends.new JSON.parse(json)
89
+ end
90
+
91
+ def sanitize_query(opts)
92
+ if opts.is_a? String
93
+ "q=#{CGI.escape(opts)}"
94
+ elsif opts.is_a? Hash
95
+ "#{sanitize_query_hash(opts)}"
96
+ end
97
+ end
98
+
99
+ def sanitize_query_hash(query_hash)
100
+ query_hash.collect { |key, value|
101
+ "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
102
+ }.join('&')
103
+ end
104
+
105
+ def ensure_no_location_operators(query_string)
106
+ if query_string.include?("near%3A") ||
107
+ query_string.include?("within%3A")
108
+ raise TwitterSearch::SearchOperatorError,
109
+ "near: and within: are available from the Twitter Search web interface, but not the API. The API requires the geocode parameter. See dancroak/twitter-search README."
110
+ end
111
+ end
112
+
113
+ end
114
+ end
@@ -0,0 +1,36 @@
1
+ module TwitterSearch
2
+ module Shoulda
3
+ def should_have_default_search_behaviors
4
+ should_find_tweets
5
+ should_have_text_for_all_tweets
6
+ should_return_page 1
7
+ should_return_tweets_in_sets_of 15
8
+ end
9
+
10
+ def should_find_tweets
11
+ should 'find tweets' do
12
+ assert @tweets.any?
13
+ end
14
+ end
15
+
16
+ def should_have_text_for_all_tweets
17
+ should 'have text for all tweets' do
18
+ assert @tweets.all? { |tweet| tweet.text.size > 0 }
19
+ end
20
+ end
21
+
22
+ def should_return_page(number)
23
+ should "return page #{number}" do
24
+ assert_equal number, @tweets.page
25
+ end
26
+ end
27
+
28
+ def should_return_tweets_in_sets_of(number)
29
+ should "return tweets in sets of #{number}" do
30
+ assert_equal number, @tweets.results_per_page
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ Test::Unit::TestCase.extend(TwitterSearch::Shoulda)
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jcrossley-twitter-search
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 5
8
+ - 8
9
+ version: 0.5.8
10
+ platform: ruby
11
+ authors:
12
+ - Dustin Sallings
13
+ - Dan Croak
14
+ - Luke Francl
15
+ - Matt Jankowski
16
+ - Matt Sanford
17
+ - Alejandro Crosa
18
+ - Danny Burkes
19
+ - Don Brown
20
+ - HotFusionMan
21
+ autorequire:
22
+ bindir: bin
23
+ cert_chain: []
24
+
25
+ date: 2010-03-12 00:00:00 -05:00
26
+ default_executable:
27
+ dependencies:
28
+ - !ruby/object:Gem::Dependency
29
+ name: json
30
+ prerelease: false
31
+ requirement: &id001 !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ segments:
36
+ - 1
37
+ - 1
38
+ - 2
39
+ version: 1.1.2
40
+ type: :runtime
41
+ version_requirements: *id001
42
+ description: Ruby client for Twitter Search.
43
+ email: dcroak@thoughtbot.com
44
+ executables: []
45
+
46
+ extensions: []
47
+
48
+ extra_rdoc_files: []
49
+
50
+ files:
51
+ - Rakefile
52
+ - README.markdown
53
+ - TODO.markdown
54
+ - CHANGELOG.textile
55
+ - lib/tweets.rb
56
+ - lib/twitter_search.rb
57
+ - lib/trends.rb
58
+ - shoulda_macros/twitter_search.rb
59
+ has_rdoc: true
60
+ homepage: http://github.com/jcrossley/twitter-search
61
+ licenses: []
62
+
63
+ post_install_message:
64
+ rdoc_options: []
65
+
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ segments:
80
+ - 0
81
+ version: "0"
82
+ requirements: []
83
+
84
+ rubyforge_project:
85
+ rubygems_version: 1.3.6
86
+ signing_key:
87
+ specification_version: 3
88
+ summary: Ruby client for Twitter Search. Includes trends.
89
+ test_files: []
90
+