rorra-twitter 0.9.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.
Files changed (97) hide show
  1. data/History +290 -0
  2. data/License +20 -0
  3. data/Notes +33 -0
  4. data/README.rdoc +19 -0
  5. data/Rakefile +36 -0
  6. data/VERSION.yml +5 -0
  7. data/examples/connect.rb +30 -0
  8. data/examples/friendship_existence.rb +13 -0
  9. data/examples/helpers/config_store.rb +38 -0
  10. data/examples/httpauth.rb +11 -0
  11. data/examples/ids.rb +13 -0
  12. data/examples/lists.rb +11 -0
  13. data/examples/oauth.rb +27 -0
  14. data/examples/search.rb +15 -0
  15. data/examples/timeline.rb +19 -0
  16. data/examples/tumblr.rb +9 -0
  17. data/examples/unauthorized.rb +16 -0
  18. data/examples/update.rb +11 -0
  19. data/examples/user.rb +5 -0
  20. data/lib/twitter.rb +156 -0
  21. data/lib/twitter/base.rb +393 -0
  22. data/lib/twitter/geo.rb +25 -0
  23. data/lib/twitter/httpauth.rb +53 -0
  24. data/lib/twitter/local_trends.rb +30 -0
  25. data/lib/twitter/oauth.rb +64 -0
  26. data/lib/twitter/request.rb +71 -0
  27. data/lib/twitter/search.rb +163 -0
  28. data/lib/twitter/trends.rb +55 -0
  29. data/test/fixtures/blocking.json +1632 -0
  30. data/test/fixtures/firehose.json +1 -0
  31. data/test/fixtures/follower_ids.json +1 -0
  32. data/test/fixtures/followers.json +1 -0
  33. data/test/fixtures/friend_ids.json +1 -0
  34. data/test/fixtures/friends_timeline.json +1 -0
  35. data/test/fixtures/friendship.json +1 -0
  36. data/test/fixtures/friendship_exists.json +1 -0
  37. data/test/fixtures/geo_place.json +1 -0
  38. data/test/fixtures/geo_reverse_geocode.json +1 -0
  39. data/test/fixtures/geo_reverse_geocode_granularity.json +1 -0
  40. data/test/fixtures/geo_reverse_geocode_limit.json +1 -0
  41. data/test/fixtures/geo_search.json +1 -0
  42. data/test/fixtures/geo_search_ip_address.json +1 -0
  43. data/test/fixtures/geo_search_query.json +1 -0
  44. data/test/fixtures/home_timeline.json +1 -0
  45. data/test/fixtures/ids.json +1 -0
  46. data/test/fixtures/list.json +1 -0
  47. data/test/fixtures/list_statuses.json +1 -0
  48. data/test/fixtures/list_statuses_1_1.json +1 -0
  49. data/test/fixtures/list_statuses_2_1.json +1 -0
  50. data/test/fixtures/list_subscriptions.json +1 -0
  51. data/test/fixtures/list_users.json +1 -0
  52. data/test/fixtures/lists.json +1 -0
  53. data/test/fixtures/memberships.json +1 -0
  54. data/test/fixtures/mentions.json +1 -0
  55. data/test/fixtures/not_found.json +1 -0
  56. data/test/fixtures/people_search.json +39 -0
  57. data/test/fixtures/rate_limit_exceeded.json +1 -0
  58. data/test/fixtures/report_spam.json +41 -0
  59. data/test/fixtures/retweet.json +1 -0
  60. data/test/fixtures/retweeted_by_me.json +1 -0
  61. data/test/fixtures/retweeted_to_me.json +1 -0
  62. data/test/fixtures/retweeters_of_tweet.json +166 -0
  63. data/test/fixtures/retweets.json +1 -0
  64. data/test/fixtures/retweets_of_me.json +1 -0
  65. data/test/fixtures/sample-image.png +0 -0
  66. data/test/fixtures/saved_search.json +7 -0
  67. data/test/fixtures/saved_searches.json +16 -0
  68. data/test/fixtures/search.json +1 -0
  69. data/test/fixtures/search_from_jnunemaker.json +1 -0
  70. data/test/fixtures/status.json +1 -0
  71. data/test/fixtures/status_show.json +1 -0
  72. data/test/fixtures/trends_available.json +253 -0
  73. data/test/fixtures/trends_current.json +1 -0
  74. data/test/fixtures/trends_current_exclude.json +1 -0
  75. data/test/fixtures/trends_daily.json +1925 -0
  76. data/test/fixtures/trends_daily_date.json +1 -0
  77. data/test/fixtures/trends_daily_exclude.json +1 -0
  78. data/test/fixtures/trends_location.json +57 -0
  79. data/test/fixtures/trends_weekly.json +1 -0
  80. data/test/fixtures/trends_weekly_date.json +1 -0
  81. data/test/fixtures/trends_weekly_exclude.json +1 -0
  82. data/test/fixtures/unauthorized.json +1 -0
  83. data/test/fixtures/update_profile_background_image.json +1 -0
  84. data/test/fixtures/update_profile_image.json +1 -0
  85. data/test/fixtures/user.json +1 -0
  86. data/test/fixtures/user_timeline.json +710 -0
  87. data/test/fixtures/users.json +1 -0
  88. data/test/test_helper.rb +47 -0
  89. data/test/twitter/base_test.rb +426 -0
  90. data/test/twitter/geo_test.rb +79 -0
  91. data/test/twitter/httpauth_test.rb +86 -0
  92. data/test/twitter/oauth_test.rb +108 -0
  93. data/test/twitter/request_test.rb +217 -0
  94. data/test/twitter/search_test.rb +208 -0
  95. data/test/twitter/trends_test.rb +112 -0
  96. data/test/twitter_test.rb +106 -0
  97. metadata +328 -0
