lib/meal_ticket.rb in meal_ticket-0.0.0 vs lib/meal_ticket.rb in meal_ticket-0.1.0

- old
+ new

@@ -1,14 +1,176 @@ -# -# == Steps -# -# 1. Developer signs up for 3rd-part API (let's say Facebook) -# 1. Developer copies & pastes access info into some config location -# 1. Developer creates a callback action that will receive the user's token -# !. Developer redirects the user to something like facebook_auth_url() passing a parameter indicating what permissions he needs -# -# -module MealTicket - +# :include:README.rdoc +require 'cgi' +module MealTicketRoutes + FLICKR_API_BASE_URL = "http://api.flickr.com/services/" # :nodoc: + + # [root_url] The base url of your app, eg "http://www.google.com/". If you're running a rails app, you can literally type root_url + # [scope] A comma-separated list of permissions. For a full list of permissions, see https://developers.facebook.com/docs/authentication/permissions/ + def facebook_auth_url(root_url, scope) + "https://graph.facebook.com/oauth/authorize?client_id=#{FACEBOOK_APP_ID}&redirect_uri=#{root_url}meal_ticket/facebook_callback&scope=#{scope}" + end + + # Generates the URL for the 2nd step of Facebook auth - exchanging the code for user data + def facebook_exchange_url(root_url, code) # :nodoc: + "https://graph.facebook.com/oauth/access_token?client_id=#{FACEBOOK_APP_ID}&redirect_uri=#{root_url}meal_ticket/facebook_callback&client_secret=#{FACEBOOK_SECRET}&code=#{CGI::escape code}" + end + + # [perm] A single permission level. Permissions can be read, write, or delete. Each successive permission implies the ones before it, eg "write" implies "read". For more information, see http://www.flickr.com/services/api/auth.spec.html + def flickr_auth_url(perm) + flickr_url({"perms" => perm}, "auth") + end + + # Generates the URL for the 2nd step of Facebook auth - exchanging the frob for user data + def flickr_frob_url(frob) # :nodoc: + flickr_url({"method" => "flickr.auth.getToken", "frob" => frob}) + end + + private + + def flickr_url(arg_hash, endpoint = "rest") + arg_hash.merge!({"api_key" => FLICKR_TOKEN}) + arg_list = [] + arg_hash.each do |key, value| + arg_list << "#{key}=#{value}" + end + "#{FLICKR_API_BASE_URL}#{endpoint}/?&api_sig=#{flickr_sign(arg_hash)}&#{arg_list.join('&')}" + end + + def flickr_sign(arg_hash) + arg_list = [] + arg_hash.keys.sort.each do |key| + arg_list << key + arg_list << arg_hash[key] + end + Digest::MD5.hexdigest("#{FLICKR_SECRET}#{arg_list.join()}") + end end + + +require 'net/http' +require 'net/https' +require 'crack' +require 'json' +class MealTicket + include MealTicketRoutes + + def initialize(app) + @app = app + end + + def call(env) + @env = env + if env["PATH_INFO"] =~ /^\/meal_ticket\/facebook_callback/ + facebook_callback + elsif env["PATH_INFO"] =~ /^\/meal_ticket\/flickr_callback/ + flickr_callback + else + @app.call(env) + end + end + + def facebook_callback + response = get facebook_exchange_url(get_root_url, get_query_string_parameter("code")) + + if response.body.include? "access_token" + response.body =~ /access_token=([^&]+)(?:&expires=(.*))?/ # TODO: genericize get_query_string_parameter to handle this? + token = $1 || "" + expires = $2 || "" + query_string = "facebook[token]=#{CGI.escape(token)}&facebook[expires]=#{CGI.escape(expires)}" + else + parsed = JSON.parse(response.body) + query_string = to_params({:facebook => parsed}) + end + + body = %(<html><body>You are being redirected.</body></html>) + headers = { + 'Location' => "#{get_root_url}#{FACEBOOK_CALLBACK}?#{query_string}", + 'Content-Type' => 'text/html', + 'Content-Length' => body.length.to_s + } + [302, headers, [body]] + end + + def flickr_callback + + xml = get flickr_frob_url(get_query_string_parameter("frob")) + + parsed = Crack::XML.parse(xml.body) + if parsed["rsp"]["stat"] == "ok" + remote_id = parsed["rsp"]["auth"]["user"]["nsid"] || "" + token = parsed["rsp"]["auth"]["token"] || "" + query_string = "flickr[token]=#{CGI.escape(token)}&flickr[user_id]=#{CGI.escape(remote_id)}" + else + code = parsed["rsp"]["err"]["code"] + msg = parsed["rsp"]["err"]["msg"] + query_string = "flickr[error][code]=#{CGI.escape code}&flickr[error][msg]=#{CGI.escape msg}" + end + + body = %(<html><body>You are being redirected.</body></html>) + headers = { + 'Location' => "#{get_root_url}#{FLICKR_CALLBACK}?#{query_string}", + 'Content-Type' => 'text/html', + 'Content-Length' => body.length.to_s + } + [302, headers, [body]] + end + + private + def get(url) + use_ssl = url.include? 'https' + url = URI.parse(url) + + path = url.query.blank? ? url.path : "#{url.path}?#{url.query}" + + http = Net::HTTP.new(url.host, use_ssl ? 443 : nil) + http.use_ssl = use_ssl + res = http.get(path) + + if res.code == '302' + return get(res['location']) # follow redirects + end + res + end + + private + def get_query_string_parameter(param) + @env["QUERY_STRING"] =~ Regexp.new("#{param}=(.*)&|#{param}=(.*)$") + return $1 || $2 # whichever one worked + end + + def get_root_url + @env["REQUEST_URI"] =~ /(^.*?:\/\/.*?\/)/ # looking for the form .....://.........../ + return $1 + end + + + # stolen from merb, according to StackOverflow + def to_params(obj) + params = '' + stack = [] + + obj.each do |k, v| + if v.is_a?(Hash) + stack << [k, v] + else + params << "#{CGI.escape k}=#{CGI.escape v}&" + end + end + + stack.each do |parent, hash| + hash.each do |k, v| + if v.is_a?(Hash) + stack << ["#{parent}[#{k}]", v] + else + params << "#{parent}[#{CGI.escape k}]=#{CGI.escape v}&" + end + end + end + + params.chop! # trailing & + params + end +end + +