foursquare 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/History +7 -0
  2. data/README.rdoc +84 -42
  3. data/Rakefile +3 -3
  4. data/lib/foursquare.rb +203 -174
  5. metadata +4 -3
data/History CHANGED
@@ -1,3 +1,10 @@
1
+ == 0.3.0 March 5, 2010
2
+ * oauth support
3
+ * method_missing api mapping
4
+
5
+ == 0.2.1 February 17th, 2010
6
+ * added post_install_message
7
+
1
8
  == 0.2.0 February 7th, 2010
2
9
  * updated methods to reflect updates to Foursquare API
3
10
  * added validations
@@ -1,51 +1,93 @@
1
1
  == foursquare
2
2
 
3
- A simple Ruby Gem wrapper for the Foursquare API.
3
+ A simple Ruby Gem wrapper for the Foursquare API. With OAuth authentication.
4
4
 
5
5
  == install
6
-
7
- gem install foursquare
8
-
6
+
7
+ gem install foursquare
8
+
9
9
  == example
10
-
11
- require 'rubygems'
12
- require 'foursquare'
13
10
 
14
- # foursquare's site uses email as the user name
15
- fq = Foursquare.new('username_or_phone','password')
16
-
17
- fq.test
18
-
19
- fq.venues(geolat, geolong, {:limit=>10,:q=>'pizza'})
20
- fq.tips(geolat,geolong,{:limit=>10})
21
- fq.checkins({:geolat=>'',:geolong=>''})
22
- fq.checkin(vid,venue,shout,{:private=>0,:twitter=>0,:facebook=>1,:geolat=>'12.03',:geolong=>'-123.33'})
23
- fq.history({:limit=>10})
24
- fq.user_details(user_id,{:badges=>0,:mayor=>0})
25
- fq.friends({:uid=>99999})
26
- fq.venue_details(venue_id)
27
- fq.add_venue(name,address,cross_street,city,state,options)
28
- fq.propose_edit(venue_id,name,address,cross_street,city,state,options)
29
- fq.flag_venue_as_closed(venue_id)
30
- fq.add_tip(venue_id,text,{:type=type})
31
- fq.mark_tip_as_todo(tid)
32
- fq.mark_tip_as_done(tid)
33
- fq.friend_requests
34
- fq.friend_approve(uid)
35
- fq.friend_deny(uid)
36
- fq.request_friend(uid)
37
- fq.find_friends_by_name('Bugs Bunny')
38
- fq.find_friends_by_phone('8675309')
39
- fq.find_friends_by_twitter('github')
40
- fq.set_pings('self','goodnight')
41
-
42
-
43
- # Foursquare is moving the API to geolat/geolong and removing the concept of cities
44
- fq.cities
45
- fq.check_city(geolat, geolong)
46
- fq.switch_city(city_id)
47
- fq.friend_checkins({:geolat=>'',:geolong=>''})
48
-
11
+ require 'rubygems'
12
+ require 'foursquare'
13
+
14
+ oauth_key 'your_key'
15
+ oauth_secret = 'your_secret'
16
+
17
+ oauth = Foursquare::OAuth.new(oauth_key, oauth_secret)
18
+
19
+ request_token = oauth.request_token.token
20
+ request_secret = oauth.request_token.secret
21
+
22
+ # redirecting user to foursquare to authorize
23
+ oauth.request_token.authorize_url
24
+
25
+ # foursquare redirects back to your callback url, passing the verifier in the url params
26
+
27
+ access_token, access_secret = oauth.authorize_from_request(request_token, request_secret, verifier)
28
+
29
+ # save the user's access token and secret
30
+
31
+
32
+ oauth = Foursquare::OAuth.new(oauth_key, oauth_secret)
33
+ oauth.authorize_from_access(access_token, access_secret)
34
+ foursquare = Foursquare::Base.new(oauth)
35
+
36
+ foursquare.test
37
+
38
+ foursquare.venues :geolat => geolat, :geolong => geolong, :limit => 10, :q => 'pizza'
39
+ foursquare.tips :geolat => geolat, :geolong => geolong, :limit => 10
40
+ foursquare.checkins :geolat => geolat, :geolong => geolong
41
+
42
+ checkin = {
43
+ :vid => vid,
44
+ :shout => "this is what i'm up to",
45
+ :venue => "Cohabitat",
46
+ :private => 0,
47
+ :twitter => 0,
48
+ :geolat => geolat,
49
+ :geolong => geolong
50
+ }
51
+
52
+ # these all do the same thing
53
+ # the response is a hashie object built from the checkin json. so you can do new_checkin.shout
54
+ new_checkin = foursquare.checkin(checkin)
55
+ new_checkin.class
56
+ => Hashie::Mash
57
+ new_checkin
58
+ => {...checkin hashie...}
59
+ new_checkin = foursquare.send('checkin=', checkin)
60
+ new_checkin.class
61
+ => Hash
62
+ new_checkin
63
+ => {'checkin' => {...}}
64
+ new_checkin = foursquare.api(:checkin=, checkin)
65
+ new_checkin.class
66
+ => Hashie::Mash
67
+ new_checkin
68
+ => {:checkin => {...}}
69
+
70
+ foursquare.history :limit => 10
71
+ foursquare.api(:history, :limit => 10).checkins
72
+ foursquare.user :uid => user_id :badges => 0
73
+ foursquare.user # currently authenticated user
74
+ foursquare.friends :uid => 99999
75
+ foursquare.venue :vid => venue_id
76
+ foursquare.addvenue :name => name, :address => address, :city => city, ...
77
+ foursquare.venue_proposeedit :venue_id => venue_id, :name => name, :address => address, :city => ...
78
+ foursquare.venue_flagclosed :vid => venue_id
79
+ foursquare.addtip :vid => 12345, :tip => 'here is a tip'
80
+ foursquare.tip_marktodo :tid => tip_id
81
+ foursquare.tip_markdone :tid => tip_id
82
+ foursquare.friend_requests
83
+ foursquare.friend_approve :uid => friend_id
84
+ foursquare.friend_deny :uid => friend_id
85
+ foursquare.friend_sendrequest :uid => friend_id
86
+ foursquare.findfriends_byname :q => search_string
87
+ foursquare.findfriends_byphone :q => '555 123'
88
+ foursquare.findfriends_bytwitter :q => twitter_name
89
+ foursquare.settings_setping :uid => user_id, :self => global_ping_status
90
+
49
91
  == license
