Pages

Monday, June 20, 2011

Google Apps Scripts: Shared Contact Groups

UPDATE: Jul 7, 2012
While revising the script I did more debugging as there were some code chunks I wanted to improve, thinking that maybe Apps script performance got better. It turns out, creating a new or updating existing contact is very inefficient. Here's a screenshot from my debugging session:


Notice a bunch of cc.setXxx() methods. From my understanding every setXxx() call (e.g. setFamilyName) actually does a real API call to Google Contacts backend, so you can imagine how many API calls the script does only to update or create one contact. I suspect Google (reasonably) sets an API call rate threshold which is probably what limits the synchronization process and "We're sorry, a server error occurred. Please try again".

A much more efficient approach would be to create or update a contact data in one API call, e.g. ContactsApp.createContact(...all fields go here...).

Another efficient approach would be to update all fields without making actual Contacts API calls and only afterwords do one API call e.g. ContactsApp.updateContact(contact_object).

Unfortunately, there are no efficient approaches currently available. At least, I could not dig it up from Apps script documentation. This leaves the script to be quite inefficient as there is no way to do "batch updates", especially with a medium to large contact groups. Well, it is possible to use directly Google Contacts API protocol (GData) from Apps script, but App Engine would probably be a better platform to do the job.


ORIGINAL POST (Jun 20, 2011)
------------------------------------------------


This is a simple Apps script to share a group of contacts among some users within your Google Apps domain or just any user with a Google account, if you don't want to buy an app from Google Apps Marketpalce.

Here's how to use it.

1. Create a new spreadsheet, go to Tools > Script Gallery, search for "shared contact groups" and click on "install" button. If you can't find it just download this text file and paste its content into the script editor window: docs.google.com/uc?id=0B19L0O2pMOKjNDk3ZDJiZjMtZjUyNy00MjY0LTg2NjctMjZjNTgxMzhhM2Nm&export=download

2. After you install the script you should be able to see an additional menu called "Contacts". Use it to start sharing an existing group of your contacts:


Select a group you want to share and click on "continue"



3. The script will create a new sheet named after the contacts group and populate it with contacts of that group:



4. That's it. Now, share the spreadsheet with someone else and tell them to go Contacts > Sync with my contacts:



5. Keep it synchronized! Go to
Tools > Script editor and then Triggers > Current script's triggers:



Now, add a time-driven trigger keep the contacts in sync with other users you've shared the spreadsheet with:






Let me know if you have any troubles.

