31. module OAuth
class Consumer
def create_http_with_featureviz(*args)
@http ||= create_http_without_featureviz(*args).tap do |http|
begin
http.set_debug_output($stdout) unless options[:
suppress_debugging]
rescue => e
puts "error trying to set debugging #{e.message}"
end
end
end
alias_method_chain :create_http, :featureviz
end
end
33. Responsibilities
● Network plumbing
● Translating xml/json/whatever to "smart"
hashes *
● Converting Java-like or XML-like field
names to Ruby-like field names
● Parsing string dates and integers and
turning them into real Ruby data types
● Setting reasonable defaults
● Flattening awkward data structures
● That’s it!
34. 172 Lines of Code
https://github.com/trcull/pollen-snippets/blob/master/lib/abstract_api.rb
35. class Instagram::Api
def get_photos()
resp = get("v1/users/#{@user_id}/media/recent")
parse_response_and_stuff resp
end
def get(url)
req = Net::HTTP::Get.new url
do_request req
end
def do_request( req )
net = Net::HTTP.new
net.start do |http|
http.request req
end
end
end
36. Goal
api = InstagramApi.new current_user
photos = api.get_photos
photos.each do |photo|
puts photo.thumbnail_url
end
37. def get_photos
rv = []
response = get( "v1/users/#{@user_id}/media/recent",
{:access_token=>@access_token})
data = JSON.parse(response.body)
data['data'].each do |image|
photo = ApiResult.new
photo[:id] = image['id']
photo[:thumbnail_url] = image['images']['thumbnail']['url']
if image['caption'].present?
photo[:caption] = image['caption']['text']
else
photo[:caption] = 'Instagram image'
end
rv << photo
end
rv
end
38. def get(url, params, with_retry=true)
real_url = "#{@protocol}://#{@host}/#{url}?"
.concat(params.collect{|k,v|
"#{k}=#{CGI::escape(v.to_s)}"}.join("&"))
request = Net::HTTP::Get.new(real_url)
if with_retry
response = do_request_with_retry request
else
response = do_request request
end
response
end
39. Goal
api = InstagramApi.new current_user
photos = api.get_photos
photos.each do |photo|
puts photo.thumbnail_url
end
40. class ApiResult < Hash
def []=(key, value)
store(key.to_sym,value)
methodify_key key.to_sym
end
def methodify_key(key)
if !self.respond_to? key
self.class.send(:define_method, key) do
return self[key]
end
self.class.send(:define_method, "#{key}=") do |val|
self[key] = val
end
end
end
end
See: https://github.com/trcull/pollen-snippets/blob/master/lib/api_result.rb
42. describe Instagram::Api do
subject do
user = create(:user, :token=>token, :secret=>secret)
Instagram::Api.new(user)
end
describe "making fake HTTP calls" do
end
describe "in a test bed" do
end
describe "making real HTTP calls" , :integration => true do
end
end
44. describe Instagram::Api do
subject do
#sometimes have to be gotten manually, unfortunately.
user = create(:user,
:token=>"stuff",
:secret=>"more stuff")
Instagram::Api.new(user)
end
describe "in a test bed" do
it "pulls the caption out of photos" do
photos = subject.get_photos
photos[0].caption.should eq 'My Test Photo'
end
end
end
45. describe Instagram::Api do
subject do
#sometimes have to be gotten manually, unfortunately.
user = create(:user,
:token=>"stuff",
:secret=>"more stuff")
Instagram::Api.new(user)
end
describe making real HTTP calls" , :integration => true do
it "pulls the caption out of photos" do
photos = subject.get_photos
photos[0].caption.should eq 'My Test Photo'
end
end
end
46. describe Evernote::Api do
describe "making real HTTP calls" , :integration => true do
subject do
req_token = Evernote::Api.oauth_request_token
oauth_verifier = Evernote::Api.authorize_as('testacct','testpass', req_token)
access_token = Evernote::Api.oauth_access_token(req_token,
oauth_verifier)
Evernote::Api.new access_token.token, access_token.secret
end
it "can get note" do
note_guid = "4fb9889d-813a-4fa5-b32a-1d3fe5f102b3"
note = subject.get_note note_guid
note.guid.should == note_guid
end
end
end
47. def authorize_as(user, password, request_token)
#pretend like the user logged in
get("Login.action") #force a session to start
session_id = @cookies['JSESSIONID'].split('=')[1]
login_response = post("Login.action;jsessionid=#{session_id}",
{:username=>user,
:password=>password,
:login=>'Sign In',
:targetUrl=>CGI.escape("/OAuth.action?oauth_token=#{request_token.token}")})
response = post('OAuth.action', {:authorize=>"Authorize",
:oauth_token=>request_token.token})
location = response['location'].scan(/oauth_verifier=(d*w*)/)
oauth_verifier = location[0][0]
oauth_verifier
end
48. describe "making fake HTTP calls" do
before do
Net::HTTP.stub(:new).and_raise("unexpected network call")
end
it "pulls the thumbnail out of photos" do
response = double("response")
data_hash = {"data" =>
[{
"link"=>"apple.jpg",
"id"=>"12",
"images"=>
{
"thumbnail"=>{"url"=>"www12"},
"standard_resolution"=>{"url"=>"www12"}
}
}]}
response.stub(:body).and_return data_hash.to_json
subject.should_receive(:do_request).and_return(response)
photos = subject.get_photos
photos[0].thumbnail_url.should eq 'www12'
end
end
49. class Instagram::Api
def get_photos()
resp = get("v1/users/#{@user_id}/media/recent")
parse_response_and_stuff resp
end
def get(url)
req = Net::HTTP::Get.new url
do_request req
end
def do_request( req )
net = Net::HTTP.new
net.start do |http|
http.request req
end
end
end
50. describe "making fake HTTP calls" do
before do
Net::HTTP.stub(:new).and_raise("unexpected network call")
end
it "pulls the thumbnail out of photos" do
response = double("response")
data_hash = {"data" =>
[{
"link"=>"apple.jpg",
"id"=>"12",
"images"=>
{
"thumbnail"=>{"url"=>"www12"},
"standard_resolution"=>{"url"=>"www12"}
}
}]}
response.stub(:body).and_return data_hash.to_json
subject.should_receive(:do_request).and_return(response)
photos = subject.get_photos
photos[0].thumbnail_url.should eq 'www12'
end
end
51. describe EvernoteController do
before do
@api = double('fakeapi')
Evernote::Api.stub(:new).and_return @api
end
describe "list_notes" do
it "should list notes by title" do
a_note = ApiResult.new({:title=>'test title'})
@api.should_receive(:get_notes).and_return [a_note]
get "/mynotes"
response.body.should match /<td>test title</td>/
end
end
end
53. Ask for a Request Token
Redirect User to Site (w/ Request Token)
User Logs in and Authorizes
Site Redirects Back to You W/ OAuth Verifier
Trade OAuth Verifier and Request Token for
an Access Token
Store Access Token (Securely)
Make API Calls W/ Access Token
62. class SlurpImagesJob
def self.enqueue(user_id)
SlurpImagesJob.new.delay.perform(user_id)
end
#this makes unit testing with simulated errors easier
def perform(user_id)
begin
do_perform user_id
rescue => e
Alerts.log_error "We encountered an error slurping your Instagram images please try again",
e
end
end
def do_perform(user_id)
user = User.find user_id
cafepress = Cafepress::Api.new user
instagram = Instagram::Api.new user
photos = instagram.get_photos
photos.each do |photo|
cafepress.upload_design photo.caption, photo.standard_resolution_url
end
end
end
63. class InstagramController < ApplicationController
def oauth_callback
request_access_token params
SlurpImagesJob.enqueue current_user.id
end
end
describe InstagramController do
it "enqueues a job to slurp images" do
SlurpImagesJob.should_receive :enqueue
post '/oauth_callback'
end
end
65. Register a Callback URL for a User
Site Verifies URL actually works (typically)
User Does Something
Site Calls Back URL With an ID (typically)
Application Polls Site for More Details
Application Does Whatever
66. class FreshbooksApi < AbstractApi
def register_callback(user_id)
xml = "<request method="callback.create">
<callback>
<event>invoice.create</event>
<uri>http://app.featureviz.com/webhook/freshbooks/#{user_id}</uri>
</callback>
</request>"
post_with_body("api/2.1/xml-in", xml)
end
def verify_callback(our_user_id, verifier, callback_id)
xml = "<request method="callback.verify">
<callback>
<callback_id>#{callback_id}</callback_id>
<verifier>#{verifier}</verifier>
</callback>
</request>"
post_with_body("api/2.1/xml-in", xml)
end
end
67. class WebhooksController < ApplicationController
# URL would be /webhook/freshbooks/:our_user_id
def freshbooks_callback
our_user_id = params[:our_user_id]
event_name = params[:name]
object_id = params[:object_id]
api = Freshbooks::API.new User.find(our_user_id)
if event_name == "callback.verify"
verifier = params[:verifier]
api.verify_callback our_user_id, verifier, object_id
elsif event_name == "invoice.create"
freshbooks_user_id = params[:user_id]
InvoiceUpdatedJob.new.delay.perform our_user_id, object_id,
freshbooks_user_id
end
respond_to do |format|
format.html { render :nothing => true}
format.json { head :no_content}
end
end
68. class FreshbooksApi < AbstractApi
def register_callback(user_id)
xml = "<request method="callback.create">
<callback>
<event>invoice.create</event>
<uri>http://app.featureviz.com/webhook/freshbooks/#{user_id}</uri>
</callback>
</request>"
post_with_body("api/2.1/xml-in", xml)
end
def verify_callback(our_user_id, verifier, callback_id)
xml = "<request method="callback.verify">
<callback>
<callback_id>#{callback_id}</callback_id>
<verifier>#{verifier}</verifier>
</callback>
</request>"
post_with_body("api/2.1/xml-in", xml)
end
end
80. def fb_collect(root_element_name, result_element_name, &block)
rv = []
conn = fb_connection()
page = 1
pages = 1
while page <= pages
temp = yield conn, page
page += 1
if !temp[root_element_name].nil? && !temp[root_element_name]
[result_element_name].nil?
if temp[root_element_name][result_element_name].kind_of?(Array)
temp[root_element_name][result_element_name].each do |elem|
rv.push(elem)
end
else
#if there's only one result, the freshbooks api returns a bare hash instead of a
hash inside an array
elem = temp[root_element_name][result_element_name]
rv.push(elem)
end
end
end
return rv
end