50
92
 
51
93
  (the MIT license)
data/Rakefile CHANGED
@@ -6,12 +6,12 @@ require 'spec/rake/spectask'
6
6
 
7
7
  spec = Gem::Specification.new do |s|
8
8
  s.name = "foursquare"
9
- s.version = "0.2.1"
10
- s.authors = ['Jeremy Welch', 'Thomas Hanley']
9
+ s.version = "0.3.0"
10
+ s.authors = ['Jeremy Welch', 'Thomas Hanley', 'Elise Wood']
11
11
  s.email = "hello@jeremyrwelch.com"
12
12
  s.homepage = "http://foursquare.rubyforge.org"
13
13
  s.description = s.summary = "A simple Ruby wrapper for the Foursquare API"
14
- s.post_install_message = "NOTE: This version of the Foursquare Gem has significant changes to the way methods are called. Please review the examples in the README"
14
+ s.post_install_message = "NOTE: This version of the Foursquare Gem has added OAuth support. Basic Auth has been deprecated. Also significant changes have bee made to the way methods are called. Please review the examples in the README."
15
15
 
16
16
  s.platform = Gem::Platform::RUBY
17
17
  s.has_rdoc = true
@@ -1,197 +1,226 @@
1
1
  require 'rubygems'
2
2
  require 'httparty'
3
+ require 'hashie'
4
+ require 'oauth'
3
5
 
