GData にだってアクセスできるんだぜ

Google Calendar API に node.js でアクセスするサンプルです。 Google は JSのクライアント実装を提供してくれていますが、node.js だと使い物にならないので、必要な部分だけ書いてみました。

雰囲気だけつかんでいただければ。HTTP Call 自体も非同期なので、がつがつ呼び出してもさくさく動きます。

/**
 * GData Client
 */
var sys = require("sys"),
    EventEmitter = require("events").EventEmitter,
    http = require('http'),
    fs = require('fs'),
    path = require('path'),
    url = require('url'),
    querystring = require('querystring');

require(path.join(__dirname, '../ext'));
var HOST = "www.google.com";
var PORT = 443;
var SOURCE = "nodejs-gdata-0.0.1";

var Client = function(params){
   if(!params){
     params = {};
   }
   this._httpsClient = http.createClient(443, HOST, true);
   this._httpClient = http.createClient(80, HOST, false);
   this._logger = params.logger || require('logger').DefaultLogger;
}

exports.Client = Client;

Client.prototype.login = function(email, password, service, callback){
   if( service == undefined ){
      throw 'service parameter must be specified.';
   }

   var event = new EventEmitter();
   var self = this;
   var params = {
      'accountType': 'HOSTED_OR_GOOGLE',
      'Email' : email,
      'Passwd' : password,
      'service' : service,
      'source' : SOURCE
   };
   var body = querystring.stringify(params);
   var req = this._httpsClient.request(
      'POST', '/accounts/ClientLogin',
      {
         'Content-Type': "application/x-www-form-urlencoded"
      }
   );
   req.write(body);
   req.on('response', function(res){
      var body = '';
      res.on('data', function(chunk){
         body += chunk;
      });
      res.on('end', function(){
         var obj = {};
         var lines = body.split('\n');
         for(var i in lines){
            var l  = lines[i];
            var kv = l.split('=');
            if( kv.length == 2 ){
               obj[kv[0]] = kv[1];
            }
         }
         if( res.statusCode == 200 ){
            self._authEmail = email;
            self._authToken = obj['Auth'];
            self._authType = 'GoogleLogin';
            self._logger.info('[GData] logged in as ' + email);
            self._logger.debug('[GData] token: ' + obj['Auth']);
            event.emit('success');
         }else{
            self._authToken = undefined;
            event.emit('failure');
         }
         callback && callback(res.statusCode == 200, obj);
      });
   });
   req.end();
   return event;
};

function _handleResponse (event, obj, fun,  payload){
   return function(res){
      if( res.statusCode >= 300 && res.statusCode < 400){
         obj._logger.debug('[GData] << Redirect: ' + res.headers.location);
         var newurl = url.parse(res.headers.location);
         fun.apply(obj, [event, newurl.pathname + "?" + newurl.query, payload]);
      }else{
         var body = '';
         res.on('data', function(chunk){
            body += chunk;
         });
         res.on('end', function(){
            obj._logger.debug('[GData] << ' + body);
            if( res.statusCode >= 200 && res.statusCode < 300 ){
               event.emit('data', JSON.parse(body));
            }else{
               sys.puts(body);
               event.emit('error', body);
            }
         });
      }
   };
}

Client.prototype.getMetaFeeds = function(params){
   var requestPath = this._getCalendarUrl('/calendar/feeds/default',
                                          params);
   var event = new EventEmitter();
   this._getCalendarFeeds(event, requestPath);
   return event;
}
Client.prototype.getAllCalendarFeeds = function(params){
   var requestPath = this._getCalendarUrl('/calendar/feeds/default/allcalendars/full',
                                          params);
   var event = new EventEmitter();
   this._getCalendarFeeds(event, requestPath);
   return event;
}

Client.prototype.getPrivateCalendarFeeds = function(params){
   var requestPath = this._getCalendarUrl('/calendar/feeds/default/private/full',
                                          params);
   var event = new EventEmitter();
   this._getCalendarFeeds(event, requestPath);
   return event;
}

Client.prototype.getOwnCalendarFeeds = function(params){
   var requestPath = this._getCalendarUrl('/calendar/feeds/default/owncalendars/full',
                                          params);
   var event = new EventEmitter();
   this._getCalendarFeeds(event, requestPath);
   return event;
}

Client.prototype.addSingleOccurrenceEvent = function(data, id){
   var event = new EventEmitter();
   var requestPath;
   if( !id ){
      id = 'default';
   }
   requestPath = this._getCalendarUrl('/calendar/feeds/' + id + '/private/full');
   var body = this._buildSingleOccurrenceEvent(data);
   this._postEvent(event, requestPath, body);
   return event;
}

Client.prototype._getCalendarUrl = function(basePath, params){
   if( !params ){ params = {}; }
   if( params.id ){
      basePath = basePath + '/' + params.id;
   }
   var query = {};
   query.alt = 'json'; // force JSON
   return basePath + "?" + querystring.stringify(query);
};


Client.prototype._postEvent = function(event, requestPath, body){
   var self = this;
   var auth = self._authType + ' auth=' + self._authToken;
   var req = self._httpClient.request(
      'POST', requestPath,
      {
         'Content-Type': 'application/atom+xml',
         'Content-Length': body.length,
         'Authorization': auth
      }
   );
   this._logger.debug('[GData] >> POST ' + requestPath);
   this._logger.debug('[GData] >> ' + body);
   req.write(body);
   req.end();
   req.on('response', _handleResponse(event, self, self._postEvent, body));
   return event;
}

Client.prototype._getCalendarFeeds = function(event, requestPath){
   this._logger.debug('[GData] GET ' + requestPath);
   var self = this;
   var auth = self._authType + ' auth=' + self._authToken;
   var req = self._httpClient.request(
      'GET', requestPath,
      {
         'Content-Type': 'application/atom+xml',
         'Authorization': auth
      }
   );
   req.end();
   req.on('response', _handleResponse(event, self, self._getCalendarFeeds));
   return event;
};

Client.prototype._buildSingleOccurrenceEvent = function(data){
   var strings = ['<entry',
                  'xmlns="http://www.w3.org/2005/Atom"',
                  'xmlns:gd="http://schemas.google.com/g/2005">',
                  '<category',
                  'scheme="http://schemas.google.com/g/2005#kind"',
                  'term="http://schemas.google.com/g/2005#event">',
                  '</category>'];
   strings.push("<title type='text'>{title}</title>".format({
      title: data.title.htmlEscape()
   }));
   strings.push("<content type='text'>{content}</content>".format({
      content: data.content.htmlEscape()
   }));
   if( data.startTime || data.endTime ){
      var when = "";
      var st = Date.parseExt(data.startTime);
      var et = Date.parseExt(data.endTime);
      var tz;
      if( st ){
         tz = st.strftime('%z');
         when += ' startTime="{t}{tz}"'.format({
            t: st.strftime("%Y-%m-%dT%T"),
            tz: tz.substr(0,3) + ":" + tz.substr(3)
         });
      }
      if( et ){
         tz = et.strftime('%z');
         when += ' endTime="{t}{tz}"'.format({
            t: et.strftime("%Y-%m-%dT%T"),
            tz: tz.substr(0,3) + ":" + tz.substr(3)
         });
      }
      strings.push("<gd:when " + when + "></gd:when>");
   }
   strings.push("</entry>");
   return strings.join("\n");
}

XML のライブラリなんかいいのないかなぁ。。。