25 comments:

  1. Alex
    I cant´find "shared contact groups" at Script Gallery

    where could I find the script alternatively?

    thanks in advance
    RAFAEL

    ReplyDelete
  2. Alex,

    Nice concept. Is there also a way to sync to the domain directory? I have tested it, I really like the concept. Unfortunately it doesn't seem to work reliably, especially with lager set's of data in groups (1700 contacts)

    Good effort thanks!!

    ReplyDelete
  3. Well, I might have a solution, in March, when the API was improved, it was already working fine and got stuck in the debug loop of the GAS team. I will merge your solution and mine and come back to you. HMK

    ReplyDelete
  4. I have started merging concepts and like the style of your coding. To sum up the strategy in an abstract upfront: User Defined field used for UNIFIER making each Contact a unique entity. A master record stored in a registry. Iteration over Groups. In case User applies the merge duplicates function of the Contacts App, Unifier get's replicated. Replication is resolved via indirection i.e. mapping to one randomly chosen master record. In case of ambiguity, leave decision to user. I have to repeat, the style of your programming is a good lesson form me. The main solution results from a diffent perspective, instead of iterating over contact groups to identify changes the iteration processes 10 to 50 rows of the master record in a period of 5 to 10 minutes. Somewhere in between both strategies lies the answer, because new contacts would not be processed without additional iteration over contact groups. Dates is still TODO, ok, I will check http://code.google.com/intl/de-DE/googleapps/appsscript/class_datefield.html. HMK

    ReplyDelete
  5. Input for the TODO


    var MONTH = [
    ContactsApp.Month.JANUARY,
    ContactsApp.Month.FEBRUARY,
    ContactsApp.Month.MARCH,
    ContactsApp.Month.APRIL,
    ContactsApp.Month.MAY,
    ContactsApp.Month.JUNE,
    ContactsApp.Month.JULY,
    ContactsApp.Month.AUGUST,
    ContactsApp.Month.SEPTEMBER,
    ContactsApp.Month.OCTOBER,
    ContactsApp.Month.NOVEMBER,
    ContactsApp.Month.DECEMBER
    ];

    function getDate( DateString ) { //* 2.3.2011, 1.3.2011
    if (typeof(DateString)=='object') { return DateString };
    if (typeof(DateString)=='string') {
    var d = DateString.split(" ");
    var d = d[0].split(".");
    if (d.length == 1) {d = d[0].split("/")}
    var month = d[1]*1-1;
    var year = d[2]*1;
    var day= d[0]*1;
    return new Date( year, month, day);
    } else { return null }
    } // getDate

    function eqDate(d1,d2) {
    if (d1.getDay() == d2.getDay() ) {
    if (MONTH[d1.getMonth()] == d2.getMonth() ) {
    if (d2.getYear() == d2.getYear() ) {
    return true;
    }
    }
    }
    return false;
    } // eqDate

    function _changeDate(c, value, label) {
    var dates = c.getDates();
    var found = false;
    var v = getDate(value);
    if (v.getYear() != 1759) {
    for (var date in dates) {
    var d = dates[date];
    if (d.getYear() == 1759) {
    dates[date].deleteDateField();
    } else {
    if (eqDate(dates[date],v)) {
    found = true;
    break;
    }
    }
    }
    if (!found) { // v.getDay() liefert Wochentag
    try {
    c.addDate(label,MONTH[v.getMonth()] , v.getDate(), v.getFullYear() );
    } catch (e) {} // es kann nur ein Geburtstag Feld festgelegt werden
    }
    }
    } //_changeDate

    You would have to adopt it to your style, Alex.

    ReplyDelete
  6. Hi Alex,
    something is wron with deletePhoneFields. Your implementation should cause random deletion of phone numbers, it does not delete the field it should address, but the first field in the list. Workaraoung: Copy all phone fields in a buffer, skip the one that should be deleted, delete all fields, newly create the remaining ones.
    Does not have anything to do with your implementation, is a flaw left by the GAS team (they somehow have their own logic, otherwise the API would be complete, RELATIONSHIP is missing :-) and properly documented. You allready know the leak in documentation with c.addCompany(company, title) :-) It took some time to get over this, well thanks for your feedback and keep in mind, the code provided for handling date fields does not address the main function yet, I am still working on a migration as the old API was not complete I have used customFields (as they are called now) that are no longer needed except for Relationships. By the way these customFields cannot be manipulated on an Android Smartphone.

    ReplyDelete
  7. Hi Alex,
    there is little mistake in creating custom fields:

    // add missing fields
    for (var i in custms) {
    var e = custms[i];
    if (e.value && e.value.length > 0) {
    cc.addCustomField(e.label, e.value); // delete get Valid Label! Alex
    }
    }
    You would loose Custom Labels and get "other" instead.

    ReplyDelete
  8. Alex,
    I like what you guys have been doing. One thing I have noticed is that if you have a contact group (in my case of around 87), that only 30 or 31 synchronize over to the other account's contacts. Do you know why that is and how it can be fixed?

    thanks and please keep up the great work!! Also HMK--appreciate your contributions to this project.

    ReplyDelete
  9. Alex and HMK,
    I was wondering if you guys could guys make an adjustment to your code for me to help me out. I have noticed that some of the contacts will not synchronize over if they do not have an email address. Since I have many contacts that do not have an email address, is there any way that they could by synch'ed over based on some other criteria?

    Any assistance you can give is very much appreciated.

    ReplyDelete
  10. Hi Dannyboy,
    we are about to finish a project (took 5 Months of work) on behalf ob this issue. The concept is outlined already,I actually gave up when the GAS team released a buggy update 29.2.2011 and because of Alex we have retried and even adopted the style and format of his implementation for master-records. I will first share the result with Alex and may be follow up on your request later.
    HMK

    ReplyDelete
  11. Thank you very much HMK for the response. I think you guys are on to a great work-around for syncing contacts between Google Apps Domain users. Just to reiterate what I was trying to say before, it would be great if your code would sync a contact even if it did not have an email address. Perhaps instead basing it on whether they had a Home OR Mobile phone, as most contacts have at least one of those. That is just a thought. I realize you two know way better than I. Thanks again for sharing the excellent work you both have done. Don't give up--there is a lot of poor folks out there that will appreciate this work around to having shared contacts on a Google Apps Domain.

    Sincerely,
    dannyboy

    ReplyDelete
  12. Hi Alex, there is a bugfix for your script. In case eg "http://" would be found as value, your version would fail. You may use:

    /**
    * convert serialized string to javascript 1d array
    */
    function _stringToFields(str, fns) {
    var fields = [];
    var vals = str.split(MF_SEPARATOR);
    for (var i in vals) {
    var v = vals[i];
    var field = new Object();
    var attrs = v.split(F_SEPARATOR);
    for (var ai in attrs) {
    var attr = attrs[ai].split(LB_SEPARATOR);
    var rechts = attr[1];
    for(var aj=2; aj<attr.length; aj++) {
    rechts = rechts+':'+attr[aj];
    }
    field[attr[0]] = rechts;
    }
    fields.push(field);
    }
    return fields;
    } //_stringToFields

    ReplyDelete
  13. Hello,

    Any update on Enhancement of this script ?

    I love this script ;)

    ReplyDelete
  14. one of the most obvious missing features in google apps ... and the only curent solution is to pay TONS of money for app just to add a Share Contact link. It is already built into my android phone, blah. Sorry for the rant, I meant to say ... there is a HUGE need for this functionality, and you guys rock for working on it. I wish I was more of a programmer so I could code it myself ... ha ha.
    BTW, developers out there, I would happily pay 1 time fee for a well written app, but monthly charges are not gonna happen.

    ReplyDelete
  15. There are several errors where getValidLabel needs to be called before passing the parameter to f.setLabel as well as the one mentioned by HMK above when it is called when it shouldn't be.

    Runtime errors (changed eTag) went away when I put a lock around the update process so that several users running the script simultaneously didn't interfere.

    I also modified it to handle named contacts with no email address.

    My version is here: https://docs.google.com/document/d/1IrHyXh5T5JEj1vLP3fqPty115f-gAWhSfvOhbqV3_vU/edit

    ReplyDelete
  16. Updated my version (now 0.21) to better handle deleted phone numbers, addresses, etc. I think I've zapped some more runtime errors with these fixes.

    ReplyDelete
  17. Well I tried setting it up and Pull from contacts works. But sync with my contacts doesn't work for me. Is there any debugging I can do to see what's happening? It doesn't partially sync or anything like it. Just creates the shared group and that's that.

    ReplyDelete
  18. Well it works for me one way - pull from contacts but syncing to contacts with another user isn't working. It just generates a group with no contacts in there.

    Any debugging I can do ?

    ReplyDelete
  19. Same for me here : with revision 0.21 from Alan, I was able to pull my contacts into the spreadsheet, but then syncing with my contacts, or syncing once shared with another user, the user get following error : "Erreur liée à un service : Contacts: [Line 1, Column 384, element gd:givenName] Missing required text content (ligne 238)". On other user's account, group of contacts was created containing only the very first contact from my group. (out of 470) So i guess the script stopped working at Line 1.
    I intended to use the script sharing a group of relevant contacts with a colleague of mine, so that both may update the group. Is it the right script to perform such a task ?
    Line 238 of the script, I've following code : gc = ContactsApp.createContact(sc.firstName, sc.lastName, sc.primaryEmail);

    ReplyDelete
  20. Hi, i have similar problems. i put a try|catch exception, because it seems that the contacts service got some issue.

    try {
    gc.addToGroup(ggroup);// add the contact to the new group
    } catch(e) {
    Utilities.sleep(1000);
    try {
    gc.addToGroup(ggroup);
    } catch(e) {
    Utilities.sleep(1000);
    try {
    gc.addToGroup(ggroup);
    } catch(e) {
    Utilities.sleep(1000);
    gc.addToGroup(ggroup);
    }
    }
    }

    this is an example. I put it on the group association.
    see http://googleappsdeveloper.blogspot.com/2011/03/contact-sharing-using-google-apps.html

    if you copy the spreadsheet and look into the script there are many comments that explain errors and error traps.

    It seems there is a 100/day contacts limit on creation.
    see http://code.google.com/p/google-apps-script-issues/issues/detail?id=478

    So if you put a sleep the service it runs quiet and it works. But then if you have many contacts (i am trying with 400.. not so many at all for an addressbook) it stops me after 5 minutes or throws an internal error around 300 contacts.

    The script worked fine for me until october 2011 then some issues begun. Now is very hard to sync contacts using script.

    It seems there is no answer to our question: how to share contacts in google apps ? (no domain contacts plz)

    even if i do the google way (writing javascript code) it does not work.

    Very very very frustrating.

    ReplyDelete
  21. in my previous post i cited a couple of sources, just to put togheter information, and experience of other programmers.

    If you have somethig similar please post it. This contact issue is becoming a nightmare. We need all the support available, even from experiments.

    Thank you.

    ReplyDelete
  22. Gentlemen,
    In view of the latest release from Google on Delegating Contacts(http://support.google.com/a/bin/answer.py?hl=en&topic=20016&answer=2590392), there are still some problems with it. In this Google Group discussion, some of them are mentioned: https://groups.google.com/forum/?fromgroups#!topic/google-contacts-api/u8L7TegcbOM

    While it looks like Google might be getting a shared contacts solution out, in the mean time, I am wondering if there is any way to share out a users contacts (including the groups they are members of)? Also, it would be nice if the other users who these contacts are shared with can also modify the groups these contacts are members of.

    Please advise.

    Thanks

    ReplyDelete
  23. Hi Alex (and others who have worked on this Contacts Sync Script),
    Just wondering if you have made any further progress with this script? I was loving it but, right now it doesn't seem to be working. Any ideas? Is it working for you?

    Thanks in advance.

    danny

    ReplyDelete
  24. Trying to test this out for use in my organization. I ran the upload once without apparent results (a week or so ago). When I got back to this today and try to run it I get an error 401 when I run the upload script.

    ReplyDelete
  25. trying to click sync with my contacts. I have error message ;

    서비스 오류: ContactsApp: [Line 1, Column 418, element gd:familyName] Missing required text content

    I don't know what is problem.

    ReplyDelete

What do you think?