module FireEagle
class Client
# TODO add access_token=() and request_token=() methods that check whether the tokens are usable
attr_reader :access_token, :request_token, :consumer, :format
# Initialize a FireEagle Client. Takes an options Hash.
#
# == Required keys:
#
# [:consumer_key] OAuth Consumer key representing your FireEagle Application
# [:consumer_secret] OAuth Consumer secret representing your FireEagle Application
#
# == Optional keys:
#
# [:request_token] OAuth Request Token, for use with convert_to_access_token
# [:request_token_secret] OAuth Request Token Secret, for use with convert_to_access_token
# [:access_token] OAuth Token, either User-specific or General-purpose
# [:access_token_secret] OAuth Token, either User-specific or General-purpose
# [:app_id] Your Mobile Application ID
# [:debug] Boolean
#
# User-specific OAuth tokens tie FireEagle users to your application. As such, they are intended to be
# distributed (with keys) to that user's mobile device and/or computer running your desktop or mobile client.
# For web-based applications User-specific tokens will be retrieved by your web server where they should be
# treated as private data. Take care to avoid releasing this data to the public, as the corresponding User's location
# information may be inadvertently exposed. User-specific OAuth tokens should be considered the property of
# your users.
#
# General-purpose OAuth tokens are tied to your application and allow you, as a developer, to make more
# general (often batch-style) queries against FireEagle. As a result, allowing this token/secret combination
# loose has the potential to reveal a much greater amount of personal data. In an attempt to mitigate this, we will
# only grant general-purpose tokens to web applications (contact us with details, if you seek an exception). In
# addition, we require developers to provide a restrictive IP range at registration time in order to further mitigate
# the risk of general-purpose tokens being used inappropriately.
#
# In general, OAuth tokens should be considered sacrosanct in order to help us respect our users' privacy. Please
# take this responsibility on as your own. If your Application Oauth tokens are compromised, FireEagle will
# turn off your application service until the problem is resolved.
#
# If the Client is initialized without an OAuth access token, it's assumed you're operating a non-web based application.
#
# == Non web-based applications
#
# For non web-based applications, such as a mobile client application, the authentication between the user and
# the application is slightly different. The request token is displayed to the user by the client application. The
# user then logs into the FireEagle website (using mobile_authorization_url) and enters this code to authorize the application.
# When the user finishes the authorization step the client application exchanges the request token for an access token
# (using convert_to_access_token). This is a lightweight method for non-web application users to authenticate an application
# without entering any identifying information into a potentially insecure application. Request tokens are valid for only
# 1 hour after being issued.
#
# == Example mobile-based authentication flow:
#
# Initialize a client with your consumer key, consumer secret, and your mobile application id:
#
# >> c = FireEagle::Client.new(:consumer_key => "key", :consumer_secret => "sekret", :app_id => 12345)
# => #
#
# Generate a request token:
#
# >> c.get_request_token
# => #
#
# Prompt your user to visit your app's mobile authorization url and enter ENTER_THIS_TOKEN:
#
# >> c.mobile_authorization_url
# => "http://fireeagle.yahoo.net/oauth/mobile_auth/12345"
#
# Once the user has indicated to you that they've done this, convert their request token to an access token:
#
# >> c.convert_to_access_token
# => #
#
# You're done!
def initialize(options = {})
options = {
:debug => false,
:format => FireEagle::FORMAT_XML
}.merge(options)
# symbolize keys
options.map do |k,v|
options[k.to_sym] = v
end
raise FireEagle::ArgumentError, "OAuth Consumer Key and Secret required" if options[:consumer_key].nil? || options[:consumer_secret].nil?
@consumer = OAuth::Consumer.new(options[:consumer_key], options[:consumer_secret], :site => FireEagle::API_SERVER, :authorize_url => FireEagle::AUTHORIZATION_URL)
@debug = options[:debug]
@format = options[:format]
@app_id = options[:app_id]
if options[:access_token] && options[:access_token_secret]
@access_token = OAuth::AccessToken.new(@consumer, options[:access_token], options[:access_token_secret])
else
@access_token = nil
end
if options[:request_token] && options[:request_token_secret]
@request_token = OAuth::RequestToken.new(@consumer, options[:request_token], options[:request_token_secret])
else
@request_token = nil
end
end
# Obtain an new unauthorized OAuth Request token
def get_request_token(force_token_regeneration = false)
if force_token_regeneration || @request_token.nil?
@request_token = consumer.get_request_token
end
@request_token
end
# Return the Fire Eagle authorization URL for your mobile application. At this URL, the User will be prompted for their request_token.
def mobile_authorization_url
raise FireEagle::ArgumentError, ":app_id required" if @app_id.nil?
"#{FireEagle::MOBILE_AUTH_URL}#{@app_id}"
end
# The URL the user must access to authorize this token. get_request_token must be called first. For use by web-based and desktop-based applications.
def authorization_url
raise FireEagle::ArgumentError, "call #get_request_token first" if @request_token.nil?
request_token.authorize_url
end
#Exchange an authorized OAuth Request token for an access token. For use by desktop-based and mobile applications.
def convert_to_access_token
raise FireEagle::ArgumentError, "call #get_request_token and have user authorize the token first" if @request_token.nil?
@access_token = request_token.get_access_token
end
# Disambiguates potential values for update query. Results from lookup can be passed to
# update to ensure that FireEagle will understand how to parse the Location Hash.
#
# All three Location methods (lookup, update, and within) accept a Location Hash.
#
# There is a specific order for looking up locations. For example, if you provide lat, lon, and address,
# FireEagle will use the the latitude and longitude geo-coordinates and ignore the address.
#
# Location Hash keys, in order of priority:
#
# [(:lat, :lon)] both required, valid values are floats of -180 to 180 for lat and -90 to 90 for lon
# [(:woeid)] Where on Earth ID
# [:place_id] Place ID (via Flickr/Upcomoing); deprecated in favor of WOE IDs when possible
# [:address] street address (may contain a full address, but will be combined with postal, city, state, and country when available)
# [(:mnc, :mcc, :lac, :cellid)] cell tower information, all required (as integers) for a valid tower location
# [:postal] a ZIP or postal code (combined with address, city, state, and country when available)
# [:city] city (combined with address, postal, state, and country when available)
# [:state] state (combined with address, postal, city, and country when available)
# [:country] country (combined with address, postal, city, and state when available)
# [:q] Free-text fallback containing user input. Lat/lon pairs and geometries will be extracted if possible, otherwise this string will be geocoded as-is.
#
# Not yet supported:
#
# * upcoming_venue_id
# * yahoo_local_id
# * plazes_id
def lookup(params)
raise FireEagle::ArgumentError, "OAuth Access Token Required" unless @access_token
response = get(FireEagle::LOOKUP_API_PATH, :params => params)
FireEagle::Response.parse(response.body).locations
end
# Sets a User's current Location using using a Place ID hash or a set of Location parameters. If the User
# provides a Location unconfirmed with lookup then FireEagle makes a best guess as to the User's Location.
#
# All three Location methods (lookup, update, and within) accept a Location Hash.
#
# There is a specific order for looking up locations. For example, if you provide lat, lon, and address,
# FireEagle will use the the latitude and longitude geo-coordinates and ignore the address.
#
# Location Hash keys, in order of priority:
#
# [(:lat, :lon)] both required, valid values are floats of -180 to 180 for lat and -90 to 90 for lon
# [:place_id] Place ID - valid values decrypts to an integer value
# [:address] street address (may contain a full address, but will be combined with postal, city, state, and country when available)
# [(:mnc, :mcc, :lac, :cellid)] cell tower information, all required (as integers) for a valid tower location
# [:postal] a ZIP or postal code (combined with address, city, state, and country when available)
# [:city] city (combined with address, postal, state, and country when available)
# [:state] state (combined with address, postal, city, and country when available)
# [:country] country (combined with address, postal, city, and state when available)
# [:q] Free-text fallback containing user input. Lat/lon pairs and geometries will be extracted if possible, otherwise this string will be geocoded as-is.
#
# Not yet supported:
#
# * upcoming_venue_id
# * yahoo_local_id
# * plazes_id
def update(location = {})
raise FireEagle::ArgumentError, "OAuth Access Token Required" unless @access_token
location = sanitize_location_hash(location)
response = post(FireEagle::UPDATE_API_PATH, :params => location)
FireEagle::Response.parse(response.body)
end
# Returns the Location of a User.
def user
raise FireEagle::ArgumentError, "OAuth Access Token Required" unless @access_token
response = get(FireEagle::USER_API_PATH)
FireEagle::Response.parse(response.body).users.first
end
alias_method :location, :user
# Query for Users of an Application who have updated their Location recently. Returns a list of
# Users for the Application with recently updated locations.
#
# == Optional parameters:
#
# time The time to start looking at recent updates from. Value is flexible, supported forms are 'now', 'yesterday', '12:00', '13:00', '1:00pm' and '2008-03-12 12:34:56'. (default: 'now')
# count Number of users to return per page. (default: 10)
# start The page number at which to start returning the list of users.
def recent(time = 'now', count = 10, start = 1)
raise FireEagle::ArgumentError, "OAuth Access Token Required" unless @access_token
params = { :count => count, :start => start, :time => time }
response = get(FireEagle::RECENT_API_PATH, :params => params)
FireEagle::Response.parse(response.body).users
end
# Takes a Place ID or a Location and returns a list of users of your application who are within the bounding box of that Location.
#
# Location Hash keys, in order of priority:
#
# [(:lat, :lon)] both required, valid values are floats of -180 to 180 for lat and -90 to 90 for lon
# [:woeid] Where on Earth ID
# [:place_id] Place ID
# [:address] street address (may contain a full address, but will be combined with postal, city, state, and country when available)
# [(:mnc, :mcc, :lac, :cellid)] cell tower information, all required (as integers) for a valid tower location
# [:postal] a ZIP or postal code (combined with address, city, state, and country when available)
# [:city] city (combined with address, postal, state, and country when available)
# [:state] state (combined with address, postal, city, and country when available)
# [:country] country (combined with address, postal, city, and state when available)
# [:q] Free-text fallback containing user input. Lat/lon pairs and geometries will be extracted if possible, otherwise this string will be geocoded as-is.
#
# Not yet supported:
#
# * upcoming_venue_id
# * yahoo_local_id
# * plazes_id
def within(location = {}, count = 10, start = 1)
raise FireEagle::ArgumentError, "OAuth Access Token Required" unless @access_token
location = sanitize_location_hash(location)
params = { :count => count, :start => start }.merge(location)
response = get(FireEagle::WITHIN_API_PATH, :params => params)
FireEagle::Response.parse(response.body).users
end
protected
def sanitize_location_hash(location)
location.map do |k,v|
location[k.to_sym] = v
end
location = location.reject { |key, value| !FireEagle::UPDATE_PARAMS.include?(key) }
raise FireEagle::ArgumentError, "Requires all or none of :lat, :lon" unless location.has_all_or_none_keys?(:lat, :lon)
raise FireEagle::ArgumentError, "Requires all or none of :mnc, :mcc, :lac, :cellid" unless location.has_all_or_none_keys?(:mnc, :mcc, :lac, :cellid)
location
end
def get(url, options = {}) #:nodoc:
request(:get, url, options)
end
def post(url, options = {}) #:nodoc:
request(:post, url, options)
end
def request(method, url, options) #:nodoc:
response = case method
when :post
access_token.request(:post, "#{url}.#{format}", options[:params])
when :get
qs = options[:params].collect { |k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join("&") if options[:params]
access_token.request(:get, "#{url}.#{format}?#{qs}")
else
raise ArgumentError, "method #{method} not supported"
end
case response.code
when '404'; then raise FireEagle::FireEagleException, "Not Found"
when '500'; then raise FireEagle::FireEagleException, "Internal Server Error"
else response
end
end
end
end