4
- class Foursquare
5
- # Current Version of the Foursquare API: http://groups.google.com/group/foursquare-api/web/api-documentation
6
-
7
- include HTTParty
8
- base_uri 'api.foursquare.com'
9
- format :xml
10
-
11
- # auth user
12
- # TODO: add OAuth support (follow Twitter gem from jnunemaker http://github.com/jnunemaker/twitter)
13
- def initialize(user="",pass="", options={})
14
- self.class.basic_auth user, pass
15
- self.class.headers(options[:headers]) if options.has_key?(:headers)
16
- end
17
-
18
- # =========================
19
- # = No Auth Required Methods =
20
- # =========================
21
- # test for response from Foursquare
22
- def test
23
- self.class.get("/v1/test")
24
- end
25
-
26
- def venues(geolat, geolong, options={})
27
- options.merge!({:geolat=>geolat, :geolong=>geolong})
28
- self.class.get("/v1/venues", :query=>options)
29
- end
6
+ Hash.send :include, Hashie::HashExtensions
30
7
 
31
- def tips(geolat,geolong,options={})
32
- options.merge!({:geolat=>geolat, :geolong=>geolong})
33
- self.class.get("/v1/tips", :query=>options)
34
- end
35
8
 
36
- # =========================
37
- # = Auth Required Methods =
38
- # =========================
39
- def checkins(options={})
40
- self.class.get("/v1/checkins",:query=>options)
41
- end
42
9
 
43
- def checkin(vid=nil,venue=nil,shout=nil,options={})
44
- unless vid || venue || shout
45
- raise ArgumentError, "A vid or venue or shout is required to checkin", caller
10
+ module Foursquare
11
+ class OAuth
12
+ def initialize(ctoken, csecret, options={})
13
+ @consumer_token, @consumer_secret = ctoken, csecret
46
14
  end
47
- options.merge!({:vid=>vid, :venue=>venue, :shout=>shout})
48
- self.class.post("/v1/checkin", :body => options)
49
- end
50
15
 
51
- def history(options={})
52
- limit = options.delete(:limit) || 10
53
- uri = "/v1/history?l=#{limit}"
54
- sinceid = options.delete(:sinceid)
55
- uri << "&sinceid=#{sinceid}" unless sinceid.nil?
56
- self.class.get(uri)
57
- end
16
+ def consumer
17
+ return @consumer if @consumer
18
+ @consumer = ::OAuth::Consumer.new(@consumer_token, @consumer_secret, {
19
+ :site => "http://foursquare.com",
20
+ :scheme => :header,
21
+ :http_method => :post,
22
+ :request_token_path => "/oauth/request_token",
23
+ :access_token_path => "/oauth/access_token",
24
+ :authorize_path => "/oauth/authorize",
25
+ :proxy => (ENV['HTTP_PROXY'] || ENV['http_proxy'])
26
+ })
27
+ end
58
28
 
59
- def user_details(user_id,options={})
60
- unless user_id
61
- raise ArgumentError, "A user_id is required to get details about a user", caller
62
- end
63
- self.class.get("/v1/user",:query=>options)
64
- end
29
+ def set_callback_url(url)
30
+ clear_request_token
31
+ request_token(:oauth_callback => url)
32
+ end
33
+
34
+ def request_token(options={})
35
+ @request_token ||= consumer.get_request_token(options)
36
+ end
65
37
 
66
- def friends(options={})
67
- self.class.get("/v1/friends",:query=>options)
68
- end
38
+ def authorize_from_request(request_token, request_secret, verifier)
39
+ request_token = ::OAuth::RequestToken.new(consumer, request_token, request_secret)
40
+ access_token = request_token.get_access_token(:oauth_verifier => verifier)
41
+ @atoken, @asecret = access_token.token, access_token.secret
42
+ end
69
43
 
