edgar-twitter-search 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+