require 'contacts'
require 'hpricot'
require 'md5'
require 'net/https'
require 'uri'
require 'json'
module Contacts
# = How I can fetch Yahoo Contacts?
# To gain access to a Yahoo user's data in the Yahoo Address Book Service,
# a third-party developer first must ask the owner for permission. You must
# do that through Yahoo Browser Based Authentication (BBAuth).
#
# This library give you access to Yahoo BBAuth and Yahoo Address Book API.
# Just follow the steps below and be happy!
#
# === Registering your app
# First of all, follow the steps in this
# page[http://developer.yahoo.com/wsregapp/] to register your app. If you need
# some help with that form, you can get it
# here[http://developer.yahoo.com/auth/appreg.html]. Just two tips: inside
# Required access scopes in that registration form, choose
# Yahoo! Address Book with Read Only access. Inside
# Authentication method choose Browser Based Authentication.
#
# === Configuring your Yahoo YAML
# After registering your app, you will have an application id and a
# shared secret. Use their values to fill in the config/contacts.yml
# file.
#
# === Authenticating your user and fetching his contacts
#
# yahoo = Contacts::Yahoo.new
# auth_url = yahoo.get_authentication_url
#
# Use that *auth_url* to redirect your user to Yahoo BBAuth. He will authenticate
# there and Yahoo will redirect to your application entrypoint URL (that you provided
# while registering your app with Yahoo). You have to get the path of that
# redirect, let's call it path (if you're using Rails, you can get it through
# request.request_uri, in the context of an action inside ActionController)
#
# Now, to fetch his contacts, just do this:
#
# contacts = wl.contacts(path)
# #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
# ['William Paginate', 'will.paginate@gmail.com'], ...
# ]
#--
# This class has two responsibilities:
# 1. Access the Yahoo Address Book API through Delegated Authentication
# 2. Import contacts from Yahoo Mail and deliver it inside an Array
#
class Yahoo
AUTH_DOMAIN = "https://api.login.yahoo.com"
AUTH_PATH = "/WSLogin/V1/wslogin?appid=#appid&ts=#ts"
CREDENTIAL_PATH = "/WSLogin/V1/wspwtoken_login?appid=#appid&ts=#ts&token=#token"
ADDRESS_BOOK_DOMAIN = "address.yahooapis.com"
ADDRESS_BOOK_PATH = "/v1/searchContacts?format=json&fields=all&appid=#appid&WSSID=#wssid"
CONFIG_FILE = File.dirname(__FILE__) + '/../config/contacts.yml'
attr_reader :appid, :secret, :token, :wssid, :cookie
# Initialize a new Yahoo object.
#
# ==== Paramaters
# * config_file :: The contacts YAML config file name
#--
# You can check an example of a config file inside config/ directory
#
def initialize(config_file=CONFIG_FILE)
confs = YAML.load_file(config_file)['yahoo']
@appid = confs['appid']
@secret = confs['secret']
end
# Yahoo Address Book API need to authenticate the user that is giving you
# access to his contacts. To do that, you must give him a URL. This method
# generates that URL. The user must access that URL, and after he has done
# authentication, hi will be redirected to your application.
#
def get_authentication_url(appdata= nil)
path = AUTH_PATH.clone
path.sub!(/#appid/, @appid)
timestamp = Time.now.utc.to_i
path.sub!(/#ts/, timestamp.to_s)
path<< "&appdata=#{appdata}" unless appdata.nil?
signature = MD5.hexdigest(path + @secret)
"#{AUTH_DOMAIN}#{path}&sig=#{signature}"
end
# This method return the user's contacts inside an Array in the following
# format:
#
# [
# ['Brad Fitzgerald', 'fubar@gmail.com'],
# [nil, 'nagios@hotmail.com'],
# ['William Paginate', 'will.paginate@yahoo.com'] ...
# ]
#
# ==== Paramaters
# * path :: The path of the redirect request that Yahoo sent to you
# after authenticating the user
#
def contacts(token)
begin
if token.is_a?(YahooToken)
@token = token.token
else
validate_signature(token)
end
credentials = access_user_credentials()
parse_credentials(credentials)
contacts_json = access_address_book_api()
Yahoo.parse_contacts(contacts_json)
rescue Exception => e
"Error #{e.class}: #{e.message}."
end
end
# This method processes and validates the redirect request that Yahoo send to
# you. Validation is done to verify that the request was really made by
# Yahoo. Processing is done to get the token.
#
# ==== Paramaters
# * path :: The path of the redirect request that Yahoo sent to you
# after authenticating the user
#
def validate_signature(path)
path.match(/^(.+)&sig=(\w{32})$/)
path_without_sig = $1
sig = $2
if sig == MD5.hexdigest(path_without_sig + @secret)
path.match(/token=(.+?)&/)
@token = $1
return true
else
raise 'Signature not valid. This request may not have been sent from Yahoo.'
end
end
# This method accesses Yahoo to retrieve the user's credentials.
#
def access_user_credentials
url = get_credential_url()
uri = URI.parse(url)
http = http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
response = nil
http.start do |http|
request = Net::HTTP::Get.new("#{uri.path}?#{uri.query}")
response = http.request(request)
end
return response.body
end
# This method generates the URL that you must access to get user's
# credentials.
#
def get_credential_url
path = CREDENTIAL_PATH.clone
path.sub!(/#appid/, @appid)
path.sub!(/#token/, @token)
timestamp = Time.now.utc.to_i
path.sub!(/#ts/, timestamp.to_s)
signature = MD5.hexdigest(path + @secret)
return AUTH_DOMAIN + "#{path}&sig=#{signature}"
end
# This method parses the user's credentials to generate the WSSID and
# Coookie that are needed to give you access to user's address book.
#
# ==== Paramaters
# * xml :: A String containing the user's credentials
#
def parse_credentials(xml)
doc = Hpricot::XML(xml)
@wssid = doc.at('/BBAuthTokenLoginResponse/Success/WSSID').inner_text.strip
@cookie = doc.at('/BBAuthTokenLoginResponse/Success/Cookie').inner_text.strip
end
# This method accesses the Yahoo Address Book API and retrieves the user's
# contacts in JSON.
#
def access_address_book_api
http = http = Net::HTTP.new(ADDRESS_BOOK_DOMAIN, 80)
response = nil
http.start do |http|
path = ADDRESS_BOOK_PATH.clone
path.sub!(/#appid/, @appid)
path.sub!(/#wssid/, @wssid)
request = Net::HTTP::Get.new(path, {'Cookie' => @cookie})
response = http.request(request)
end
response.body
end
# This method parses the JSON contacts document and returns an array
# contaning all the user's contacts.
#
# ==== Parameters
# * json :: A String of user's contacts in JSON format
# "fields": [
# {
# "type": "phone",
# "data": "808 123 1234",
# "home": true,
# },
# {
# "type": "email",
# "data": "martin.berner@mail.com",
# },
#
# {
# "type": "otherid",
# "data": "windowslive@msn.com",
# "msn": true,
# }
# ]
#
def self.parse_contacts(json)
contacts = []
people = JSON.parse(json)
people['contacts'].each do |contact|
name = nil
email = nil
firstname = nil
lastname = nil
contact_fields=Yahoo.array_to_hash contact['fields']
emails = (contact_fields['email'] || []).collect {|e| e['data']}
ims = (contact_fields['otherid'] || []).collect { |im| get_type_value(im) }
phones = (contact_fields['phone'] || []).collect { |phone| get_type_value(phone) }
addresses = (contact_fields['address'] || []).collect do |address|
type=get_type(address)
type = {"home" => "home", "work" => "work"}[type.downcase] || "other"
value = [address['street'], address['city'], address['state'], address['zip'], address['country']].compact.join(", ")
{"type" => type, "value" => value}
end
name_field=(contact_fields['name'] || [])
# if name is blank, try go for the yahoo id, and if that's blank too, ignore the record altogether (probably a mailing list)
if (name_field.empty?)
if contact_fields['yahooid']
name = contact_fields['yahooid'][0]['data']
else
next
end
else
name_field = name_field[0]
name = "#{name_field['first']} #{name_field['last']}"
name.strip!
lastname = name_field['last']
firstname = name_field['first']
end
yahoo_contact = Contact.new(nil, name, nil, firstname, lastname)
yahoo_contact.emails = emails
yahoo_contact.ims = ims
yahoo_contact.phones = phones
yahoo_contact.addresses = addresses
yahoo_contact.service_id = contact['cid']
contacts.push yahoo_contact
end
contacts
end
#
# grab the type field from each array item
# and turn it into a "email"=>{}, "phone"=>{} array
#
private
def self.array_to_hash(a)
(a || []).inject({}) {|x,y|
x[y['type']] ||= []
x[y['type']] << y
x
}
end
#
# return type/value from a datastructure like
# {
# "data": "808 456 7890",
# "mobile": true
# }
# -----> "type"=>"mobile", "value"=>"808 456 7890"
#
def self.get_type_value(hash)
type_field = hash.find{ |x| x[1] == true }
type = type_field ? type_field[0] : nil
{"type" => type, "value" => hash["data"]}
end
#
# return just the type from a datastructure like
# {
# "data": "808 456 7890",
# "mobile": true
# }
# -----> "mobile"
#
def self.get_type(hash)
type_field = hash.find{ |x| x[1] == true }
type = type_field ? type_field[0] : nil
end
end
class YahooToken
attr_reader :token
def initialize(token)
@token = token
end
end
end