We recently had the opportunity to create a note in Evernote through their Evernote API. It was a little different from using other APIs because Evernote uses Thrift instead of XML/JSON-RPC like many other web applications.
As with any integration, there are a few gotchas with this one. In particular, we found the Ruby-oriented documentation on the web a little lacking. Other than that, here were the major challenges:
- We like to simulate a user being redirected and authorizing the app in our integration tests, but getting a session to work as if a user were driving it was tricky
- The API has a very Java-like feel, which wasn’t a natural fit for Ruby idioms
- The contents of notes need to be wrapped in special XML, which is documented but isn’t the first thing you find when Googling.
- Exceptions thrown from the API are cryptic at first, until you know how to get the info you want out of them.
Without further ado, below is the full source code:
require 'oauth'
require 'evernote-thrift'
#this is for testing. in production, it's www.evernote.com
base = "https://sandbox.evernote.com"
# given to you by Evernote
key = "developer-key-from-evernote"
# given to you by Evernote
secret = "developer-secret-from-evernote"
# needs to be a valid path in your application
callback = "http://www.pollen.io/evernote-oauth-callback"
oauth = OAuth::Consumer.new( key,secret,{:site=>base,
:authorize_path => "/OAuth.action",
:access_token_path=>"/oauth",
:request_token_path=>"/oauth"})
#this lets you see raw wire calls
oauth.http.set_debug_output($stdout)
#this callback is not ignored and is actually used
request_token = oauth.get_request_token(:oauth_callback=>callback)
puts request_token.inspect
#pretend like the user logged in and pushed the "Authorize" button
oauth_verifier = pretend_to_authorize_as_user(request_token)
#end of pretending like the user logged in
#now, finally, get the access_token you wanted and then use it to your heart's content
access_token = request_token.get_access_token(:oauth_verifier=>oauth_verifier)
puts access_token.inspect
# you need to store the url for the note store when you get it in the access_token.
#this can differ by user, so don't just use the same one for everybody
note_store_url = access_token.params['edam_noteStoreUrl']
access_token_str = access_token.token
#Here is where we create an actual note.
# This will all feel familiar for someone with a Java background, but if you've
# only ever used Ruby it's going to feel really weird.
note = Evernote::EDAM::Type::Note.new
my_content = "Hello, this is a note from Pollen.io. Please check us out if you need enterprise-grade cloud integration"
#This is a special gotcha. The content of your note needs to be wrapped in this special xml
note.content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\"><en-note><![CDATA[#{my_content}]]></en-note>"
note.title = "I love Evernote"
note.tagNames = ['pollen.io','evernote']
noteStoreTransport = Thrift::HTTPClientTransport.new(note_store_url)
noteStoreProtocol = Thrift::BinaryProtocol.new(noteStoreTransport)
noteStore = Evernote::EDAM::NoteStore::NoteStore::Client.new(noteStoreProtocol)
begin
note = noteStore.createNote(access_token_str, note)
rescue Evernote::EDAM::Error::EDAMUserException => e
#the exceptions that come back from Evernote are hard to read, but really important to keep track of
msg = "Caught an exception from Evernote trying to create a note. #{translate_error(e)}"
raise msg
end
We omitted a few functions above for clarity, so here they are for completeness sake.
First, the function to simulate a user being redirected to Evernote, logging in, and pushing the “Authorize” button. Of course, in a real app, you’d actually redirect a real user there instead, but as I said we like to simulate this in integration tests.
def store_cookies(response)
rv = {}
cookies = response.get_fields("Set-Cookie")
if cookies
cookies.each do |cookie|
real_cookie = cookie.split('; ')[0]
key, value = real_cookie.split("=")
rv[key] = real_cookie #yes, setting the whole cookie and not just the value
end
end
rv
end
#here we are pretending to be the user and submitting the login form as if we were them. In reality, you'd redirect
# the user to the url and then wait for Freshbooks to redirect them back to you.
def pretend_to_authorize_as_user(request_token)
net = Net::HTTP.new("sandbox.evernote.com", 443)
net.use_ssl = true
net.verify_mode = OpenSSL::SSL::VERIFY_NONE
net.set_debug_output $stdout
net.read_timeout = 5
net.open_timeout = 5
#force a session to start
# this doesn't seem to work unless you have a session
start_request = Net::HTTP::Post.new("https://sandbox.evernote.com/Login.action")
start_response = net.start do |http|
http.request(start_request)
end
cookies = store_cookies(start_response)
#extract the session id from the cookies
session_id = cookies['JSESSIONID'].split('=')[1]
#now login as if you were that user
login_params = {:username=>'a-test-user-name',
:password=>'a-test-user-password',
:login=>'Sign In',
:targetUrl=>CGI.escape("/OAuth.action?oauth_token=#{request_token.token}")}
login_request = Net::HTTP::Post.new("https://sandbox.evernote.com/Login.action;jsessionid=#{session_id}")
login_request.set_form_data(login_params)
login_request.add_field("Cookie", cookies.values.join("; "))
login_request.add_field("Cookie", "JSESSIONID=#{@session_id}") if @session_id
login_response = net.start do |http|
http.request(login_request)
end
cookies = cookies.merge(store_cookies(login_response))
#now actually push the "Authorize" button
authorize_params = {:authorize=>"Authorize", :oauth_token=>request_token.token}
authorize_request = Net::HTTP::Post.new("https://sandbox.evernote.com/OAuth.action")
authorize_request.set_form_data(authorize_params)
authorize_request.add_field("Cookie", cookies.values.join("; "))
authorize_request.add_field("Cookie", "JSESSIONID=#{@session_id}") if @session_id
authorize_response = net.start do |http|
http.request(authorize_request)
end
#now, finally, extract what we came here for in the first place, the access token
location = authorize_response['location'].scan(/oauth_verifier=(\d*\w*)/)
oauth_verifier = location[0][0]
oauth_verifier
end
Next, our error handling code, so you can see how to understand what happened when something goes wrong:
# see: http://www.ruby-doc.org/gems/docs/e/evernote-1.2.0/Evernote/EDAM/Error/EDAMErrorCode.html
# see: http://www.ruby-doc.org/gems/docs/e/evernote-1.2.0/Evernote/EDAM/Error/EDAMUserException.html
def translate_error(e)
error_name = "unknown"
case e.errorCode
when Evernote::EDAM::Error::EDAMErrorCode::AUTH_EXPIRED
error_name = "AUTH_EXPIRED"
when Evernote::EDAM::Error::EDAMErrorCode::BAD_DATA_FORMAT
error_name = "BAD_DATA_FORMAT"
when Evernote::EDAM::Error::EDAMErrorCode::DATA_CONFLICT
error_name = "DATA_CONFLICT"
when Evernote::EDAM::Error::EDAMErrorCode::DATA_REQUIRED
error_name = "DATA_REQUIRED"
when Evernote::EDAM::Error::EDAMErrorCode::ENML_VALIDATION
error_name = "ENML_VALIDATION"
when Evernote::EDAM::Error::EDAMErrorCode::INTERNAL_ERROR
error_name = "INTERNAL_ERROR"
when Evernote::EDAM::Error::EDAMErrorCode::INVALID_AUTH
error_name = "INVALID_AUTH"
when Evernote::EDAM::Error::EDAMErrorCode::LIMIT_REACHED
error_name = "LIMIT_REACHED"
when Evernote::EDAM::Error::EDAMErrorCode::PERMISSION_DENIED
error_name = "PERMISSION_DENIED"
when Evernote::EDAM::Error::EDAMErrorCode::QUOTA_REACHED
error_name = "QUOTA_REACHED"
when Evernote::EDAM::Error::EDAMErrorCode::SHARD_UNAVAILABLE
error_name = "SHARD_UNAVAILABLE"
when Evernote::EDAM::Error::EDAMErrorCode::UNKNOWN
error_name = "UNKNOWN"
when Evernote::EDAM::Error::EDAMErrorCode::VALID_VALUES
error_name = "VALID_VALUES"
when Evernote::EDAM::Error::EDAMErrorCode::VALUE_MAP
error_name = "VALUE_MAP"
end
rv = "Error code was: #{error_name}[#{e.errorCode}] and parameter: [#{e.parameter}]"
end

