edgar-twitter-search 0.5.9

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.textile ADDED
@@ -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
+
data/README.markdown ADDED
@@ -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.
data/Rakefile ADDED
@@ -0,0 +1,70 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ test_files_pattern = 'test/*_test.rb'
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'lib'
7
+ t.pattern = test_files_pattern
8
+ t.verbose = false
9
+ end
10
+
11
+ desc "Run the test suite"
12
+ task :default => :test
13
+
14
+ gem_spec = Gem::Specification.new do |gem_spec|
15
+ gem_spec.name = "edgar-twitter-search"
16
+ gem_spec.version = "0.5.9"
17
+ gem_spec.summary = "Ruby client for Twitter Search. Includes trends."
18
+ gem_spec.email = "edgar@hasmanydevelopers.com"
19
+ gem_spec.homepage = "http://github.com/edgar/twitter-search"
20
+ gem_spec.description = "Ruby client for Twitter Search."
21
+ gem_spec.authors = ["Dustin Sallings", "Dan Croak", "Luke Francl", "Matt Jankowski", "Matt Sanford", "Alejandro Crosa", "Danny Burkes", "Don Brown", "HotFusionMan", "Edgar Gonzalez"]
22
+ gem_spec.files = FileList["[A-Z]*", "{lib,shoulda_macros}/**/*"]
23
+ gem_spec.add_dependency('json', '>= 1.1.2')
24
+ end
25
+
26
+ desc "Generate a gemspec file"
27
+ task :gemspec do
28
+ File.open("#{gem_spec.name}.gemspec", 'w') do |f|
29
+ f.write gem_spec.to_yaml
30
+ end
31
+ end
32
+
33
+ require File.expand_path('lib/twitter_search', File.dirname(__FILE__))
34
+ require 'rubygems'
35
+ require 'yaml'
36
+
37
+ namespace :yaml do
38
+ desc "Write Twitter Search results to yaml file so API is not hit every test."
39
+ task :write do
40
+ write_yaml :tweets => 'Obama', :file => 'obama'
41
+ write_yaml :tweets => 'twitter search', :file => 'twitter_search'
42
+ write_yaml :tweets => {:q => 'twitter search'}, :file => 'twitter_search_and'
43
+ write_yaml :tweets => {:q => '"happy hour"'}, :file => 'happy_hour_exact'
44
+ write_yaml :tweets => {:q => 'obama OR hillary'}, :file => 'obama_or_hillary'
45
+ write_yaml :tweets => {:q => 'beer -root'}, :file => 'beer_minus_root'
46
+ write_yaml :tweets => {:q => '#haiku'}, :file => 'hashtag_haiku'
47
+ write_yaml :tweets => {:q => 'from:alexiskold'}, :file => 'from_alexiskold'
48
+ write_yaml :tweets => {:q => 'to:techcrunch'}, :file => 'to_techcrunch'
49
+ write_yaml :tweets => {:q => '@mashable'}, :file => 'reference_mashable'
50
+ write_yaml :tweets => {:q => '"happy hour" near:"san francisco"'}, :file => 'happy_hour_near_sf'
51
+ write_yaml :tweets => {:q => 'near:NYC within:15mi'}, :file => 'within_15mi_nyc'
52
+ write_yaml :tweets => {:q => 'superhero since:2008-05-01'}, :file => 'superhero_since'
53
+ write_yaml :tweets => {:q => 'ftw until:2008-05-03'}, :file => 'ftw_until'
54
+ write_yaml :tweets => {:q => 'movie -scary :)'}, :file => 'movie_positive_tude'
55
+ write_yaml :tweets => {:q => 'flight :('}, :file => 'flight_negative_tude'
56
+ write_yaml :tweets => {:q => 'traffic ?'}, :file => 'traffic_question'
57
+ write_yaml :tweets => {:q => 'hilarious filter:links'}, :file => 'hilarious_links'
58
+ write_yaml :tweets => {:q => 'congratulations', :lang => 'en'}, :file => 'english'
59
+ write_yaml :tweets => {:q => 'با', :lang => 'ar'}, :file => 'arabic'
60
+ write_yaml :tweets => {:q => 'Boston Celtics', :rpp => '30'}, :file => 'results_per_page'
61
+ end
62
+ end
63
+
64
+ def write_yaml(opts = {})
65
+ @client = TwitterSearch::Client.new 'twitter-search'
66
+ tweets = @client.query(opts[:tweets])
67
+ File.open(File.join(File.dirname(__FILE__), 'test', 'yaml', "#{opts[:file]}.yaml"), 'w+') do |file|
68
+ file.puts tweets.to_yaml
69
+ end
70
+ end
data/TODO.markdown ADDED
@@ -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
+
data/lib/trends.rb ADDED
@@ -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
data/lib/tweets.rb ADDED
@@ -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,117 @@
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 TwitterSearchError < StandardError
11
+ end
12
+ class SearchOperatorError < TwitterSearchError
13
+ end
14
+ class SearchServerError < TwitterSearchError
15
+ end
16
+ class RateLimited < TwitterSearchError
17
+ attr_reader :retry_after
18
+
19
+ def initialize(msg, retry_after = nil)
20
+ super(msg)
21
+ @retry_after = retry_after
22
+ end
23
+ end
24
+
25
+ class Client
26
+ TWITTER_SEARCH_API_URL = 'http://search.twitter.com/search.json'
27
+ TWITTER_TRENDS_API_URL = 'http://search.twitter.com/trends/current.json'
28
+ DEFAULT_TIMEOUT = 5
29
+
30
+ attr_accessor :agent
31
+ attr_accessor :timeout
32
+
33
+ def initialize(agent = 'twitter-search', timeout = DEFAULT_TIMEOUT)
34
+ @agent = agent
35
+ @timeout = timeout
36
+ end
37
+
38
+ def headers
39
+ { "Content-Type" => 'application/json',
40
+ "User-Agent" => @agent }
41
+ end
42
+
43
+ def query(opts = {})
44
+ url = URI.parse(TWITTER_SEARCH_API_URL)
45
+ url.query = sanitize_query(opts)
46
+
47
+ ensure_no_location_operators(url.query)
48
+
49
+ req = Net::HTTP::Get.new(url.path)
50
+ http = Net::HTTP.new(url.host, url.port)
51
+ http.read_timeout = timeout
52
+
53
+ res = http.start { |http|
54
+ http.get("#{url.path}?#{url.query}", headers)
55
+ }
56
+
57
+ if res.code == '404'
58
+ raise TwitterSearch::SearchServerError,
59
+ "Twitter responded with a 404 for your query."
60
+ end
61
+
62
+ unless res['retry-after'].blank?
63
+ raise TwitterSearch::RateLimited.new("You have been rate limited", res['retry-after'].to_i)
64
+ end
65
+
66
+ json = res.body
67
+ parsed_json = JSON.parse(json)
68
+
69
+ if parsed_json['error']
70
+ raise TwitterSearch::SearchServerError,
71
+ "Twitter responded with an error body: #{parsed_json['error']}"
72
+ end
73
+
74
+ Tweets.new parsed_json
75
+ end
76
+
77
+ def trends(opts = {})
78
+ url = URI.parse(TWITTER_TRENDS_API_URL)
79
+ if opts['exclude_hashtags']
80
+ url.query = sanitize_query_hash({ :exclude_hashtags => opts['exclude_hashtags'] })
81
+ end
82
+
83
+ req = Net::HTTP::Get.new(url.path)
84
+ http = Net::HTTP.new(url.host, url.port)
85
+ http.read_timeout = timeout
86
+
87
+ json = http.start { |http|
88
+ http.get("#{url.path}?#{url.query}", headers)
89
+ }.body
90
+
91
+ Trends.new JSON.parse(json)
92
+ end
93
+
94
+ def sanitize_query(opts)
95
+ if opts.is_a? String
96
+ "q=#{CGI.escape(opts)}"
97
+ elsif opts.is_a? Hash
98
+ "#{sanitize_query_hash(opts)}"
99
+ end
100
+ end
101
+
102
+ def sanitize_query_hash(query_hash)
103
+ query_hash.collect { |key, value|
104
+ "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
105
+ }.join('&')
106
+ end
107
+
108
+ def ensure_no_location_operators(query_string)
109
+ if query_string.include?("near%3A") ||
110
+ query_string.include?("within%3A")
111
+ raise TwitterSearch::SearchOperatorError,
112
+ "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."
113
+ end
114
+ end
115
+
116
+ end
117
+ 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,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: edgar-twitter-search
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 5
8
+ - 9
9
+ version: 0.5.9
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
+ - Edgar Gonzalez
22
+ autorequire:
23
+ bindir: bin
24
+ cert_chain: []
25
+
26
+ date: 2010-04-23 00:00:00 -04:30
27
+ default_executable:
28
+ dependencies:
29
+ - !ruby/object:Gem::Dependency
30
+ name: json
31
+ prerelease: false
32
+ requirement: &id001 !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ segments:
37
+ - 1
38
+ - 1
39
+ - 2
40
+ version: 1.1.2
41
+ type: :runtime
42
+ version_requirements: *id001
43
+ description: Ruby client for Twitter Search.
44
+ email: edgar@hasmanydevelopers.com
45
+ executables: []
46
+
47
+ extensions: []
48
+
49
+ extra_rdoc_files: []
50
+
51
+ files:
52
+ - TODO.markdown
53
+ - CHANGELOG.textile
54
+ - README.markdown
55
+ - Rakefile
56
+ - lib/tweets.rb
57
+ - lib/twitter_search.rb
58
+ - lib/trends.rb
59
+ - shoulda_macros/twitter_search.rb
60
+ has_rdoc: true
61
+ homepage: http://github.com/edgar/twitter-search
62
+ licenses: []
63
+
64
+ post_install_message:
65
+ rdoc_options: []
66
+
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ segments:
74
+ - 0
75
+ version: "0"
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ segments:
81
+ - 0
82
+ version: "0"
83
+ requirements: []
84
+
85
+ rubyforge_project:
86
+ rubygems_version: 1.3.6
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: Ruby client for Twitter Search. Includes trends.
90
+ test_files: []
91
+