Friday, March 16, 2012

OmniContacts: import email contacts in your Rails application

Few days ago I released the first version of OmniContacts.
As it says in the README:

Inspired by the popular OmniAuth, OmniContacts is a library that enables users of an application to import contacts from their email accounts. The current version allows to import contacts from the three most popular web email providers: Gmail, Yahoo and Hotmail. OmniContacts is a Rack middleware, therefore you can use it with Rails, Sinatra and with any other Rack-based framework.

While working on CueCup I faced a problem shared by many web projects: I did not have much users.
I said to myself: What is the best way the find new users? Let the current users do the job for you!
With OmniContacts, users can invite their e-mail contacts to join the website.
Unfortunately, after few months of running OmniContacts in production I still do not have that many users :(
My consolation is that I produced something I could share with the Ruby Community! Considering I could not find many alternatives, if not paid services, I think the gem have some value and I hope it can help others in achieving what I could not.

I will not go into the details of how the gem works since there is a README for that!
If you encounter any problem using them gem or you have any idea about how to improve it, just comment on this post or create an issue for it.

42 comments:

  1. Personally I loath this feature, but all my clients seem to think it is the holy grail for getting new users.

    Keep up the good work. The more chatter in more places about your app, the more success you will eventually have.

    Thanks

    ReplyDelete
    Replies
    1. Hi Bill,

      Thanks for your feedback and thanks for the pull request!

      Diego

      Delete
  2. I've been seriously frustrated with the contacts gem and the turing-contacts gem. I'm rooting for your solution to help me end this nightmare! last chance before i go at it alone :D

    Anto

    ReplyDelete
  3. Hi diego,

    I'm trying to figuring out how it works your gem, which is cool btw, can you explain me a little bit how can I integrate this with my application, I already install the gem, create the initializers, also i registered the app with the providers, but how can I test this features from my console.

    Thanks in advance!

    ReplyDelete
  4. Hi Jonathan,
    Have you used OmniAuth before? I ask you this because OmniContacts works in a very similar way.
    I suggest you to watch this screencast about OmniAuth, which will surely clarify your ideas: http://railscasts.com/episodes/241-simple-omniauth
    Let me know if this helps.

    Diego

    ReplyDelete
  5. Hi diego,

    Well, when I redirect a user to /contacts/:importer (yahoo/hotmail) doesn't redirect to an authorization page instead I got an error, with yahoo I got an internal error and with hotmail I got 'The provided value for the input parameter 'redirect_uri' is not valid.'
    I'm testing from my local machine, could this be the problem?.

    I would appreciate if you could help me with that.

    ReplyDelete
  6. The fact you are testing from your local machine is the reason Hotmail does not work. I know it is ridiculous, but unfortunately this is Microsoft :)
    Regarding Yahoo, when you register your app you have to configure the relative permissions. Did you select the read permissions for the user's contact list?

    ReplyDelete
    Replies
    1. Hi diego,

      Finally, I have tested on a staging-server, it looks good, now the app redirects me to hotmail and ask me for my permissions, but once I allow the app to acces my info, how can I retrieve the contacts?, I meant, how can I acces that info. can't understand how do you do that in your explanation ;).

      Thanks a lot man, nice job!

      Delete
    2. Hi Jonathan,

      In routes.rb you need to register the callback action to be invoked. Something like this:
      match "/contacts/:importer/callback" => "invitations#contacts_callback"

      Once the user authorizes your app he will be redirected to the callback's path. You can then access the list of contacts through the request object:
      def contacts_callback
      @contacts = request.env['omnicontacts.contacts']
      puts "List of contacts obtained from #{params[:importer]}:"
      @contacts.each do |contact|
      puts "Contact found: name => #{contact[:name]}, email => #{contact[:email]}"
      end
      end

      Let me know if this works,
      Diego

      Delete
    3. Diego,

      I already did that, the thing is that this line:
      @contacts = request.env['omnicontacts.contacts']
      does not retrieve me anything. ;(

      it retrieves nil.

      and for some reason i got a "not_authorized" error.

      it still missing another configuration?

      Sorry for all these questions, thanks for your help ;)

      Delete
    4. Hi Diego, thanks for your Gem, it's really helpfull to my app.

      but I'm having the same issue that Jonathan has with hotmail, I tested with 3 hotmail accounts and i'm getting the same error message "not_authorized", I follow all the setup steps on Github and test in this url: http://alkiller.heroku.com/contacts/hotmail , it looks like in the callback action the request.env["omnicontacts.contacts"] always return nil.

      maybe there is any missing configuration

      Att:
      Lumir Olivares

      Delete
    5. Hi Jonathan,
      Is it only Hotmail not working? Does Gmail or Yahoo works?
      Are you running on Windows? Does your log provides additional info?
      The strange thing is that if you get get not_authorized than you should be redirected to /contacts/failure. The callback action should not be called, so it is not possible that request.env["omnicontacts.contacts"] since that code should not execute at all.

      Diego

      Delete
    6. Hi Diego, I'm using this gem only to get the contacts from Hotmail and it's running on Mac.
      when i go to the path /contacts/hotmail then it show me the windows live login page, then it redirect me to the contacts/failure?error_message=not_authorized path, also the log show this error:

      undefined method `each' for nil:NilClass in the next line in the callback action:

      @contacts.each do |contact|


      thanks for your help, :)

      Att:
      Lumir Olivares

      Delete
    7. Hi Lumir,
      Can you paste the log starting from the request to /contacts/hotmail?

      Diego

      Delete
    8. sure,

      2012-04-13T19:52:11+00:00 app[web.1]: Started GET "/contacts/hotmail" for 190.242.128.66 at 2012-04-13 12:52:11 -0700
      2012-04-13T19:52:11+00:00 heroku[router]: GET alkiller.heroku.com/contacts/hotmail dyno=web.1 queue=0 wait=0ms service=412ms status=302 bytes=0
      2012-04-13T19:52:11+00:00 heroku[nginx]: 190.242.128.66 - - [13/Apr/2012:19:52:11 +0000] "GET /contacts/hotmail HTTP/1.1" 302 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.151 Safari/535.19" alkiller.heroku.com

      2012-04-13T19:53:30+00:00 heroku[router]: GET alkiller.heroku.com/contacts/failure?error_message=not_authorized dyno=web.1 queue=0 wait=0ms service=27ms status=500 bytes=728
      2012-04-13T19:53:30+00:00 app[web.1]:
      2012-04-13T19:53:30+00:00 app[web.1]:
      2012-04-13T19:53:30+00:00 app[web.1]: app/controllers/alkilers_controller.rb:14:in `new'
      2012-04-13T19:53:30+00:00 app[web.1]: NoMethodError (undefined method `each' for nil:NilClass):
      2012-04-13T19:53:30+00:00 heroku[nginx]: 190.242.128.66 - - [13/Apr/2012:19:53:30 +0000] "GET /contacts/failure?error_message=not_authorized HTTP/1.1" 500 728 "http://alkiller.heroku.com/users/sign_in" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.151 Safari/535.19" alkiller.heroku.com



      Lumir

      Delete
    9. Looks really strange. It is like there is some sort of redirection within your server itself.
      I see two lines of log for each request. Once points to 190.242.128.66 and the second to alkiller.heroku.com/
      Is there anything worth mentioning about your network configuration?
      Could you access /contacts/hotmail?q=anything and show the log only for the request? I want to see if the parameters get lost during the redirection.

      Delete
    10. still the same error message on the callback action, but now also show that the user did not grant access to contact lists

      2012-04-13T21:07:33+00:00 app[web.1]: Started GET "/alkilers/new?code=40782e26-6ffd-38d6-1316-d7b6a17aa5c3" for 190.242.128.66 at 2012-04-13 14:07:33 -0700
      2012-04-13T21:07:33+00:00 heroku[router]: GET alkiller.heroku.com/alkilers/new?code=40782e26-6ffd-38d6-1316-d7b6a17aa5c3 dyno=web.1 queue=0 wait=0ms service=2ms status=302 bytes=0
      2012-04-13T21:07:33+00:00 heroku[nginx]: 190.242.128.66 - - [13/Apr/2012:21:07:33 +0000] "GET /alkilers/new?code=40782e26-6ffd-38d6-1316-d7b6a17aa5c3 HTTP/1.1" 302 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.151 Safari/535.19" alkiller.heroku.com
      2012-04-13T21:07:34+00:00 app[web.1]: Error not_authorized while processing /alkilers/new: User did not grant access to contacts list

      2012-04-13T21:07:34+00:00 app[web.1]: "List of contacts obtained from failure:"
      2012-04-13T21:07:34+00:00 app[web.1]:
      2012-04-13T21:07:34+00:00 app[web.1]: NoMethodError (undefined method `each' for nil:NilClass):
      2012-04-13T21:07:34+00:00 app[web.1]: app/controllers/alkilers_controller.rb:14:in `new'
      2012-04-13T21:07:34+00:00 app[web.1]:
      2012-04-13T21:07:34+00:00 app[web.1]:
      2012-04-13T21:07:34+00:00 heroku[nginx]: 190.242.128.66 - - [13/Apr/2012:21:07:34 +0000] "GET /contacts/failure?error_message=not_authorized HTTP/1.1" 500 728 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.151 Safari/535.19" alkiller.heroku.com
      2012-04-13T21:07:34+00:00 heroku[router]: GET alkiller.heroku.com/contacts/failure?error_message=not_authorized dyno=web.1 queue=0 wait=0ms service=39ms status=500 bytes=728

      Lumir Olivares

      Delete
    11. Hi Lumir,
      Before the callback was /contacts/hotmail/callback but now I see /alkilers/new. What have you changed in the configuration?
      Can you show the initializer file? Can you also include the snippet of routes.rb relative to your callback?
      Diego

      Delete
    12. Hi diego, it seems that Lumir has the same Problem.. I've do the same things, this is mi initializer file, check it out:

      importer :yahoo, "dj0yJmk9aXdiNHJHbTFZU1hKJmQ9WVdrOU5reFBkVlJoTkhVbWNHbzlNakEwTmpZeU5URTJNZy0tJnM9Y29uc3VtZXJzZWNyZXQmeD1jNw--", "828411bd3a77eec72715ad53e297aabe8ddf5a1a", {:callback_path => '/users/:id/contacts'}
      importer :hotmail, "00000000480A726F", "xKS53APtkLLZ-xOdHdQezAcr-tBRHzrJ", {:redirect_path => '/callback'}
      end

      those ids are from my apps on each provider.

      and these are my routes:

      match "/contacts/:importer" => "invitations#new"

      match "/contacts/:importer/callback" => "users#contacts"

      it shows me that the user did not grant access to contact lists.
      and retrieves me nil.

      Thanks for your help!

      Delete
    13. Hi Jonathan,

      I see something strange in your config. The callback for Hotmail is /callback but I do not see any controller registered for that path.
      Can you try leaving the default setting? You can do this by only configuring client_id and callback_id:
      :hotmail, "00000000480A726F", "xKS53APtkLLZ-xOdHdQezAcr-tBRHzrJ"
      Then in your routes.rb try add this line:
      match "/contacts/:importer/callback" => "invitations#new"
      This should work :)

      Diego

      Delete
  7. This looks great. I will try it out and see how it does! thanks alot.

    ReplyDelete
  8. Thanks, I used your gem for Sandglaz.com. It works nicely, a few things that I ran across though:
    - For hotmail, I noticed that the contacts that do get imported are the ones that don't have any names. I saw your comment in the README "Their API returns a Contact object, which does not contain an e-mail field! However, if the contact has either name, family name or both set to null, than there is a field called name which does contain the e-mail address. To summarize, a Hotmail contact will only be returned if the name field contains a valid e-mail address, otherwise it will be skipped. "
    I didn't dig in to debug it, but it's hard to believe that this is the behaviour of their api. I thought I read somewhere that it returns a hash of multiple e-mails addresses per contact. Maybe when I have time, I'll dig in.
    - For testing it would be nice if it had some mock helpers like omniauth.
    Thanks again for sharing the gem! It sure helped.

    ReplyDelete
    Replies
    1. Hi Nada,
      thanks for your feedback.
      Being the multiple e-mails addresses represented as a hash, they cannot be decoded to their original value. It sounds absurd but I really think Hotmail API works like this :(
      To be honest I would not be too surprised, the fact you cannot test on localhost represents another weird limitation.
      Regarding testing, thanks for the suggestion! I will have probably add mocks in the following version.

      Diego

      Delete
    2. Hi Nada,
      I created an issue for this: https://github.com/Diego81/omnicontacts/issues/8
      Will soon start working in it.
      Btw, thanks for your pull request!

      Diego

      Delete
  9. Hi Diego, i'm having problems with your GEM...

    My code is like this...
    initializer...
    require "omnicontacts"

    Rails.application.middleware.use OmniContacts::Builder do
    #importer :gmail, "client_id", "client_secret", {:redirect_path => "/oauth2callback", :ssl_ca_file => "/etc/ssl/certs/curl-ca-bundle.crt"}
    importer :yahoo, "dj0yJmk9TUtDckMzRFJqaURWJmQ9WVdrOVJtUTBka1JrTm5FbWNHbzlPRFUyTURRM09EWXkmcz1jb25zdW1lcnNlY3JldCZ4PWVj", "9fb4a1d3de38d6fcbeecbf8edd5755136724b348", {:callback_path => '/oc_callbacks/callback'}
    importer :hotmail, "00000000440BD228", "0mV1JfA4VERiDJxz4emcf8ZxPsXPC0aD"
    end

    routes...
    # omnicontacts controller
    match 'oc_callbacks/callback', :to => 'oc_callbacks#callback'
    match 'contacts/:provider', :to => 'invitations#new'
    match 'contacts/:provider/callback', :to => 'oc_callbacks#callback'

    callbacks controller...
    # oc_callbacks_controller
    class OcCallbacksController < AdminController
    def callback
    @contacts = request.env['omnicontacts.contacts']

    puts "List of contacts obtained from #{params[:importer]}:"

    @contacts.each do |contact|
    puts "Contact found: name => #{contact[:name]}, email => #{contact[:email]}"
    end

    render :text => request
    end
    end

    it returns correctly with yahoo and hotmail, but it's allways empty the contacts array...

    What am i doing wrong??

    Thanks!!

    ReplyDelete
  10. Sorry, but also, when i try with gmail, it always return failed and not_authorized :S

    ReplyDelete
  11. Javi, your code looks fine.
    However, you can remove match 'contacts/:provider', :to => 'invitations#new' from your routes.rb. This is because OmniContacts will serve the request by itself and redirect the user to the email provider's website.
    For Yahoo, have you configured the contacts permission for your application on the Yahoo website?
    For Hotmail, it is possible all your contacts have both name and family name. In that case you would get no result. To know if this is true you could debug line 31 of lib/omnicontacts/importer/hotmail.rb
    For Google, could you provide some additional info? Maybe some of your logs?

    Diego

    ReplyDelete
  12. I have the same problem. My code is excatly as in the readme file or as the example Javi provided, and every thime I try to use google I get "not_authorized" error.

    Some of the logs:

    Started GET "/gmail_contacts/import?token=1%HtvN8WpdRwhghghfgsdfgrewt4cxvbmmn8" for 127.0.0.1 at 2012-06-11 10:33:30 +0200

    Started GET "/contacts/failure?error_message=not_authorized" for 127.0.0.1 at 2012-06-11 10:33:31 +0200

    Processing by GmailContactsController#failure as HTML
    Parameters: {"error_message"=>"not_authorized"}
    User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = 173746 LIMIT 1
    Candidate Load (0.6ms) SELECT "candidates".* FROM "candidates" WHERE "candidates"."id" = 170251 LIMIT 1
    Redirected to http://devmyhost.com/
    Completed 302 Found in 101ms (ActiveRecord: 9.6ms)

    ReplyDelete
    Replies
    1. Your log looks very strange. How come you have a token parameter in the GET request in place of the code parameter? Are you being redirected to the Google site? How does your routes.rb looks like?

      Delete
  13. This comment has been removed by the author.

    ReplyDelete
  14. This is good gem. It's easy to use. I have create a sample on github: https://github.com/khanhnguyen/omnicontacts_example. Thanks Diego. Great work!

    ReplyDelete
    Replies
    1. Thanks Diego for putting up such a gem, and Khanh for putting a example to illustrate the usage.

      Delete
  15. Hey Diego,

    Thank you for this excellent gem. You just ended 8 hours of digging with a quick 30 minute implemenation.

    Here's a question: we already have a refresh_token from Google, with the scope for contacts (we're using OmniAuth). Do you think it's possible we can use our token to get contacts via omnicontacts without asking the user to re-authorize?

    Best,
    Galen

    ReplyDelete
  16. Hi Galen,

    With current implementation of OmniContacts I don't think that's possible but I have not tried that yet. You can try and modify the code yourself. If you need any help, just let me know.

    Regards,
    Diego

    ReplyDelete
  17. Thanks Diego. Will let you know how we proceed.

    ReplyDelete
  18. Hi diego. I also have a problem with hotmail on my local machine. I read somewhere that I had to configure my hosts file and so I added this line:
    127.0.0.1 testserver.com

    and I checked that it worked when I typed it on my server. However, I was still getting "redirect_uri invalid" error page from hotmail. I added http://testserver.com/contacts/hotmail/callback as redirect url in my microsoft app settings and in omnicontacts.rb I had
    importer :hotmail, "0000000000000000", "xxxxxxxxxxxxxxxxxxx", {:redirect_path => '/contacts/hotmail/callback'}

    Did I do something wrong? Do you have any ideas?

    ReplyDelete
  19. Very useful gem Diego, thanks a lot. I forked the project because I needed specifically the given name and family name. Not sure if that is something any other people would consider useful.
    Cheers,
    Fede

    ReplyDelete
  20. Hello Diego, I have one issue about the callback of omnicontacts. I have a view /shares/new?share_type=yahoo and I use this route like a callback for yahoo "/shares/new?share_type=yahoo" but when I realice the authentication process omnicontacts return a callback with the authentication token params and here is where I have trouble cause the callback_path have a params and omnicontacts concat the rest of url like :

    /shares/new?share_type=yahoo/callback?oauth_token=b9uhe3t&oauth_verifier=atujsv

    and this doesn't will match anything.

    Shall you help me with that ?

    by Luis Gutierrez

    ReplyDelete
  21. Hello Diego, I have some problem with yahoo in production,but it is working fine in development(local) this is not working in production. but when I realice the authentication process omnicontacts return a callback with the authentication token params and here is where I have trouble cause the callback_path have a params and omnicontacts concat the rest of url like :

    /contacts/yahoo/callback?oauth_token=bptttar&oauth_verifier=n5bmax

    then this is redirecting this url /contacts/failure?error_message=internal_error

    but at development it is working fine.

    Please guide me what i can do?

    ReplyDelete
  22. HI Luis David Gutierrez Molina,

    Did you get solution for yahoo contacts, i have also same issue having. please let me know if you get this.

    ReplyDelete
  23. Guys, i have a suggestion, if you are using devise to login in your app(most probably), you can use devise_invitable gem, and combine with omnicontacts, send invitations with emails obtained by omnicontacts, is working for me.
    regards

    ReplyDelete