Saturday, April 12, 2008

Operator user script: Add to Google Contacts (GData API)

I wanted to export a hcard to my google contacts with Operator, but there was no such thing.

I went off and tinkered around with the google contacts API, and operator.

I did this all while experimenting with Flock 1.1, (by the way, Flock still sucks, but this time because it's only got a handful of sites it integrates with); so if it breaks in your fancy new firefox, don't look at me!

To use it;
Copy and Paste, save as add-google-contact.js
Install Operator
Tools, Options, Operator
User scripts tab
Find add-google-contact.js
Find a site with hcards (like this one), and export away to your Google account!

var add_google_contact_login_details = {email: false, password: false, auth_token: false};

/**
* A helper method to send http requests to the google services.
*/
function add_google_contact_send_request(method, url, content, auth, content_type) {
request = new XMLHttpRequest();

request.open(method, url, false);

if (auth) {
request.setRequestHeader("Authorization", "GoogleLogin auth=" + auth);
}

if (content_type) {
request.setRequestHeader("Content-type", content_type);
}

try {
request.send(content);

if (request.status == 200 || request.status == 201 || request.status == 409) {
return request.responseText;
}
dump(request.status);
dump(request.responseText);
} catch (ex) {
dump(ex);
}

return null;
}

/**
* Extracts information out from a hCard semantic object
* and returns a google-friendly XML representation.
*/
function add_google_contact_create_xml_from_vcard(hcard) {
var i;
var full_address;
var email;
var tel;
var url;
var xml = "";

xml += "<atom:entry xmlns:atom='http://www.w3.org/2005/Atom' xmlns:gd='http://schemas.google.com/g/2005'>" + "\n";
xml += " <atom:category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/contact/2008#contact' />" + "\n";

//Parse name
xml += " <atom:title type='text'>" + hcard.fn + "</atom:title> " + "\n";

xml += " <atom:content type='text'>Notes</atom:content>" + "\n";

if (hcard.email) {
for (i = 0; i < hcard.email.length; i++) {
email = hcard.email[i];

type = 'home';
if (email.type && email.type[0] == 'work') {
type = 'work';
}

xml += " <gd:email rel='http://schemas.google.com/g/2005#" + type + "' address='" + email.value + "' />" + "\n";
}
}


if (hcard.tel) {
for (i = 0; i < hcard.tel.length; i++) {
tel = hcard.tel[i];

type = 'home';
if (tel.type && tel.type[0] == 'work') {
type = 'work';
}
xml += " <gd:phoneNumber rel='http://schemas.google.com/g/2005#" + type + "'>" + tel.value + "</gd:phoneNumber>" + "\n";
}
}


if (hcard.adr) {
for (i = 0; i < hcard.adr.length; i++) {
adr = hcard.adr[i];
full_address = "";
if (adr["street-address"]) {
full_address += adr["street-address"] + " ";
}

if (adr["locality"]) {
full_address += adr["locality"] + " ";
}

if (adr["region"]) {
full_address += adr["region"] + " ";
}

if (adr["postal-code"]) {
full_address += adr["postal-code"] + " ";
}

if (adr["country-name"]) {
full_address += adr["country-name"] + " ";
}

if (full_address != "") {
xml += " <gd:postalAddress rel='http://schemas.google.com/g/2005#work'>" + full_address + "</gd:postalAddress>" + "\n";
}
}
}


xml += "</atom:entry>" + "\n";

return xml;
}

/**
* Send a create contact request for the email & auth_token provided.
*
* The contact is described in xml.
*
* @see add_google_contact_create_xml_from_vcard()
*/
function add_google_contact_create_contact(email_address, auth_token, xml) {
url = 'http://www.google.com/m8/feeds/contacts/' + escape(email_address) + "/base";

return add_google_contact_send_request("POST", url, xml, auth_token, "application/atom+xml");
}

/**
* Fetch an authorisation token for a given
* username and password
*
* @return An authorisation token string
*/
function add_google_contact_login(username, password) {
var url = 'https://www.google.com/accounts/ClientLogin';
var content = "";

content += "accountType=HOSTED_OR_GOOGLE";
content += "&Email=" + username;
content += "&Passwd=" + password;
content += "&service=cp";
content += "&source=NoCompany-Operator-0.1";


response = add_google_contact_send_request("POST", url, content, null, "application/x-www-form-urlencoded");


// Sample response
/*
HTTP/1.0 200 OK
Server: GFE/1.3
Content-Type: text/plain

SID=DQAAAGgA...7Zg8CTN
LSID=DQAAAGsA...lk8BBbG
Auth=DQAAAGgA...dk3fA5N
*/
if (response) {
parts = response.split("\n");
return parts[2].substring(5);
}

return null;
}

function add_google_contact_get_login_details() {
var passwordManager = Components.classes["@mozilla.org/passwordmanager;1"]
.getService(Components.interfaces.nsIPasswordManager);

var e = passwordManager.enumerator;

//Ask the existing password manager for google account details
var queryString = 'https://www.google.com';

while (e.hasMoreElements()) {
try {
var pass = e.getNext().QueryInterface(Components.interfaces.nsIPassword);

if (pass.host == queryString) {
email_address = pass.user;
password = pass.password;

//TODO: Check if the email_address is valid (I store my username without the @ details)
return {email: email_address, password: password, auth_token: false};
}
} catch (ex) {
dump(ex);
}
}

//We didn't find the details. Oh dear.
//Better ask nicely.

var prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);


email_address = {value: ""};
password = {value: ""};
check = {value: true};

var result = prompts.promptUsernameAndPassword(null, "", "Enter email and password for your Google Account:",
email_address, password, "Remember password", check);


if (check.value) {
try {
passwordManager.addUser(queryString, email_address.value, password.value);
} catch (ex) {
dump(ex);
}
}

return {email: email_address, password: password, auth_token: false};
}


var add_google_contact = {
description: "Add to Google Contacts",
shortDescription: "Add Google Contact",
scope: {
semantic: {
"hCard" : "fn"
}
},
doAction: function(semanticObject, semanticObjectType, propertyIndex) {
//Do we have login details?
if (add_google_contact_login_details.email == false) {
add_google_contact_login_details = add_google_contact_get_login_details();

//If the user cancelled finding them...
if (add_google_contact_login_details.email == false) {
return false
}
}

if (!add_google_contact_login_details.auth_token) {
add_google_contact_login_details.auth_token = add_google_contact_login(add_google_contact_login_details.email,
add_google_contact_login_details.password);
}

xml = add_google_contact_create_xml_from_vcard(semanticObject);

result = add_google_contact_create_contact(add_google_contact_login_details.email, add_google_contact_login_details.auth_token, xml);
}
};

SemanticActions.add("add_google_contact", add_google_contact);

3 comments:

Unknown said...

This is good, but it doesn't seem to be working for me. It says: Error: Operator.actions[action] is undefined
Source File: chrome://operator/content/operator.js
Line: 1241

Anonymous said...

It doesn't work for me. You are the only one that made this script, is it possible to fix it?

I'm able to beta-test.

Thx

Dan said...

Updated for Firefox 3, seems to work in 3.5.6