70
- def venue_details(venue_id)
71
- self.class.get("/v1/venue?vid=#{venue_id}")
72
- end
73
-
74
- # city_id has been removed from API
75
- def add_venue(name,address,cross_street,city,state,options={})
76
- unless name && address && cross_street && city && state
77
- raise ArgumentError, "A venue's name, address, cross_street, city, state are required to add_venue", caller
78
- end
79
- options.merge!({:name=>name, :address=>address, :cross_street=>cross_street, :city=>city, :state=>state})
80
- self.class.post("/v1/addvenue", :body => options)
81
- end
44
+ def access_token
45
+ @access_token ||= ::OAuth::AccessToken.new(consumer, @atoken, @asecret)
46
+ end
82
47
 
83
- def propose_edit(venue_id,name,address,cross_street,city,state,options={})
84
- unless venue_id && name && address && cross_street && city && state
85
- raise ArgumentError, "A venue's venue_id, name, address, cross_street, city, state are required to propose_edit", caller
48
+ def authorize_from_access(atoken, asecret)
49
+ @atoken, @asecret = atoken, asecret
86
50
  end
87
- options.merge!({:venue_id=>venue_id,:name=>name, :address=>address, :cross_street=>cross_street, :city=>city, :state=>state})
88
- self.class.post("/v1/venue/proposeedit", :body => options)
89
- end
90
-
91
- def flag_venue_as_closed(venue_id)
92
- unless venue_id
93
- raise ArgumentError, "A venue's venue_id is required to flag as closed", caller
51
+
52
+ private
53
+
54
+ def clear_request_token
55
+ @request_token = nil
94
56
  end
95
- self.class.post("/v1/venue/flagclosed?vid=#{venue_id}")
96
- end
97
-
98
- # ===============
99
- # = TIP methods =
100
- # ===============
101
- def add_tip(venue_id,text,options={})
102
- unless venue_id && text
103
- raise ArgumentError, "venue_id and text are required to add_tip", caller
104
- end
105
- options.merge!({:vid=>venue_id, :text=>text})
106
- self.class.post("/v1/addtip", :body => options)
107
57
  end
108
58
 
109
- def mark_tip_as_todo(tid)
110
- unless tid
111
- raise ArgumentError, "tip_id is required to mark tip as todo", caller
112
- end
113
- self.class.post("/v1/tip/marktodo", :body => {:tip=>tip})
114
- end
115
-
116
- def mark_tip_as_done(tid)
117
- unless tid
118
- raise ArgumentError, "tid is required to mark tip as done", caller
119
- end
120
- self.class.post("/v1/tip/markdone", :body => {:tip=>tip})
121
- end
122
-
123
- # ==================
124
- # = FRIEND Methods =
125
- # ==================
126
- def friend_requests
127
- self.class.get("/v1/friend/requests")
128
- end
129
-
130
- def friend_approve(uid)
131
- unless uid
132
- raise ArgumentError, "uid is required to approve friend request", caller
59
+ class Base
60
+ BASE_URL = 'http://api.foursquare.com/v1'
61
+ FORMAT = 'json'
62
+
63
+ attr_accessor :oauth
64
+
65
+ def initialize(oauth)
66
+ @oauth = oauth
133
67
  end
134
- self.class.post("/v1/friend/requests", :body => {:uid=>uid})
135
- end
136
-
137
- def friend_deny(uid)
138
- unless uid
139
- raise ArgumentError, "uid is required to deny friend request", caller
68
+
69
+ #
70
+ # Foursquare API: http://groups.google.com/group/foursquare-api/web/api-documentation
71
+ #
72
+ # .test # api test method
73
+ # => {'response': 'ok'}
74
+ # .checkin = {:shout => 'At home. Writing code'} # post new check in
75
+ # => {...checkin hash...}
76
+ # .history # authenticated user's checkin history
77
+ # => [{...checkin hashie...}, {...another checkin hashie...}]
78
+ # .send('venue.flagclosed=', {:vid => 12345}) # flag venue 12345 as closed
79
+ # => {'response': 'ok'}
80
+ # .venue_flagclosed = {:vid => 12345}
81
+ # => {'response': 'ok'}
82
+ #
83
+ # Assignment methods(POSTs) always return a hash. Annoyingly Ruby always returns what's on
84
+ # the right side of the assignment operator. So there are some wrapper methods below
85
+ # for POSTs that make sure it gets turned into a hashie
86
+ #
87
+ def method_missing(method_symbol, params = {})
88
+ method_name = method_symbol.to_s.split(/\.|_/).join('/')
89
+
90
+ if (method_name[-1,1]) == '='
91
+ method = method_name[0..-2]
92
+ result = post(api_url(method), params)
93
+ params.replace(result[method] || result)
94
+ else
95
+ result = get(api_url(method_name, params))
96
+ result[method_name] || result
97
+ end
140
98
  end