@@ -0,0 +1,38 @@
1
+ class ConfigStore
2
+ attr_reader :file
3
+
4
+ def initialize(file)
5
+ @file = file
6
+ end
7
+
8
+ def load
9
+ @config ||= YAML::load(open(file))
10
+ self
11
+ end
12
+
13
+ def [](key)
14
+ load
15
+ @config[key]
16
+ end
17
+
18
+ def []=(key, value)
19
+ @config[key] = value
20
+ end
21
+
22
+ def delete(*keys)
23
+ keys.each { |key| @config.delete(key) }
24
+ save
25
+ self
26
+ end
27
+
28
+ def update(c={})
29
+ @config.merge!(c)
30
+ save
31
+ self
32
+ end
33
+
34
+ def save
35
+ File.open(file, 'w') { |f| f.write(YAML.dump(@config)) }
36
+ self
37
+ end
38
+ end
@@ -0,0 +1,11 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'twitter')
2
+ require File.join(File.dirname(__FILE__), 'helpers', 'config_store')
3
+ require 'pp'
4
+
5
+ config = ConfigStore.new("#{ENV['HOME']}/.twitter")
6
+
7
+ httpauth = Twitter::HTTPAuth.new(config['email'], config['password'])
8
+ base = Twitter::Base.new(httpauth)
9
+
10
+ pp base.user_timeline
11
+ pp base.verify_credentials
@@ -0,0 +1,13 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'twitter')
2
+ require File.join(File.dirname(__FILE__), 'helpers', 'config_store')
3
+ require 'pp'
4
+
5
+ config = ConfigStore.new("#{ENV['HOME']}/.twitter")
6
+
7
+ oauth = Twitter::OAuth.new(config['token'], config['secret'])
8
+ oauth.authorize_from_access(config['atoken'], config['asecret'])
9
+
10
+ client = Twitter::Base.new(oauth)
11
+
12
+ puts client.friend_ids
13
+ puts client.follower_ids
@@ -0,0 +1,11 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'twitter')
2
+ require File.join(File.dirname(__FILE__), 'helpers', 'config_store')
3
+ require 'pp'
4
+
5
+ config = ConfigStore.new("#{ENV['HOME']}/.twitter")
6
+
7
+ httpauth = Twitter::HTTPAuth.new(config['email'], config['password'])
8
+ base = Twitter::Base.new(httpauth)
9
+
10
+ pp base.lists('pengwynn')
11
+ pp base.list_members('pengwynn', 'rubyists')
@@ -0,0 +1,27 @@
1
+ require 'pp'
2
+ require 'pathname'
3
+ dir = Pathname(__FILE__).dirname.expand_path
4
+ require (dir + '..' + 'lib' + 'twitter').expand_path
5
+ require dir + 'helpers' + 'config_store'
6
+
7
+ config = ConfigStore.new("#{ENV['HOME']}/.twitter")
8
+ oauth = Twitter::OAuth.new(config['token'], config['secret'])
9
+ rtoken = oauth.request_token.token
10
+ rsecret = oauth.request_token.secret
11
+
12
+ puts "> redirecting you to twitter to authorize..."
13
+ %x(open #{oauth.request_token.authorize_url})
14
+
15
+ print "> what was the PIN twitter provided you with? "
16
+ pin = gets.chomp
17
+
18
+ begin
19
+ oauth.authorize_from_request(rtoken, rsecret, pin)
20
+
21
+ twitter = Twitter::Base.new(oauth)
22
+ twitter.user_timeline.each do |tweet|
23
+ puts "#{tweet.user.screen_name}: #{tweet.text}"
24
+ end
25
+ rescue OAuth::Unauthorized
26
+ puts "> FAIL!"
27
+ end
@@ -0,0 +1,15 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'twitter')
2
+ require 'pp'
3
+
4
+ search = Twitter::Search.new.from('jnunemaker')
5
+
6
+ puts '*'*50, 'First Run', '*'*50
7
+ search.each { |result| pp result }
8
+
9
+ puts '*'*50, 'Second Run', '*'*50
10
+ search.each { |result| pp result }
11
+
12
+ puts '*'*50, 'Parameter Check', '*'*50
13
+ pp Twitter::Search.new('#austineats').fetch().results.first
14
+ pp Twitter::Search.new('#austineats').page(2).fetch().results.first
15
+ pp Twitter::Search.new('#austineats').since(1412737343).fetch().results.first
@@ -0,0 +1,19 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'twitter')
2
+ require File.join(File.dirname(__FILE__), 'helpers', 'config_store')
3
+ require 'pp'
4
+
5
+ config = ConfigStore.new("#{ENV['HOME']}/.twitter")
6
+
7
+ oauth = Twitter::OAuth.new(config['token'], config['secret'])
8
+ oauth.authorize_from_access(config['atoken'], config['asecret'])
9
+
10
+ client = Twitter::Base.new(oauth)
11
+
12
+ pp client.friends_timeline
13
+ puts '*'*50
14
+
15
+ pp client.user_timeline
16
+ puts '*'*50
17
+
18
+ pp client.replies
19
+ puts '*'*50
@@ -0,0 +1,9 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'twitter')
2
+ require 'pp'
3
+
4
+
5
+ httpauth = Twitter::HTTPAuth.new('email', 'password', :api_endpoint => 'tumblr.com')
6
+ base = Twitter::Base.new(httpauth)
7
+
8
+ pp base.user_timeline
9
+ pp base.verify_credentials
@@ -0,0 +1,16 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'twitter')
2
+ require 'pp'
3
+
4
+ puts 'User', '*'*50
5
+ pp Twitter.user('jnunemaker')
6
+ pp Twitter.user('snitch_test')
7
+
8
+ puts 'Status', '*'*50
9
+ pp Twitter.status(1533815199)
10
+
11
+ puts 'Friend Ids', '*'*50
12
+ pp Twitter.friend_ids('jnunemaker')
13
+
14
+ puts 'Follower Ids', '*'*50
15
+ pp Twitter.follower_ids('jnunemaker')
16
+
@@ -0,0 +1,11 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'twitter')
2
+ require File.join(File.dirname(__FILE__), 'helpers', 'config_store')
3
+ require 'pp'
4
+
5
+ config = ConfigStore.new("#{ENV['HOME']}/.twitter")
6
+
7
+ oauth = Twitter::OAuth.new(config['token'], config['secret'])
8
+ oauth.authorize_from_access(config['atoken'], config['asecret'])
9
+
10
+ client = Twitter::Base.new(oauth)
11
+ pp client.update('This is an update from the twitter gem')
@@ -0,0 +1,5 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'twitter')
2
+ require 'pp'
3
+
4
+ pp Twitter.user('jnunemaker')
5
+ pp Twitter.user('snitch_test')
@@ -0,0 +1,156 @@
1
+ require "forwardable"
2
+ require "oauth"
3
+ require "hashie"
4
+ require "httparty"
5
+ require "yajl"
6
+
7
+ module Twitter
8
+ include HTTParty
9
+ API_VERSION = "1".freeze
10
+ format :json
11
+
12
+ class TwitterError < StandardError
13
+ attr_reader :data
14
+
15
+ def initialize(data)
16
+ @data = data
17
+ super
18
+ end
19
+ end
20
+
21
+ class RateLimitExceeded < TwitterError; end
22
+ class Unauthorized < TwitterError; end
23
+ class General < TwitterError; end
24
+
25
+ class Unavailable < StandardError; end
26
+ class InformTwitter < StandardError; end
27
+ class NotFound < StandardError; end
28
+
29
+ def self.api_endpoint
30
+ @api_endpoint ||= "api.twitter.com/#{API_VERSION}"
31
+ end
32
+
33
+ def self.api_endpoint=(value)
34
+ @api_endpoint = value
35
+ end
36
+
37
+ def self.firehose(options = {})
38
+ perform_get("/statuses/public_timeline.json")
39
+ end
40
+
41
+ def self.user(id,options={})
42
+ perform_get("/users/show/#{id}.json")
43
+ end
44
+
45
+ def self.status(id,options={})
46
+ perform_get("/statuses/show/#{id}.json")
47
+ end
48
+
49
+ def self.friend_ids(id,options={})
50
+ perform_get("/friends/ids/#{id}.json")
51
+ end
52
+
53
+ def self.follower_ids(id,options={})
54
+ perform_get("/followers/ids/#{id}.json")
55
+ end
56
+
57
+ def self.timeline(id, options={})
58
+ perform_get("/statuses/user_timeline/#{id}.json", :query => options)
59
+ end
60
+
61
+ # :per_page = max number of statues to get at once
62
+ # :page = which page of tweets you wish to get
63
+ def self.list_timeline(list_owner_username, slug, query = {})
64
+ perform_get("/#{list_owner_username}/lists/#{slug}/statuses.json", :query => query)
65
+ end
66
+
67
+ private
68
+
69
+ def self.perform_get(uri, options = {})
70
+ base_uri self.api_endpoint
71
+ make_friendly(get(uri, options))
72
+ end
73
+
74
+ def self.make_friendly(response)
75
+ raise_errors(response)
76
+ data = parse(response)
77
+ # Don't mash arrays of integers
78
+ if data && data.is_a?(Array) && data.first.is_a?(Integer)
79
+ data
80
+ else
81
+ mash(data)
82
+ end
83
+ end
84
+
85
+ def self.raise_errors(response)
86
+ case response.code.to_i
87
+ when 400
88
+ data = parse(response)
89
+ raise RateLimitExceeded.new(data), "(#{response.code}): #{response.message} - #{data['error'] if data}"
90
+ when 401
91
+ data = parse(response)
92
+ raise Unauthorized.new(data), "(#{response.code}): #{response.message} - #{data['error'] if data}"
93
+ when 403
94
+ data = parse(response)
95
+ raise General.new(data), "(#{response.code}): #{response.message} - #{data['error'] if data}"
96
+ when 404
97
+ raise NotFound, "(#{response.code}): #{response.message}"
98
+ when 500
99
+ raise InformTwitter, "Twitter had an internal error. Please let them know in the group. (#{response.code}): #{response.message}"
100
+ when 502..503
101
+ raise Unavailable, "(#{response.code}): #{response.message}"
102
+ end
103
+ end
104
+
105
+ def self.parse(response)
106
+ Yajl::Parser.parse(response.body)
107
+ end
108
+
109
+ def self.mash(obj)
110
+ if obj.is_a?(Array)
111
+ obj.map{|item| make_mash_with_consistent_hash(item)}
112
+ elsif obj.is_a?(Hash)
113
+ make_mash_with_consistent_hash(obj)
114
+ else
115
+ obj
116
+ end
117
+ end
118
+
119
+ # Lame workaround for the fact that mash doesn't hash correctly
120
+ def self.make_mash_with_consistent_hash(obj)
121
+ m = Hashie::Mash.new(obj)
122
+ def m.hash
123
+ inspect.hash
124
+ end
125
+ return m
126
+ end
127
+
128
+ end
129
+
130
+ module Hashie
131
+ class Mash
132
+
133
+ # Converts all of the keys to strings, optionally formatting key name
134
+ def rubyify_keys!
135
+ keys.each{|k|
136
+ v = delete(k)
137
+ new_key = k.to_s.underscore
138
+ self[new_key] = v
139
+ v.rubyify_keys! if v.is_a?(Hash)
140
+ v.each{|p| p.rubyify_keys! if p.is_a?(Hash)} if v.is_a?(Array)
141
+ }
142
+ self
143
+ end
144
+
145
+ end
146
+ end
147
+
148
+ directory = File.expand_path(File.dirname(__FILE__))
149
+
150
+ require File.join(directory, "twitter", "oauth")
151
+ require File.join(directory, "twitter", "httpauth")
152
+ require File.join(directory, "twitter", "request")
153
+ require File.join(directory, "twitter", "base")
154
+ require File.join(directory, "twitter", "search")
155
+ require File.join(directory, "twitter", "trends")
156
+ require File.join(directory, "twitter", "geo")
@@ -0,0 +1,393 @@
1
+ module Twitter
2
+ class Base
3
+ extend Forwardable
4
+
5
+ def_delegators :client, :get, :post, :put, :delete
6
+
7
+ attr_reader :client
8
+
9
+ def initialize(client)
10
+ @client = client
11
+ end
12
+
13
+ # Options: since_id, max_id, count, page
14
+ def home_timeline(query={})
15
+ perform_get("/statuses/home_timeline.json", :query => query)
16
+ end
17
+
18
+ # Options: since_id, max_id, count, page, since
19
+ def friends_timeline(query={})
20
+ perform_get("/statuses/friends_timeline.json", :query => query)
21
+ end
22
+
23
+ # Options: id, user_id, screen_name, since_id, max_id, page, since, count
24
+ def user_timeline(query={})
25
+ perform_get("/statuses/user_timeline.json", :query => query)
26
+ end
27
+
28
+ def status(id)
29
+ perform_get("/statuses/show/#{id}.json")
30
+ end
31
+
32
+ # Options: count
33
+ def retweets(id, query={})
34
+ perform_get("/statuses/retweets/#{id}.json", :query => query)
35
+ end
36
+
37
+ # Options: in_reply_to_status_id
38
+ def update(status, query={})
39
+ perform_post("/statuses/update.json", :body => {:status => status}.merge(query))
40
+ end
41
+
42
+ # DEPRECATED: Use #mentions instead
43
+ #
44
+ # Options: since_id, max_id, since, page
45
+ def replies(query={})
46
+ warn("DEPRECATED: #replies is deprecated by Twitter; use #mentions instead")
47
+ perform_get("/statuses/replies.json", :query => query)
48
+ end
49
+
50
+ # Options: since_id, max_id, count, page
51
+ def mentions(query={})
52
+ perform_get("/statuses/mentions.json", :query => query)
53
+ end
54
+
55
+ # Options: since_id, max_id, count, page
56
+ def retweeted_by_me(query={})
57
+ perform_get("/statuses/retweeted_by_me.json", :query => query)
58
+ end
59
+
60
+ # Options: since_id, max_id, count, page
61
+ def retweeted_to_me(query={})
62
+ perform_get("/statuses/retweeted_to_me.json", :query => query)
63
+ end
64
+
65
+ # Options: since_id, max_id, count, page
66
+ def retweets_of_me(query={})
67
+ perform_get("/statuses/retweets_of_me.json", :query => query)
68
+ end
69
+
70
+ # options: count, page, ids_only
71
+ def retweeters_of(id, options={})
72
+ ids_only = !!(options.delete(:ids_only))
73
+ perform_get("/statuses/#{id}/retweeted_by#{"/ids" if ids_only}.json", :query => options)
74
+ end
75
+
76
+ def status_destroy(id)
77
+ perform_post("/statuses/destroy/#{id}.json")
78
+ end
79
+
80
+ def retweet(id)
81
+ perform_post("/statuses/retweet/#{id}.json")
82
+ end
83
+
84
+ # Options: id, user_id, screen_name, page
85
+ def friends(query={})
86
+ perform_get("/statuses/friends.json", :query => query)
87
+ end
88
+
89
+ # Options: id, user_id, screen_name, page
90
+ def followers(query={})
91
+ perform_get("/statuses/followers.json", :query => query)
92
+ end
93
+
94
+ def user(id, query={})
95
+ perform_get("/users/show/#{id}.json", :query => query)
96
+ end
97
+
98
+ def users(*ids_or_usernames)
99
+ ids, usernames = [], []
100
+ ids_or_usernames.each do |id_or_username|
101
+ if id_or_username.is_a?(Integer)
102
+ ids << id_or_username
103
+ elsif id_or_username.is_a?(String)
104
+ usernames << id_or_username
105
+ end
106
+ end
107
+ query = {}
108
+ query[:user_id] = ids.join(",") unless ids.empty?
109
+ query[:screen_name] = usernames.join(",") unless usernames.empty?
110
+ perform_get("/users/lookup.json", :query => query)
111
+ end
112
+
113
+ # Options: page, per_page
114
+ def user_search(q, query={})
115
+ q = URI.escape(q)
116
+ perform_get("/users/search.json", :query => ({:q => q}.merge(query)))
117
+ end
118
+
119
+ # Options: since, since_id, page
120
+ def direct_messages(query={})
121
+ perform_get("/direct_messages.json", :query => query)
122
+ end
123
+
124
+ # Options: since, since_id, page
125
+ def direct_messages_sent(query={})
126
+ perform_get("/direct_messages/sent.json", :query => query)
127
+ end
128
+
129
+ def direct_message_create(user, text)
130
+ body = {:text => text}
131
+ body[:screen_name] = user.screen_name if user.respond_to?(:screen_name) && !user.screen_name.blank?
132
+ body[:id] = user.id if user.respond_to?(:id) && !user.id.blank?
133
+ perform_post("/direct_messages/new.json", :body => body)
134
+ end
135
+
136
+ def direct_message_destroy(id)
137
+ perform_post("/direct_messages/destroy/#{id}.json")
138
+ end
139
+
140
+ def friendship_create(id, follow=false)
141
+ body = {}
142
+ body.merge!(:follow => follow) if follow
143
+ perform_post("/friendships/create/#{id}.json", :body => body)
144
+ end
145
+
146
+ def friendship_destroy(id)
147
+ perform_post("/friendships/destroy/#{id}.json")
148
+ end
149
+
150
+ def friendship_exists?(a, b)
151
+ perform_get("/friendships/exists.json", :query => {:user_a => a, :user_b => b})
152
+ end
153
+
154
+ def friendship_show(query)
155
+ perform_get("/friendships/show.json", :query => query)
156
+ end
157
+
158
+ # Options: id, user_id, screen_name
159
+ def friend_ids(query={})
160
+ perform_get("/friends/ids.json", :query => query)
161
+ end
162
+
163
+ # Options: id, user_id, screen_name
164
+ def follower_ids(query={})
165
+ perform_get("/followers/ids.json", :query => query)
166
+ end
167
+
168
+ def verify_credentials
169
+ perform_get("/account/verify_credentials.json")
170
+ end
171
+
172
+ # Device must be sms, im or none
173
+ def update_delivery_device(device)
174
+ perform_post("/account/update_delivery_device.json", :body => {:device => device})
175
+ end
176
+
177
+ # One or more of the following must be present:
178
+ # profile_background_color, profile_text_color, profile_link_color,
179
+ # profile_sidebar_fill_color, profile_sidebar_border_color
180
+ def update_profile_colors(colors={})
181
+ perform_post("/account/update_profile_colors.json", :body => colors)
182
+ end
183
+
184
+ # file should respond to #read and #path
185
+ def update_profile_image(file)
186
+ perform_post("/account/update_profile_image.json", build_multipart_bodies(:image => file))
187
+ end
188
+
189
+ # file should respond to #read and #path
190
+ def update_profile_background(file, tile = false)
191
+ perform_post("/account/update_profile_background_image.json", build_multipart_bodies(:image => file).merge(:tile => tile))
192
+ end
193
+
194
+ def rate_limit_status
195
+ perform_get("/account/rate_limit_status.json")
196
+ end
197
+
198
+ # One or more of the following must be present:
199
+ # name, email, url, location, description
200
+ def update_profile(body={})
201
+ perform_post("/account/update_profile.json", :body => body)
202
+ end
203
+
204
+ # Options: id, page
205
+ def favorites(query={})
206
+ perform_get("/favorites.json", :query => query)
207
+ end
208
+
209
+ def favorite_create(id)
210
+ perform_post("/favorites/create/#{id}.json")
211
+ end
212
+
213
+ def favorite_destroy(id)
214
+ perform_post("/favorites/destroy/#{id}.json")
215
+ end
216
+
217
+ def enable_notifications(id)
218
+ perform_post("/notifications/follow/#{id}.json")
219
+ end
220
+
221
+ def disable_notifications(id)
222
+ perform_post("/notifications/leave/#{id}.json")
223
+ end
224
+
225
+ def block(id)
226
+ perform_post("/blocks/create/#{id}.json")
227
+ end
228
+
229
+ def unblock(id)
230
+ perform_post("/blocks/destroy/#{id}.json")
231
+ end
232
+
233
+ # When reporting a user for spam, specify one or more of id, screen_name, or user_id
234
+ def report_spam(options)
235
+ perform_post("/report_spam.json", :body => options)
236
+ end
237
+
238
+ def help
239
+ perform_get("/help/test.json")
240
+ end
241
+
242
+ def list_create(list_owner_username, options)
243
+ perform_post("/#{list_owner_username}/lists.json", :body => {:user => list_owner_username}.merge(options))
244
+ end
245
+
246
+ def list_update(list_owner_username, slug, options)
247
+ perform_put("/#{list_owner_username}/lists/#{slug}.json", :body => options)
248
+ end
249
+
250
+ def list_delete(list_owner_username, slug)
251
+ perform_delete("/#{list_owner_username}/lists/#{slug}.json")
252
+ end
253
+
254
+ def lists(list_owner_username = nil, query = {})
255
+ path = case list_owner_username
256
+ when nil, Hash
257
+ query = list_owner_username
258
+ "/lists.json"
259
+ else
260
+ "/#{list_owner_username}/lists.json"
261
+ end
262
+ perform_get(path, :query => query)
263
+ end
264
+
265
+ def list(list_owner_username, slug)
266
+ perform_get("/#{list_owner_username}/lists/#{slug}.json")
267
+ end
268
+
269
+ # :per_page = max number of statues to get at once
270
+ # :page = which page of tweets you wish to get
271
+ def list_timeline(list_owner_username, slug, query = {})
272
+ perform_get("/#{list_owner_username}/lists/#{slug}/statuses.json", :query => query)
273
+ end
274
+
275
+ def memberships(list_owner_username, query={})
276
+ perform_get("/#{list_owner_username}/lists/memberships.json", :query => query)
277
+ end
278
+
279
+ def subscriptions(list_owner_username, query = {})
280
+ perform_get("/#{list_owner_username}/lists/subscriptions.json", :query => query)
281
+ end
282
+
283
+ def list_members(list_owner_username, slug, query = {})
284
+ perform_get("/#{list_owner_username}/#{slug}/members.json", :query => query)
285
+ end
286
+
287
+ def list_add_member(list_owner_username, slug, new_id)
288
+ perform_post("/#{list_owner_username}/#{slug}/members.json", :body => {:id => new_id})
289
+ end
290
+
291
+ def list_remove_member(list_owner_username, slug, id)
292
+ perform_delete("/#{list_owner_username}/#{slug}/members.json", :query => {:id => id})
293
+ end
294
+
295
+ def is_list_member?(list_owner_username, slug, id)
296
+ perform_get("/#{list_owner_username}/#{slug}/members/#{id}.json").error.nil?
297
+ end
298
+
299
+ def list_subscribers(list_owner_username, slug)
300
+ perform_get("/#{list_owner_username}/#{slug}/subscribers.json")
301
+ end
302
+
303
+ def list_subscribe(list_owner_username, slug)
304
+ perform_post("/#{list_owner_username}/#{slug}/subscribers.json")
305
+ end
306
+
307
+ def list_unsubscribe(list_owner_username, slug)
308
+ perform_delete("/#{list_owner_username}/#{slug}/subscribers.json")
309
+ end
310
+
311
+ def blocked_ids
312
+ perform_get("/blocks/blocking/ids.json", :mash => false)
313
+ end
314
+
315
+ def blocking(options={})
316
+ perform_get("/blocks/blocking.json", options)
317
+ end
318
+
319
+ def saved_searches
320
+ perform_get("/saved_searches.json")
321
+ end
322
+
323
+ def saved_search(id)
324
+ perform_get("/saved_searches/show/#{id}.json")
325
+ end
326
+
327
+ def saved_search_create(query)
328
+ perform_post("/saved_searches/create.json", :body => {:query => query})
329
+ end
330
+
331
+ def saved_search_destroy(id)
332
+ perform_delete("/saved_searches/destroy/#{id}.json")
333
+ end
334
+
335
+ protected
336
+
337
+ def self.mime_type(file)
338
+ case
339
+ when file =~ /\.jpg/ then 'image/jpg'
340
+ when file =~ /\.gif$/ then 'image/gif'
341
+ when file =~ /\.png$/ then 'image/png'
342
+ else 'application/octet-stream'
343
+ end
344
+ end
345
+
346
+ def mime_type(f) self.class.mime_type(f) end
347
+
348
+ CRLF = "\r\n"
349
+
350
+ def self.build_multipart_bodies(parts)
351
+ boundary = Time.now.to_i.to_s(16)
352
+ body = ""
353
+ parts.each do |key, value|
354
+ esc_key = CGI.escape(key.to_s)
355
+ body << "--#{boundary}#{CRLF}"
356
+ if value.respond_to?(:read)
357
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"; filename=\"#{File.basename(value.path)}\"#{CRLF}"
358
+ body << "Content-Type: #{mime_type(value.path)}#{CRLF*2}"
359
+ body << value.read
360
+ else
361
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"#{CRLF*2}#{value}"
362
+ end
363
+ body << CRLF
364
+ end
365
+ body << "--#{boundary}--#{CRLF*2}"
366
+ {
367
+ :body => body,
368
+ :headers => {"Content-Type" => "multipart/form-data; boundary=#{boundary}"}
369
+ }
370
+ end
371
+
372
+ def build_multipart_bodies(parts) self.class.build_multipart_bodies(parts) end
373
+
374
+ private
375
+
376
+ def perform_get(path, options={})
377
+ Twitter::Request.get(self, path, options)
378
+ end
379
+
380
+ def perform_post(path, options={})
381
+ Twitter::Request.post(self, path, options)
382
+ end
383
+
384
+ def perform_put(path, options={})
385
+ Twitter::Request.put(self, path, options)
386
+ end
387
+
388
+ def perform_delete(path, options={})
389
+ Twitter::Request.delete(self, path, options)
390
+ end
391
+
392
+ end
393
+ end