141
- self.class.post("/v1/friend/deny", :body => {:uid=>uid})
142
- end
143
-
144
- def request_friend(uid)
145
- unless uid
146
- raise ArgumentError, "uid is required to request friend", caller
99
+
100
+ def api(method_symbol, params = {})
101
+ Hashie::Mash.new(method_missing(method_symbol, params))
102
+ end
103
+
104
+ def api_url(method_name, options = nil)
105
+ params = options.is_a?(Hash) ? to_query_params(options) : options
106
+ params = nil if params and params.blank?
107
+ url = BASE_URL + '/' + method_name.split('.').join('/')
108
+ url += ".#{FORMAT}"
109
+ url += "?#{params}" if params
110
+ url
111
+ end
112
+
113
+ def parse_response(response)
114
+ raise_errors(response)
115
+ Crack::JSON.parse(response.body)
116
+ end
117
+
118
+ def to_query_params(options)
119
+ options.collect { |key, value| "#{key}=#{value}" }.join('&')
120
+ end
121
+
122
+ def get(url)
123
+ parse_response(@oauth.access_token.get(url))
124
+ end
125
+
126
+ def post(url, body)
127
+ parse_response(@oauth.access_token.post(url, body))
128
+ end
129
+
130
+ # API method wrappers
131
+
132
+ def checkin(params = {})
133
+ api(:checkin=, params).checkin
134
+ end
135
+
136
+ def history(params = {})
137
+ api(:history, params).checkins
138
+ end
139
+
140
+ def addvenue(params = {})
141
+ api(:addvenue=, params).venue
142
+ end
143
+
144
+ def venue_proposeedit(params = {})
145
+ api(:venue_proposeedit=, params)
146
+ end
147
+
148
+ def venue_flagclosed(params = {})
149
+ api(:venue_flagclosed=, params)
150
+ end
151
+
152
+ def addtip(params = {})
153
+ api(:addtip=, params).tip
154
+ end
155
+
156
+ def tip_marktodo(params = {})
157
+ api(:tip_marktodo=, params).tip
158
+ end
159
+
160
+ def tip_markdone(params = {})
161
+ api(:tip_markdone=, params).tip
162
+ end
163
+
164
+ def friend_requests
165
+ api(:friend_requests).requests
166
+ end
167
+
168
+ def friend_approve(params = {})
169
+ api(:friend_approve=, params).user
170
+ end
171
+
172
+ def friend_deny(params = {})
173
+ api(:friend_deny=, params).user
147
174
  end
148
- self.class.post("/v1/friend/sendrequest", :body => {:uid=>uid})
149
- end
150
-
151
- # ================
152
- # = FIND Methods =
153
- # ================
154
- def find_friends_by_name(q)
155
- self.class.get("/v1/findfriends/byname", :query=>{:q=>q})
156
- end
157
-
158
- def find_friends_by_phone(q)
159
- self.class.get("/v1/findfriends/byphone", :query=>{:q=>q})
160
- end
161
-
162
- def find_friends_by_twitter(q)
163
- self.class.get("/v1/findfriends/bytwitter", :query=>{:q=>q})
164
- end
165
175
 
166
- # ===============
167
- # = SET methods =
168
- # ===============
169
- def set_pings(user_id=nil,ping=nil)
170
- uid = user_id || "self"
171
- self.class.post("/v1/settings/setpings?#{uid}=#{ping}")
176
+ def friend_sendrequest(params = {})
177
+ api(:friend_sendrequest=, params).user
178
+ end
179
+
180
+ def findfriends_byname(params = {})
181
+ api(:findfriends_byname, params).users
182
+ end
183
+
184
+ def findfriends_byphone(params = {})
185
+ api(:findfriends_byphone, params).users
186
+ end
187
+
188
+ def findfriends_bytwitter(params = {})
189
+ api(:findfriends_bytwitter, params).users
190
+ end
191
+
192
+ def settings_setpings(params = {})
193
+ api(:settings_setpings=, params).settings
194
+ end
195
+
196
+ private
197
+
198
+
199
+ def raise_errors(response)
200
+ message = "(#{response.code}): #{response.message} - #{response.inspect} - #{response.body}"
201
+
202
+ case response.code.to_i
203
+ when 400
204
+ raise BadRequest, message
205
+ when 401
206
+ raise Unauthorized, message
207
+ when 403
208
+ raise General, message
209
+ when 404
210
+ raise NotFound, message
211
+ when 500
212
+ raise InternalError, "Foursquare had an internal error. Please let them know in the group.\n#{message}"
213
+ when 502..503
214
+ raise Unavailable, message
215
+ end
216
+ end
172
217
  end
173
218
 
174
- # From Foursquare API
175
- # 20100108 - naveen - the concept of cities (cityid and all city-related methods) has now been removed
176
- # Deprecated
177
- def cities
178
- self.class.get("/v1/cities")
179
- end
180
- # Deprecated
181
- def check_city(geolat, geolong)
182
- $stderr.puts "`check_city` Deprecated: The idea of \"cityid\" is now deprecated from the API"
183
- self.class.get("/v1/checkcity?geolat=#{geolat}&geolong=#{geolong}")
184
- end
185
- # Deprecated
186
- def switch_city(city_id)
187
- $stderr.puts "`switch_city` Deprecated: The idea of \"cityid\" is now deprecated from the API"
188
- self.class.post("/v1/switchcity", :body => {:cityid => city_id})
189
- end
190
- # Method changed to call checkins to match API
191
- def friend_checkins(options={})
192
- $stderr.puts "`friend_checkins` now calls `checkins` to match the foursquare api method call"
193
- $stderr.puts "http://groups.google.com/group/foursquare-api/web/api-documentation"
194
- checkins(options)
195
- end
196
219
 
197
- end
220
+ class BadRequest < StandardError; end
221
+ class Unauthorized < StandardError; end
222
+ class General < StandardError; end
223
+ class Unavailable < StandardError; end
224
+ class InternalError < StandardError; end
225
+ class NotFound < StandardError; end
226
+ end
metadata CHANGED
@@ -1,16 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foursquare
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Welch
8
8
  - Thomas Hanley
9
+ - Elise Wood
9
10
  autorequire: foursquare
10
11
  bindir: bin
11
12
  cert_chain: []
12
13
 
13
- date: 2010-02-17 00:00:00 -05:00
14
+ date: 2010-03-13 00:00:00 -05:00
14
15
  default_executable:
15
16
  dependencies:
16
17
  - !ruby/object:Gem::Dependency
@@ -45,7 +46,7 @@ has_rdoc: true
45
46
  homepage: http://foursquare.rubyforge.org
46
47
  licenses: []
47
48
 
48
- post_install_message: "NOTE: This version of the Foursquare Gem has significant changes to the way methods are called. Please review the examples in the README"
49
+ post_install_message: "NOTE: This version of the Foursquare Gem has added OAuth support. Basic Auth has been deprecated. Also significant changes have bee made to the way methods are called. Please review the examples in the README."
49
50
  rdoc_options: []
50
51
 
51
52
  require_paths: