// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2009
// http://flock.com
//
// This file may be used under the terms of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
//
// END FLOCK GPL

const CC = Components.classes;
const CI = Components.interfaces;
const CR = Components.results;
const CU = Components.utils;

CU.import("resource:///modules/FlockScheduler.jsm");
CU.import("resource:///modules/FlockSvcUtils.jsm");
CU.import("resource:///modules/FlockPrefsUtils.jsm");
CU.import("resource:///modules/FlockXPCOMUtils.jsm");
FlockXPCOMUtils.debug = false;
CU.import("resource:///modules/FlockXMLUtils.jsm");
CU.import("resource:///modules/FlockMediaUtils.jsm");

const MODULE_NAME = "Digg";

const API_CLASS_NAME = "Flock Digg API";
const API_CLASS_ID = Components.ID("{fd864ab8-d05e-4640-99bf-d4733982c894}");
const API_CONTRACT_ID = "@flock.com/webservice/api/digg;1";
const APP_KEY = "http%3A%2F%2Fflock.com";

const CLASS_NAME = "Flock Digg Service";
const CLASS_SHORT_NAME = "digg";
const CLASS_TITLE = "Digg";
const CLASS_ID = Components.ID("{184dbbf0-ce2d-11dc-95ff-0800200c9a66}");
const CONTRACT_ID = "@flock.com/webservice/digg;1";
const FLOCK_ERROR_CONTRACTID = "@flock.com/error;1";

const DIGG_NO_AVATAR_ICON = "http://digg.com/img/udl.png";
const FLOCK_NO_AVATAR_ICON = "chrome://flock/skin/people/no_avatar.png";
const SERVICES_PROPERTIES_FILE = "chrome://flock/locale/services/services.properties";
const DIGG_PROPERTIES = "chrome://flock/locale/services/digg.properties";
const DIGG_URL = "http://digg.com/";
const DOMAIN = "digg.com";

const DIGG_READER_CSS_URL = "chrome://flock/skin/services/digg/streamReader.css";

const FAVICON = "chrome://flock/content/services/digg/favicon.png";
const FLOCK_RDNDS_CONTRACTID = "@flock.com/rich-dnd-service;1";

// Interval to check to do a batch fetch friend.
const FRIEND_INTERVAL_FETCH = 3600;
// The first time, only get photos not older than one week.
const MEDIA_INITIAL_FETCH = 7 * 24 * 3600;

// The delay between two refreshes when the sidebar is closed (in seconds)
const REFRESH_INTERVAL = 1800; // seconds == 30 minutes
// The delay between two refreshes when the sidebar is open (in seconds)
const SHORT_INTERVAL = 300; // seconds == 5 minutes

const TOP_STORIES_REFRESH_MIN = 300; // seconds == 5 min
const TOP_STORIES_REFRESH_DEFAULT = 3600; // seconds == 60 min

const SERVICE_ENABLED_PREF = "flock.service.digg.enabled";
const TOP_STORIES_REFRESH_PREF = "flock.service.digg.topStoriesRefreshInterval";

const TOP_STORIES_STR = "topStories";
const TOP_STORIES_REFRESHED_STR = "topStoriesRefreshed";

// For use with the scheduler
var gTimers = [];

var gApi = null;

var gDuggCurrentUrl = null;

// Use to keep track of when the last refresh occurred for diggs/comments.
var gLastDiggsRefreshTimes = {};
var gLastCommentsRefreshTimes = {};
// Keep track of the logged in account.
var gLoggedInAccountUrn = null;

function createEnum(aEnumArray) {
  return {
    QueryInterface: function createEnum_QueryInterface(aIID) {
      if (aIID.equals(CI.nsISimpleEnumerator)) {
        return this;
      }
      throw CR.NS_ERROR_NO_INTERFACE;
    },
    hasMoreElements: function createEnum_hasMoreElements() {
      return (aEnumArray.length > 0);
    },
    getNext: function createEnum_getNext() {
      return aEnumArray.shift();
    }
  };
}

// This tracking code gets added as a parameter to Digg story links
gTrackingCode = "otc-fl";

/** Adds the Flock tracking code to the specified URL. */
function addTrackingCode(aUrl) {
  return aUrl + ((aUrl.indexOf("?") > -1)
                 ? "&"
                 : "?")
              + gTrackingCode;
}

var flockMediaItemFormatter = {
  canBuildTooltip: true,

  buildTooltip: function fmif_buildTooltip(aMediaItem) {

    default xml namespace =
      "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

    if (aMediaItem.thumbnail.match(/chrome:\/\//)) {
      xml =
        <hbox>
          <vbox>
            <image src={aMediaItem.icon} />
            <spacer flex="1" />
          </vbox>
          <vbox anonid="ttInfoContainer">
            <label anonid="ttTitleTxt">{aMediaItem.title}</label>
            <vbox anonid="ttUserTxt">
              <!-- XXX: localize -->
              <label>Diggs: {aMediaItem.rating_count}</label>
              <label>Comments: {aMediaItem.comment_count}</label>
              <label class="user">{aMediaItem.username}</label>
            </vbox>
          </vbox>
        </hbox>;
    } else {
      xml =
        <vbox>
          <hbox>
            <image src={aMediaItem.largeSizePhoto} style="margin-bottom: 2px;" />
            <spacer flex="1" />
          </hbox>
          <hbox>
            <vbox>
              <image src={aMediaItem.icon} />
              <spacer flex="1" />
            </vbox>
            <vbox anonid="ttInfoContainer">
              <label anonid="ttTitleTxt">{aMediaItem.title}</label>
              <vbox anonid="ttUserTxt">
                <!-- XXX: localize -->
                <label>Diggs: {aMediaItem.rating_count}</label>
                <label>Comments: {aMediaItem.comment_count}</label>
                <label class="user">{aMediaItem.username}</label>
              </vbox>
            </vbox>
          </hbox>
        </vbox>;
      }

    return xml;
  }
};

/*************************************************************************
 * Component: DiggAPI
 *************************************************************************/
function DiggAPI() {
  FlockSvcUtils.getLogger(this);
  this._logger.init("DiggAPI");

  this._WebDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);
  this._ppSvc = CC["@flock.com/people-service;1"]
                .getService(CI.flockIPeopleService);

  var wsa = FlockSvcUtils.flockIWebServiceAPI;
  wsa.addDefaultMethod(this, "getRequestMethod");
}

DiggAPI.prototype = new FlockXPCOMUtils.genericComponent(
  API_CLASS_NAME,
  API_CLASS_ID,
  API_CONTRACT_ID,
  DiggAPI,
  0,
  [
    CI.flockIDiggAPI,
    CI.flockIWebServiceAPI
  ]
);

DiggAPI.prototype.shortName = "DiggAPI";


/*************************************************************************
 * DiggAPI: flockIDiggAPI Implementation
 *************************************************************************/

/**
 * void getDiggInformation(in AString aURL,
                           in flockIListener aFlockListener,
                           in PRBool aShouldGetTopStories);
 * @see flockIDiggAPI#getDiggInformation
 */
DiggAPI.prototype.getDiggInformation =
function DiggAPI_getDiggInformation(aURL,
                                    aFlockListener,
                                    aShouldGetTopStories)
{
  this._logger.info(".getDiggInformation('" + aURL + "', ...)");
  var inst = this;
  var storyInfo = CC["@mozilla.org/hash-property-bag;1"]
                  .createInstance(CI.nsIWritablePropertyBag2);
/*
  var getDiggInfo_Listener = {
    onSuccess: function getDiggInfo_onSuccess(aSubject, aTopic) {
      inst._logger.debug("getDiggInfo_onSuccess(..., '" + aTopic + "')");
*/
      var wm = CC["@mozilla.org/appshell/window-mediator;1"]
               .getService(CI.nsIWindowMediator);
      var win = wm.getMostRecentWindow("navigator:browser");
/*
      if (aSubject.wrappedJSObject.stories) {
        // There could be several stories related to this URL; We will only
        // deal with the most recent one.
        var story = aSubject.wrappedJSObject.stories[0];
        storyInfo.setPropertyAsAString("storyId", story.id);
        storyInfo.setPropertyAsAString("diggs", story.diggs);
        storyInfo.setPropertyAsAString("submittedDate", story.submit_date);
        storyInfo.setPropertyAsAString("comments", story.comments);
        storyInfo.setPropertyAsAString("user_name", story.user.name)
        var fullname =
          (story.user.fullname) ? story.user.fullname : story.user.name;
        storyInfo.setPropertyAsAString("user_fullname", fullname)
        storyInfo.setPropertyAsAString("user_avatar", story.user.icon);
        var userUrl = inst._WebDetective
                          .getString(CLASS_SHORT_NAME, "profileURL", null)
                          .replace("%accountid%", story.user.name);
        var duggUrl = addTrackingCode(story.href);
        storyInfo.setPropertyAsAString("user_url", userUrl);
        storyInfo.setPropertyAsAString("diggUrl", duggUrl);
        storyInfo.setPropertyAsAString("link", duggUrl);
        gDuggCurrentUrl = duggUrl;
 
        // Detect user who submitted URL
        var pageDoc = win.document.getElementById("content").contentDocument;

        var mediaArr = [];
        var media = {
          name: fullname,
          query: "user:" + story.user.name + "|username:" + fullname,
          label: fullname,
          favicon: FAVICON,
          service: "digg" 
        };
        mediaArr.push(media);

        if (!pageDoc._flock_decorations) {
          pageDoc._flock_decorations = {};
        }

        if (pageDoc._flock_decorations.mediaArr) {
          pageDoc._flock_decorations.mediaArr =
            pageDoc._flock_decorations.mediaArr.concat(mediaArr);
        } else {
          pageDoc._flock_decorations.mediaArr = mediaArr;
        }

        var obs = CC["@mozilla.org/observer-service;1"]
                  .getService(CI.nsIObserverService);
        obs.notifyObservers(pageDoc, "media", "media:update");
      } else {
*/
        // This URL hasn't been dugg
        inst._logger.debug("URL has not been dugg");
        storyInfo.setPropertyAsAString("diggs", "0");
        var encodedUrl = encodeURIComponent(aURL);
        var url = inst._WebDetective
                      .getString(CLASS_SHORT_NAME, "submitUrl", null)
                      .replace("%url%", encodedUrl);

        // Prepopulate title in digg submission
        if (win) {
          url += "&title=" + win.gBrowser.contentTitle;
        }
        // Prepopulate breadcrumb in description body
        var breadcrumb = CC[FLOCK_RDNDS_CONTRACTID]
                         .getService(CI.flockIRichDNDService)
                         .getBreadcrumb("plain");
        if (breadcrumb) {
          url += "&bodytext=" + encodeURIComponent(breadcrumb);
        }

        storyInfo.setPropertyAsAString("diggUrl", url);
        gDuggCurrentUrl = url;
      //}
      if (aShouldGetTopStories) {
        inst._getTopStories(storyInfo, aFlockListener);
      } else {
        aFlockListener.onSuccess(storyInfo, "getDiggInformation");
      }
/*
    },
    onError: function getDiggInfo_onError(aFlockError, aTopic) {
      inst._logger.error("getDiggInfo_onError(..., '" + aTopic + ", ...)");
      aFlockListener.onError(aFlockError, "getDiggInformation");
    }
  };
  var params = {
    wrappedJSObject: { link: aURL }
  };
  this.call("stories",
            params,
            null,
            CI.flockIWebServiceAPI.GET,
            getDiggInfo_Listener);
*/
};

/**
 * void isFriend(in AString aUsername,
 *               in AString aFriendUsername,
 *               in flockIListener aFlockListener);
 * @see flockIDiggAPI#isFriend
 */
DiggAPI.prototype.isFriend =
function DiggAPI_isFriend(aUsername, aFriendUsername, aFlockListener) {
  this._logger.info(".isFriend('" + aUsername + "' , '"
                                  + aFriendUsername + "', ...)");
  var inst = this;
  var isFriend_Listener = {
    onSuccess: function isFriend_onSuccess(aSubject, aTopic) {
      inst._logger.debug("isFriend_onSuccess(..., '" + aTopic + "')");
      var friendStatus = CC["@mozilla.org/hash-property-bag;1"]
                         .createInstance(CI.nsIWritablePropertyBag2);
      try {
        var user = aSubject.wrappedJSObject.users[0];
        if (user && user.mutual) {
          friendStatus.setPropertyAsBool("friendStatus", true);
        } else {
          friendStatus.setPropertyAsBool("friendStatus", false);
        }
      } catch (ex) {
        // A user does not exist
        inst._logger.error("isFriend onError " + ex);
        friendStatus.setPropertyAsBool("friendStatus", false);
      }
      aFlockListener.onSuccess(friendStatus, "isFriend");
    },
    onError: function isFriend_onError(aFlockError, aTopic) {
      inst._logger.error("isFriend_onError(..., '" + aTopic + "', ...)");
      aFlockListener.onError(aFlockError, "isFriend");
    }
  };
  var method = "user/" + aUsername + "/friend/" + aFriendUsername;
  this.call(method,
            {},
            null,
            CI.flockIWebServiceAPI.GET,
            isFriend_Listener);
};


/**
 * void getDiggInfoByCleanTitle(in AString aTitle, in flockIListener aFlockListener);
 * @see flockIDiggAPI#getDiggInfoByCleanTitle
 */
DiggAPI.prototype.getDiggInfoByCleanTitle =
function DiggAPI_getDiggInfoByCleanTitle(aTitle, aFlockListener) {
  this._logger.info(".getDiggInfoByCleanTitle('" + aTitle + "', ...)");
  var inst = this;
  var getDiggInfoByCleanTitle_Listener = {
    onSuccess: function getDiggInfoByCleanTitle_onSuccess(aSubject, aTopic) {
      inst._logger.debug("getDiggInfoByCleanTitle_onSuccess(..., '"
                                                            + aTopic + "')");
      var storyInfo = CC["@mozilla.org/hash-property-bag;1"]
                      .createInstance(CI.nsIWritablePropertyBag2);
      try {
        var story = aSubject.wrappedJSObject.stories[0];
        storyInfo.setPropertyAsAString("link", addTrackingCode(story.href));
        storyInfo.setPropertyAsAString("diggs", story.diggs);
        storyInfo.setPropertyAsAString("comments", story.comments);
      } catch (ex) {
        // This URL has not yet been dugg
        inst._logger.error("getDiggInfoByCleanTitle onError " + ex);
        storyInfo.setPropertyAsAString("diggs", 0);
      }
      aFlockListener.onSuccess(storyInfo, "getDiggInfoByCleanTitle");
    },
    onError: function getDiggInfoByCleanTitle_onError(aFlockError, aTopic)
    {
      inst._logger.error("getDiggInfoByCleanTitle_onError(..., '"
                                                          + aTopic + ", ...)");
      aFlockListener.onError(aFlockError, "getDiggInfoByCleanTitle");
    }
  };
  this.call("story/" + aTitle,
            {},
            null,
            CI.flockIWebServiceAPI.GET,
            getDiggInfoByCleanTitle_Listener);
};

/**
 * void getFriends(in AString aUsername, in flockIListener aFlockListener);
 * @see flockIDiggAPI#getFriends
 */
DiggAPI.prototype.getFriends =
function DiggAPI_getFriends(aUsername, aFlockListener) {
  this._logger.debug(".getFriends('" + aUsername + "', ...)");
  var inst = this;

  // This constant determines the number of friends to retrieve via API.
  // We might as well pull the max possible - 100.
  const API_NUM_FRIENDS = 100;
  var method = "user/" + aUsername + "/friends";
  var friendsList = {};
  var friendCount = 0;

  function processFriend(aFriend) {
    var person = CC["@mozilla.org/hash-property-bag;1"]
                 .createInstance(CI.nsIWritablePropertyBag2);
    person.setPropertyAsAString("name", aFriend.name);
    person.setPropertyAsAString("fullname", aFriend.fullname);
    var avatar = "";
    if (aFriend.icon != DIGG_NO_AVATAR_ICON) {
      avatar = aFriend.icon;
    }
    person.setPropertyAsAString("avatar", avatar);
    friendsList[aFriend.name] = person;
    friendCount++;
    inst._logger.debug(" - friend: " + aFriend.name);
  }

  var getMoreFriends_Listener = {
    onSuccess: function api_getMoreFriends_onSuccess(aSubject, aTopic) {
      inst._logger.debug("getMoreFriends_onSuccess(..., '" + aTopic + "')");
      var totalFriends = aSubject.wrappedJSObject.total;
      var myFriends = aSubject.wrappedJSObject.users;
      for (var k in myFriends) {
        processFriend(myFriends[k]);
      }

      if (friendCount == totalFriends) {
        aFlockListener.onSuccess({ wrappedJSObject: {
                                     friends: friendsList
                                   }
                                 }, "getMoreFriends");
      } else if (friendCount > totalFriends) {
        inst._logger.error("Found too many friends");
        aFlockListener.onError(null, "getMoreFriends");
      }
    },
    onError: function api_getMoreFriends_onError(aFlockError, aTopic) {
      inst._logger.error("getMoreFriends_onError(..., '" + aTopic + "', ...");
      aFlockListener.onError(aFlockError, "getMoreFriends");
    }
  };

  var getFriends_Listener = {
    onSuccess: function api_getFriends_onSuccess(aSubject, aTopic) {
      inst._logger.debug("getFriends_onSuccess(..., '" + aTopic + "')");
      var totalFriends = aSubject.wrappedJSObject.total;
      var myFriends = aSubject.wrappedJSObject.users;
      for (var i in myFriends) {
        processFriend(myFriends[i]);
      }

      var friendsRemaining = totalFriends - API_NUM_FRIENDS;
      if (friendsRemaining > 0) {
        for (var page = 1; page * API_NUM_FRIENDS < totalFriends; page++) {
          var moreParams = {
            wrappedJSObject: {
              count: API_NUM_FRIENDS,
              offset: page * API_NUM_FRIENDS
            }
          };
          inst.call(method,
                    moreParams,
                    null,
                    CI.flockIWebServiceAPI.GET,
                    getMoreFriends_Listener);
        }
      } else {
        aFlockListener.onSuccess({ wrappedJSObject: {
                                     friends: friendsList
                                   }
                                 }, "getFriends");
      }
    },
    onError: function api_getFriends_onError(aFlockError, aTopic) {
      inst._logger.error("getFriends_onError(..., '" + aTopic + "', ...)");
      aFlockListener.onError(aFlockError, "getFriends");
    }
  };

  var params = {
    wrappedJSObject: { count: API_NUM_FRIENDS, offset: 0 }
  };
  this.call(method,
            params,
            null,
            CI.flockIWebServiceAPI.GET,
            getFriends_Listener);
};

/**
 * void getUser(in AString aUsername, in flockIListener aFlockListener);
 * @see flockIDiggAPI#getUser
 */
DiggAPI.prototype.getUser =
function DiggAPI_getUser(aUsername, aFlockListener) {
  this._logger.debug(".getUser('" + aUsername + "', ...)");
  // This listener handles getting the account owner's information.
  var inst = this;
  var getUser_Listener = {
    onSuccess: function api_getUser_onSuccess(aSubject, aTopic) {
      inst._logger.debug("getUser_onSuccess(..., '" + aTopic + "')");
      var userInfo = CC["@mozilla.org/hash-property-bag;1"]
                     .createInstance(CI.nsIWritablePropertyBag2);
      var user = aSubject.wrappedJSObject.users[0];
      userInfo.setPropertyAsAString("name",
                                    (user.fullname) ? user.fullname
                                                    : aUsername);
      userInfo.setPropertyAsAString("screenName",
                                    (user.name) ? user.name : aUsername);
      userInfo.setPropertyAsAString("avatar", (user.icon) ? user.icon : "");
      var profileURL = inst._WebDetective
                           .getString(CLASS_SHORT_NAME, "profileURL", null)
                           .replace("%accountid%", aUsername);
      userInfo.setPropertyAsAString("URL", profileURL);
      aFlockListener.onSuccess(userInfo, "getUser");
    },
    onError: function api_getUser_onError(aFlockError, aTopic) {
      inst._logger.error("getUser_onError(..., '" + aTopic + "', ...)");
      aFlockListener.onError(aFlockError, "getUser");
    }
  };
  this.call("user/" + aUsername,
            null,
            null,
            CI.flockIWebServiceAPI.GET,
            getUser_Listener);
};

/**
 * void getTopStories(in nsIWritablePropertyBag2 aResults, in flockIListener....
 * @see flockIDiggAPI#getTopStories
 */
DiggAPI.prototype.getTopStories =
function DiggAPI_getTopStories(aResults, aFlockListener) {
  var inst = this;
  var getTopStories_Listener = {
    onSuccess: function api_getTopStories_onSuccess(aSubject, aTopic) {
      inst._logger.debug("getTopStories_onSuccess(..., '" + aTopic + "')");
      var topStories = [];
      try {
        // There could be several stories related to this URL; We will only
        // deal with the most recent one.
        var stories = aSubject.wrappedJSObject.stories;
        for (var s in stories) {
          var story = stories[s];
          topStories.push({
            id: story.id,
            title: story.title,
            origLink: story.href, // Digg url w/o tracking code
            link: addTrackingCode(story.href), // Digg url with tracking code
            url: story.link, // Actual url of Digg story
            diggs: story.diggs
          });
        }
        var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
        var result = nsJSON.encode(topStories);
        aResults.setPropertyAsAString(TOP_STORIES_STR, result);
        aFlockListener.onSuccess(aResults, "getTopStories");
      } catch (e) {
        inst._logger.debug("getTopStories_onError " + e);
        aFlockListener.onError(null, e);
      }
    },
    onError: function api_getTopStories_onError(aFlockError, aTopic) {
      inst._logger.error("getTopStories_onError(..., '" + aTopic + "', ...)");
      aFlockListener.onError(aFlockError, "getTopStories");
    }
  };
  var params = {
    wrappedJSObject: { count: 5 }
  };
  this.call("stories/top",
            params,
            null,
            CI.flockIWebServiceAPI.GET,
            getTopStories_Listener);
}

/*************************************************************************
 * DiggAPI: flockIWebServiceAPI Implementation
 *************************************************************************/

/**
 * void call(in AString aApiMethod,
 *           in nsISupports aParams,
 *           in nsISupports aPostVars,
 *           in long aRequestMethod,
 *           in flockIListener aFlockListener);
 * @see flockIWebServiceAPI#call
 */
DiggAPI.prototype.call =
function DiggAPI_call(aApiMethod,
                      aParams,
                      aPostVars,
                      aRequestMethod,
                      aFlockListener)
{
  this._logger.debug(".call('" + aApiMethod + "', ...)");
  var req = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
            .createInstance(CI.nsIXMLHttpRequest)
            .QueryInterface(CI.nsIJSXMLHttpRequest);
  var inst = this;
  req.onreadystatechange = function call_onReadyStateChange() {
    if (req.readyState == 4) {
      inst._logger.debug(".call('" + aApiMethod + "', ...) responseText:\n"
                         + req.responseText);
      if (req.status == 200) {
        var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
        var result = nsJSON.decode(req.responseText);
        if (result) {
          aFlockListener.onSuccess({ wrappedJSObject: result }, "call");
        } else {
          inst._logger.error(".call() ERROR: no result");
          var xml_error = CI.flockIError.INVALID_RESPONSE_XML;
          aFlockListener.onError(inst.getError(xml_error), "call");
        }
      } else {
        aFlockListener.onError(inst.getError(req.status), "call");
      }
    }
  };
  // TODO: web dic
  var callURL = "http://digg.com/tools/services/?endPoint=/" + aApiMethod
              + "&appkey=" + APP_KEY
              + "&type=json";
  if (aParams) {
    var params = aParams.wrappedJSObject;
    for (var p in params) {
      callURL += "&" + encodeURIComponent(p)
              + "=" + encodeURIComponent(params[p]);
    }
  }
  this._logger.debug("calling URL: " + callURL);
  req.open(this.getRequestMethod(aRequestMethod), callURL, true);
  req.send(null);
};

/**
 * flockIError getError(in AString aErrorCode);
 * @see flockIWebServiceAPI#getError
 */
DiggAPI.prototype.getError =
function DiggAPI_getError(aErrorCode) {
  var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
  error.errorCode = aErrorCode;
  return error;
};

/**
 * flockIError getHttpError(in AString aHttpErrorCode);
 * @see flockIWebServiceAPI#getHttpError
 */
DiggAPI.prototype.getHttpError =
function DiggAPI_getHttpError(aHttpErrorCode) {
  return null;
};


/*************************************************************************
 * Component: DiggService
 *************************************************************************/
function DiggService() {
  FlockSvcUtils.getLogger(this);

  // Determine whether this service has been disabled, and unregister if so.
  var prefService = CC["@mozilla.org/preferences-service;1"]
                    .getService(CI.nsIPrefBranch);
  if (prefService.getPrefType(SERVICE_ENABLED_PREF) &&
     !prefService.getBoolPref(SERVICE_ENABLED_PREF))
  {
    this._logger.debug(SERVICE_ENABLED_PREF + " is FALSE! Not initializing.");
    this.deleteCategories();
    return;
  }

  this._obs = CC["@mozilla.org/observer-service;1"]
              .getService(CI.nsIObserverService);
  this._accountClassCtor = DiggAccount;

  // getWD() also adds the "_acUtils" property.
  this.domains = FlockSvcUtils.getWD(this).getString(CLASS_SHORT_NAME,
                                                     "domains",
                                                     DOMAIN);
  FlockSvcUtils.initService(this, true);

  this._baseUrn = "urn:digg";
  this._serviceUrn = this._baseUrn + ":service";
  this._friendIDMap = {};
  this._friendCount = 0;

  gTrackingCode = this._WebDetective.getString(CLASS_SHORT_NAME,
                                               "trackingCode",
                                               gTrackingCode);
  this._ppSvc = CC["@flock.com/people-service;1"]
                .getService(CI.flockIPeopleService);

  // Initialize member that specifies path for localized string bundle
  this._stringBundlePath = DIGG_PROPERTIES;

  // Initialize API
  gApi = CC[API_CONTRACT_ID].createInstance(CI.flockIDiggAPI);

  var ws = FlockSvcUtils.flockIWebService;
  ws.addDefaultMethod(this, "url");
  ws.addDefaultMethod(this, "getStringBundle");
  ws.addDefaultMethod(this, "isHidden");

  var lws = FlockSvcUtils.flockILoginWebService;
  lws.addDefaultMethod(this, "loginURL");
  lws.addDefaultMethod(this, "tooltip");
  lws.addDefaultMethod(this, "docRepresentsSuccessfulLogin");
  lws.addDefaultMethod(this, "getAccount");
  lws.addDefaultMethod(this, "getAccountIDFromDocument");
  lws.addDefaultMethod(this, "getAccountUrns");
  lws.addDefaultMethod(this, "getAuthenticatedAccount");
  lws.addDefaultMethod(this, "getCredentialsFromForm");
  lws.addDefaultMethod(this, "getSessionValue");
  lws.addDefaultMethod(this, "ownsDocument");
  lws.addDefaultMethod(this, "ownsLoginForm");

  var mws = FlockSvcUtils.flockIMediaWebService;
  mws.addDefaultMethod(this, "getChannel");
  mws.addDefaultMethod(this, "enumerateChannels");
  mws.addDefaultMethod(this, "getMediaItemFormatter");
  mws.addDefaultMethod(this, "getIconForQuery");

  var sws = FlockSvcUtils.flockISocialWebService;
  sws.addDefaultMethod(this, "markAllMediaSeen");

  var rdh = FlockSvcUtils.flockIRichContentDropHandler;
  rdh.addDefaultMethod(this, "_handleTextareaDrop");

  // Default media channels
  var bundle = this.getStringBundle();
  this._channels = {
    "all": {
      title: bundle.GetStringFromName("flock.digg.title.all"),
      supportsSearch: false,
      subChannels: {
        "special:all": {
          title: bundle.GetStringFromName("flock.digg.title.recent"),
          supportsSearch: false
        },
        "special:all-24h": {
          title: bundle.GetStringFromName("flock.digg.title.24h"),
          supportsSearch: false
        },
        "special:all-7d": {
          title: bundle.GetStringFromName("flock.digg.title.7d"),
          supportsSearch: false
        },
        "special:all-30d": {
          title: bundle.GetStringFromName("flock.digg.title.30d"),
          supportsSearch: false
        }
      }
    },
    "news": {
      title: bundle.GetStringFromName("flock.digg.title.news"),
      supportsSearch: false,
      subChannels: {
        "special:news": {
          title: bundle.GetStringFromName("flock.digg.title.recent"),
          supportsSearch: false
        },
        "special:news-24h": {
          title: bundle.GetStringFromName("flock.digg.title.24h"),
          supportsSearch: false
        },
        "special:news-7d": {
          title: bundle.GetStringFromName("flock.digg.title.7d"),
          supportsSearch: false
        },
        "special:news-30d": {
          title: bundle.GetStringFromName("flock.digg.title.30d"),
          supportsSearch: false
        }
      }
    },
     "technology": {
      title: bundle.GetStringFromName("flock.digg.title.technology"),
      supportsSearch: false,
      subChannels: {
        "special:technology": {
          title: bundle.GetStringFromName("flock.digg.title.recent"),
          supportsSearch: false
        },
        "special:technology-24h": {
          title: bundle.GetStringFromName("flock.digg.title.24h"),
          supportsSearch: false
        },
        "special:technology-7d": {
          title: bundle.GetStringFromName("flock.digg.title.7d"),
          supportsSearch: false
        },
        "special:technology-30d": {
          title: bundle.GetStringFromName("flock.digg.title.30d"),
          supportsSearch: false
        }
      }
    },
    "world_business": {
      title: bundle.GetStringFromName("flock.digg.title.world_business"),
      supportsSearch: false,
      subChannels: {
        "special:world_business": {
          title: bundle.GetStringFromName("flock.digg.title.recent"),
          supportsSearch: false
        },
        "special:world_business-24h": {
          title: bundle.GetStringFromName("flock.digg.title.24h"),
          supportsSearch: false
        },
        "special:world_business-7d": {
          title: bundle.GetStringFromName("flock.digg.title.7d"),
          supportsSearch: false
        },
        "special:world_business-30d": {
          title: bundle.GetStringFromName("flock.digg.title.30d"),
          supportsSearch: false
        }
      }
    },
    "science": {
      title: bundle.GetStringFromName("flock.digg.title.science"),
      supportsSearch: false,
      subChannels: {
        "special:science": {
          title: bundle.GetStringFromName("flock.digg.title.recent"),
          supportsSearch: false
        },
        "special:science-24h": {
          title: bundle.GetStringFromName("flock.digg.title.24h"),
          supportsSearch: false
        },
        "special:science-7d": {
          title: bundle.GetStringFromName("flock.digg.title.7d"),
          supportsSearch: false
        },
        "special:science-30d": {
          title: bundle.GetStringFromName("flock.digg.title.30d"),
          supportsSearch: false
        }
      }
    },
    "gaming": {
      title: bundle.GetStringFromName("flock.digg.title.gaming"),
      supportsSearch: false,
      subChannels: {
        "special:gaming": {
          title: bundle.GetStringFromName("flock.digg.title.recent"),
          supportsSearch: false
        },
        "special:gaming-24h": {
          title: bundle.GetStringFromName("flock.digg.title.24h"),
          supportsSearch: false
        },
        "special:gaming-7d": {
          title: bundle.GetStringFromName("flock.digg.title.7d"),
          supportsSearch: false
        },
        "special:gaming-30d": {
          title: bundle.GetStringFromName("flock.digg.title.30d"),
          supportsSearch: false
        }
      }
    },
    "lifestyle": {
      title: bundle.GetStringFromName("flock.digg.title.lifestyle"),
      supportsSearch: false,
      subChannels: {
        "special:lifestyle": {
          title: bundle.GetStringFromName("flock.digg.title.recent"),
          supportsSearch: false
        },
        "special:lifestyle-24h": {
          title: bundle.GetStringFromName("flock.digg.title.24h"),
          supportsSearch: false
        },
        "special:lifestyle-7d": {
          title: bundle.GetStringFromName("flock.digg.title.7d"),
          supportsSearch: false
        },
        "special:lifestyle-30d": {
          title: bundle.GetStringFromName("flock.digg.title.30d"),
          supportsSearch: false
        }
      }
    },
    "entertainment": {
      title: bundle.GetStringFromName("flock.digg.title.entertainment"),
      supportsSearch: false,
      subChannels: {
        "special:entertainment": {
          title: bundle.GetStringFromName("flock.digg.title.recent"),
          supportsSearch: false
        },
        "special:entertainment-24h": {
          title: bundle.GetStringFromName("flock.digg.title.24h"),
          supportsSearch: false
        },
        "special:entertainment-7d": {
          title: bundle.GetStringFromName("flock.digg.title.7d"),
          supportsSearch: false
        },
        "special:entertainment-30d": {
          title: bundle.GetStringFromName("flock.digg.title.30d"),
          supportsSearch: false
        }
      }
    },
    "sports": {
      title: bundle.GetStringFromName("flock.digg.title.sports"),
      supportsSearch: false,
      subChannels: {
        "special:sports": {
          title: bundle.GetStringFromName("flock.digg.title.recent"),
          supportsSearch: false
        },
        "special:sports-24h": {
          title: bundle.GetStringFromName("flock.digg.title.24h"),
          supportsSearch: false
        },
        "special:sports-7d": {
          title: bundle.GetStringFromName("flock.digg.title.7d"),
          supportsSearch: false
        },
        "special:sports-30d": {
          title: bundle.GetStringFromName("flock.digg.title.30d"),
          supportsSearch: false
        }
      }
    },
    "offbeat": {
      title: bundle.GetStringFromName("flock.digg.title.offbeat"),
      supportsSearch: false,
      subChannels: {
        "special:offbeat": {
          title: bundle.GetStringFromName("flock.digg.title.recent"),
          supportsSearch: false
        },
        "special:offbeat-24h": {
          title: bundle.GetStringFromName("flock.digg.title.24h"),
          supportsSearch: false
        },
        "special:offbeat-7d": {
          title: bundle.GetStringFromName("flock.digg.title.7d"),
          supportsSearch: false
        },
        "special:offbeat-30d": {
          title: bundle.GetStringFromName("flock.digg.title.30d"),
          supportsSearch: false
        }
      }
    },
   "videos": {
      title: bundle.GetStringFromName("flock.digg.title.videos"),
      supportsSearch: false,
      subChannels: {
        "special:videos": {
          title: bundle.GetStringFromName("flock.digg.title.recent"),
          supportsSearch: false
        },
        "special:videos-24h": {
          title: bundle.GetStringFromName("flock.digg.title.24h"),
          supportsSearch: false
        },
        "special:videos-7d": {
          title: bundle.GetStringFromName("flock.digg.title.7d"),
          supportsSearch: false
        },
        "special:videos-30d": {
          title: bundle.GetStringFromName("flock.digg.title.30d"),
          supportsSearch: false
        }
      }
    },
    "images": {
      title: bundle.GetStringFromName("flock.digg.title.images"),
      supportsSearch: false,
      subChannels: {
        "special:images": {
          title: bundle.GetStringFromName("flock.digg.title.recent"),
          supportsSearch: false
        },
        "special:images-24h": {
          title: bundle.GetStringFromName("flock.digg.title.24h"),
          supportsSearch: false
        },
        "special:images-7d": {
          title: bundle.GetStringFromName("flock.digg.title.7d"),
          supportsSearch: false
        },
        "special:images-30d": {
          title: bundle.GetStringFromName("flock.digg.title.30d"),
          supportsSearch: false
        }
      }
    }
  };
}

DiggService.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME,
  CLASS_ID,
  CONTRACT_ID,
  DiggService,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.flockIDiggService,
    CI.flockILoginWebService,
    CI.flockIMediaWebService,
    CI.flockIPollingService,
    CI.flockISocialWebService,
    CI.flockIWebService,
    CI.flockIReaderExtension,
    CI.flockIRichContentDropHandler
  ]
);

DiggService.prototype._xpcom_categories = [
  { category: "wsm-startup" },
  { category: "flockMediaProvider", entry: CLASS_SHORT_NAME },
  { category: "flockWebService", entry: CLASS_SHORT_NAME },
  { category: "flockReaderExtension", entry: CONTRACT_ID },
  { category: "flockRichContentHandler", entry: CLASS_SHORT_NAME }
];

DiggService.prototype.isFriend =
function DiggService_isFriend(aUsername, aFriendUsername, aFlockListener) {
  this._logger.info(".isFriend('" + aUsername + "' , '"
                                  + aFriendUsername + "', ...)");
  gApi.isFriend(aUsername, aFriendUsername, aFlockListener);
};

/**************************************************************************
 * DiggService: flockIWebService Implementation
 **************************************************************************/

// readonly attribute AString contractId;
// ALMOST duplicated by nsIClassInfo::contractID
// with different case: contractId != contractID
DiggService.prototype.contractId = CONTRACT_ID;

// readonly attribute AString icon;
DiggService.prototype.icon = FAVICON;

// readonly attribute AString shortName;
DiggService.prototype.shortName = CLASS_SHORT_NAME;

// readonly attribute AString title;
DiggService.prototype.title = CLASS_TITLE;

// readonly attribute AString urn;
DiggService.prototype.urn = "urn:" + CLASS_SHORT_NAME + ":service";

// DEFAULT: nsIStringBundle getStringBundle();
// DEFAULT: boolean isHidden();


/**************************************************************************
 * DiggService: flockILoginWebService Implementation
 **************************************************************************/

// readonly attribute AString domains;
// gets set in _init()

// readonly attribute AString loginURL;
// gets set in _init()

// readonly attribute boolean needPassword;
DiggService.prototype.needPassword = true;

/**
 * flockIWebServiceAccount addAccount(in AString aAccountId,
 *                                    in boolean aIsTransient,
 *                                    in flockIListener aFlockListener);
 * @see flockILoginWebService#addAccount
 */
DiggService.prototype.addAccount =
function DiggService_addAccount(aAccountId, aIsTransient, aFlockListener) {
  this._logger.info(".addAccount('" + aAccountId + "', "
                                    + aIsTransient + ", ...)");

  var url = this._WebDetective
                .getString(CLASS_SHORT_NAME, "profileURL", null)
                .replace("%accountid%", aAccountId);
  var accountUrn = this._acUtils.createAccount(this,
                                               aAccountId,
                                               aAccountId,
                                               url,
                                               aIsTransient);
  // Instantiate account component
  var account = this.getAccount(accountUrn);
  account.setParam("service", this._coopObj);
  account.setParam("refreshInterval", REFRESH_INTERVAL);
  account.setCustomParam("lastFriendRefresh", 0);
  account.setCustomParam("diggComments", 0);
  if (aFlockListener) {
    aFlockListener.onSuccess(account, "addAccount");
  }
  return account;
};

// DEFAULT: boolean docRepresentsSuccessfulLogin(in nsIDOMHTMLDocument aDocument);
// DEFAULT: flockIWebServiceAccount getAccount(in AString aAccountURN);
// DEFAULT: AString getAccountIDFromDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: nsIStringEnumerator getAccountUrns();
// DEFAULT: flockIWebServiceAccount getAuthenticatedAccount();
// DEFAULT: flockILoginInfo getCredentialsFromForm(in nsIDOMHTMLFormElement aForm);
// DEFAULT: AString getSessionValue();

/**
 * void logout();
 * @see flockILoginWebService#logout
 */
DiggService.prototype.logout =
function DiggService_logout() {
  this._logger.debug(".logout()");
  this._acUtils.removeCookies(this._WebDetective
                                  .getSessionCookies(CLASS_SHORT_NAME));
  this._acUtils.markAllAccountsAsLoggedOut(this);
  if (gTimers.length > 0) {
    FlockScheduler.cancel(gTimers, 0);
  }
};

// DEFAULT: boolean ownsDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: boolean ownsLoginForm(in nsIDOMHTMLFormElement aForm);

/**
 * void removeAccount(in AString aAccountURN);
 * @see flockILoginWebService#removeAccount
 */
DiggService.prototype.removeAccount =
function DiggService_removeAccount(aAccountURN) {
  this._logger
      .debug("{flockILoginWebService}.removeAccount('" + aAccountURN + "')");
  FlockSvcUtils.getACUtils(this).removeAccount(aAccountURN);
  // We are removing the current account, reset gLoggedInAccountUrn
  if (gLoggedInAccountUrn == aAccountURN) {
    gLoggedInAccountUrn = null;
  }
};

/**
 * void updateAccountStatusFromDocument(in nsIDOMHTMLDocument aDocument,
 *                                      in flockIWebServiceAccount aAccount,
 *                                      in flockIListener aFlockListener);
 * @see flockILoginWebService#updateAccountStatusFromDocument
 */
DiggService.prototype.updateAccountStatusFromDocument =
function DiggService_updateAccountStatusFromDocument(aDocument,
                                                     aAccount,
                                                     aFlockListener)
{
  if (aAccount) {
    if (!this._acUtils.ensureOnlyAuthenticatedAccount(aAccount)) {
      // This account has JUST been logged in
      aAccount.login(aFlockListener);
    }
  } else if (this._WebDetective
                 .detect(CLASS_SHORT_NAME, "loggedout", aDocument, null))
  {
    this._acUtils.markAllAccountsAsLoggedOut(this);
  }
};


DiggService.prototype.getTopStories =
function DiggService_getTopStories(aFlockListener) {
  this._logger.debug(".getTopStories()");
  if (this._topStories && this._topStories.getProperty(TOP_STORIES_STR)) {
    aFlockListener.onSuccess(this._topStories, "getTopStories");
  } else {
    this._initRefreshTopStories();
  }
};

/**************************************************************************
 * DiggService: flockISocialWebService Implementation
 **************************************************************************/

// readonly attribute long maxStatusLength;
DiggService.prototype.maxStatusLength = 0;

// DEFAULT: void markAllMediaSeen(in AString aPersonUrn);

/**
 * nsIDOMElement parseStatus(in nsIDOMDocument aDocument,
 *                           in AString aStatusMessage)
 * @see flockISocialService#parseStatus
 */
DiggService.prototype.parseStatus =
function DiggService_parseStatus(aDocument, aStatusMessage) {
  return null;
};

/**************************************************************************
 * DiggService: flockIPageRankingWebService Implementation
 **************************************************************************/

DiggService.prototype.loadDiggInfo =
function DiggService_loadDiggInfo(aURL, aFlockListener, aShouldGetTopStories) {
  this._logger.info(".loadDiggInfo('" + aURL + "', ...)");
  gApi.getDiggInformation(aURL, aFlockListener, aShouldGetTopStories);
};

DiggService.prototype.getDiggInfoByCleanTitle =
function DiggService_getDiggInfoByCleanTitle(aTitle, aFlockListener) {
  this._logger.info(".getDiggInfoByCleanTitle('" + aTitle + "', ...)");
  gApi.getDiggInfoByCleanTitle(aTitle, aFlockListener);
};

/**************************************************************************
 * DiggService: flockIMediaWebService Implementation
 **************************************************************************/

// readonly attribute boolean flockIMediaWebService::supportsUsers
DiggService.prototype.supportsUsers = true;

/**
 * void decorateForMedia(in nsIDOMHTMLDocument aDocument,
 *                       in flockIWebServiceAccount aAccount);
 * @see flockIMediaWebService#decorateForMedia
 */
DiggService.prototype.decorateForMedia =
function DiggService_decorateForMedia(aDocument, aAccount) {
  this._logger.debug(".decorateForMedia()");
  var results = CC["@mozilla.org/hash-property-bag;1"]
                .createInstance(CI.nsIWritablePropertyBag2);
  var inst = this; 
  if (this._WebDetective.detect("digg", "person", aDocument, results)) {
    var userId = results.getPropertyAsAString("userid");
    var decorateListener = {
      onSuccess: function decorateListener_onSuccess(aSubject, aTopic) {
        var person = aSubject.QueryInterface(CI.nsIPropertyBag);
        var username;
        try {
          username = person.getProperty("name");
        } catch (ex) {
          username = userId; 
        }

        var mediaArr = [];
        var media = {
          name: username,
          query: "user:" + userId + "|username:" + username,
          label: username,
          favicon: inst.icon,
          service: inst.shortName
        }
        mediaArr.push(media);
        if (!aDocument._flock_decorations) {
          aDocument._flock_decorations = {};
        }

        if (aDocument._flock_decorations.mediaArr) {
          aDocument._flock_decorations.mediaArr =
            aDocument._flock_decorations.mediaArr.concat(mediaArr);
        } else {
          aDocument._flock_decorations.mediaArr = mediaArr;
        }
        inst._obs.notifyObservers(aDocument, "media", "media:update");
      },
      onError: function decorateListener_onError(aFlockError, aTopic) {
        inst._logger.error("getUser_onError(..., '" + aTopic + "', ...)");
      }
    };
    gApi.getUser(userId, decorateListener);
  }
};

/**
 * void findByUsername(in flockIListener aFlockListener, in AString aUsername);
 * @see flockIMediaWebService#findByUsername
 */
DiggService.prototype.findByUsername =
function DiggService_findByUsername(aFlockListener, aUsername) {
  this._logger.debug(".findByUsername(..., '" + aUsername + "')");
  aFlockListener.onError(null, aUsername);
};

/**
 * void getAlbums(in flockIListener aFlockListener, in AString aUsername);
 * @see flockIMediaWebService#getAlbums
 */
DiggService.prototype.getAlbums =
function DiggService_getAlbums(aFlockListener, aUsername) {
  this._logger.debug(".getAlbums(..., '" + aUsername + "')");
  aFlockListener.onError(null, aUsername);
};

// DEFAULT: flockIMediaChannel getChannel(in AString aChannelId);

// DEFAULT: nsISimpleEnumerator enumerateChannels();

// DEFAULT: flockIMediaItemFormatter getMediaItemFormatter();

/**
 * void search(in flockIListener aFlockListener, in AString aQuery,
               in long aCount, in long aPage, in AString aRequestId);
 * @see flockIMediaWebService#search
 */
DiggService.prototype.search =
function DiggService_search(aFlockListener, aQuery, aCount, aPage, aRequestId) {
  this._logger.debug(".search(..., '" + aQuery + "', '" + aCount + "', '"
                                      + aPage + "', '" + aRequestId +  "')");
  var query = new QueryHelper(aQuery);

  // If the search string only contains whitespace, then return empty result
  var querySearch = query.getKey("search");
  if (querySearch && (querySearch.replace(/\s/g, "") == "")) {
    aFlockListener.onSuccess(createEnum([]), aRequestId);
    return;
  }

  var inst = this;
  var flockListener = {
    onSuccess: function sl_onSuccess(aSubject, aTopic) {
      inst._logger.debug("sl_onResult()");
      var resultsArray = inst._handleUserResults(aSubject.wrappedJSObject);
      aFlockListener.onSuccess(createEnum(resultsArray), aRequestId);
    },
    onError: function sl_onError(aFlockError, aTopic) {
      inst._logger.debug("sl_onError()");
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  // Digg uses a 1-based page index
  var page = aPage;
  if (aPage == 0) {
    page = 1;
  }
  var params = {
    count: aCount,
    offset: (page - 1) * aCount
  };

  var method;
  if (query.hasKey("special")) {
    var specialArray = query.getKey("special").split("-");
    var container = specialArray[0];
    var timeFrame = specialArray[1];
    if (container == "all" ||
        container == "news" ||
        container == "videos" ||
        container == "images")
    {
      method = "stories/popular";
    } else {
      method = "stories/container/" + container + "/popular";
    }

    if (container == "news" || container == "videos" || container == "images") {
      params.media = container;
    }

    if (timeFrame) {
      // Hash table that converts timeFrame into number of seconds
      var timeFrameHash = {
        "24h": 86400,
        "7d":  604800,
        "30d": 2592000
      };

      var maxSubmitDate = Math.round(new Date().getTime() / 1000);
      var minSubmitDate = maxSubmitDate - timeFrameHash[timeFrame];
      params.max_submit_date = maxSubmitDate;
      params.min_submit_date = minSubmitDate;
      params.sort = "digg_count-desc";
    }
  } else {
    method = "user/" + query.user + "/dugg";
  }

  gApi.call(method,
            { wrappedJSObject: params },
            null,
            CI.flockIWebServiceAPI.GET,
            flockListener);
};

DiggService.prototype._handleUserResults =
function DiggService__handleUserResults(aJson) {
  var stories = [];
  if (aJson.stories) {
    var inst = this;
    for (var i = 0; (i < aJson.stories.length); i++) {
      var newMediaItem = CC["@flock.com/photo;1"]
                         .createInstance(CI.flockIMediaItem);
      newMediaItem.init(inst.shortName, flockMediaItemFormatter);
      newMediaItem.webPageUrl = addTrackingCode(aJson.stories[i].href);
      var thumb;
      if (aJson.stories[i].thumbnail) {
       thumb = aJson.stories[i].thumbnail.src;
      } else {
       thumb = "chrome://flock/skin/services/digg/diggNoImage.png";
      }
      newMediaItem.thumbnail = thumb;
      newMediaItem.midSizePhoto = thumb;
      newMediaItem.largeSizePhoto = thumb;
      var name = aJson.stories[i].user.name;
      var fullname = aJson.stories[i].user.fullname;
      newMediaItem.username = (fullname) ? fullname : name;
      newMediaItem.userid = name;
      var icon;
      if (aJson.stories[i].user.icon == DIGG_NO_AVATAR_ICON) {
       icon = FLOCK_NO_AVATAR_ICON;
      } else {
       icon = aJson.stories[i].user.icon;
      }
      newMediaItem.icon = icon;
      newMediaItem.is_public = true;
      newMediaItem.is_video = false;

      switch (aJson.stories[i].media) {
        case "news":
          newMediaItem.mediaType = CI.flockIMediaItem.MEDIA_TYPE_NEWS;
          break;
        case "images":
          newMediaItem.mediaType = CI.flockIMediaItem.MEDIA_TYPE_IMAGE;
          break;
        case "videos":
          newMediaItem.mediaType = CI.flockIMediaItem.MEDIA_TYPE_VIDEO;
          break;
        case "podcasts":
          newMediaItem.mediaType = CI.flockIMediaItem.MEDIA_TYPE_PODCAST;
          break;
        default:
          this._logger.debug("No media type found for media item for - "
                             + aJson.stories[i].media + "\n");
          break;
      }

      newMediaItem.title = aJson.stories[i].title;
      newMediaItem.id = aJson.stories[i].id;
      newMediaItem.uploadDate = aJson.stories[i].submit_date * 1000;
      newMediaItem.rating_count = aJson.stories[i].diggs;
      newMediaItem.comment_count = aJson.stories[i].comments;
      stories.push(newMediaItem);
    }
  }
  return stories;
};

/**
 * void supportsSearch(in AString aQuery);
 * @see flockIMediaWebService#supportsSearch
 */
DiggService.prototype.supportsSearch =
function DiggService_supportsSearch(aQuery) {
  return false;
};

/**************************************************************************
 * DiggService: flockIPollingService Implementation
 **************************************************************************/

/**
 * void refresh(in AString aUrn, in flockIPollingListener aPollingListener);
 * @see flockIPollingService#refresh
 */
DiggService.prototype.refresh =
function DiggService_refresh(aUrn, aPollingListener) {
  this._logger.info(".refresh('" + aUrn + "', ...)");
  var acct = this.getAccount(aUrn).QueryInterface(CI.flockISocialAccount);

  if (acct) {
    this._logger.debug("refreshing an Account");
    if (acct.isAuthenticated()) {
      this._refreshAccount(aUrn, aPollingListener);
    } else {
      // If the user is not logged in, return a success without
      // refreshing anything
      aPollingListener.onResult();
    }
  } else {
    this._logger.error("can't refresh " + aUrn + " (unsupported type)");
    aPollingListener.onError(null);
  }
};

/**************************************************************************
 * DiggService: flockIReaderExtension Implementation
 **************************************************************************/
DiggService.prototype.styleSheetUrl = DIGG_READER_CSS_URL;

DiggService.prototype.createFooterElement =
function DiggService_createFooterElement(aDocument, aURI, aFlockListener) {
  var inst = this;
  var bundle = this.getStringBundle();
  var checkDiggInfo_Listener = {
    onSuccess: function getDiggInfo_onSuccess(aResult, aTopic) {
      aResult.QueryInterface(CI.nsIPropertyBag);
      // Container for digg output
      var diggContainer = aDocument.createElement("div");
      diggContainer.setAttribute("id", "digg-container");
      if (aResult.getProperty("diggs") &&
          aResult.getProperty("diggs") == "0")
      {
        // No diggs; but contains a "digg it" URL
        var diggItUrl = aResult.getProperty("diggUrl");
        var diggItTxt = bundle.GetStringFromName("flock.feed.streamReader.digg.diggIt");
        var diggItLink = aDocument.createElement("a");
        diggItLink.setAttribute("href", diggItUrl);
        diggItLink.innerHTML = diggItTxt;

        var diggSpanDiggIt = aDocument.createElement("span");
        diggSpanDiggIt.setAttribute("id", "digg-diggit");
        diggSpanDiggIt.appendChild(diggItLink);
        diggContainer.appendChild(diggSpanDiggIt);
      } else {
        var diggs = aResult.getProperty("diggs");
        var link = aResult.getProperty("link");
        if (diggs && diggs != "0") {
          // Number of diggs
          var diggBundle;
          var diggTxt
          if (diggs == "1") {
            diggBundle = "flock.feed.streamReader.digg.digg";
            diggTxt = bundle.GetStringFromName(diggBundle);
          } else {
            diggBundle = "flock.feed.streamReader.digg.diggs";
            diggTxt = bundle.formatStringFromName(diggBundle, [diggs], 1);
          }
          var diggCount = aDocument.createElement("a");
          diggCount.setAttribute("href", link);
          diggCount.innerHTML = diggTxt;

          // Number of comments
          var commentCount = aDocument.createElement("a");
          var commentBundle = "flock.feed.streamReader.digg.comments";
          var parameters = [aResult.getProperty("comments")];
          var commentTxt = bundle.formatStringFromName(commentBundle,
                                                       parameters,
                                                       1);
          commentCount.setAttribute("href", link);
          commentCount.innerHTML = commentTxt;

          // Assemble and append output
          var diggSpanDiggs = aDocument.createElement("span");
          diggSpanDiggs.setAttribute("id", "digg-diggs");
          diggSpanDiggs.appendChild(diggCount);

          var diggSpanComments = aDocument.createElement("span");
          diggSpanComments.setAttribute("id", "digg-comments");
          diggSpanComments.appendChild(commentCount);

          diggContainer.appendChild(diggSpanDiggs);
          diggContainer.appendChild(diggSpanComments);
        }
      }
      aFlockListener.onSuccess(diggContainer, null);
    },
    onError: function getDiggInfo_onError(aFlockError, aResult) {
      inst._logger.error("checkDiggInfo_Listener_onError");
      aFlockListener.onError(aFlockError, aResult);
    }
  };

  // Check to see if the feed item is from digg.com
  if (aURI.spec.indexOf(DIGG_URL) != -1) {
    // Digg URLs come in the following form: http://digg.com/{cleanTitle}
    // Extract URL for use in a 'Clean Title' API call.
    var cleanTitle = aURI.spec.substring(aURI.spec.lastIndexOf("/") + 1);
    this.getDiggInfoByCleanTitle(cleanTitle, checkDiggInfo_Listener);
  } else {
    this.loadDiggInfo(aURI.spec, checkDiggInfo_Listener, false);
  }
};

/**************************************************************************
 * DiggService: flockIRichContentDropHandler Implementation
 **************************************************************************/

// boolean handleDrop(in nsITransferable aFlavours,
//                    in nsIDOMHTMLElement aTargetElement);
DiggService.prototype.handleDrop =
function DiggService_handleDrop(aFlavours, aTargetElement) {
  // Code cloned from Facebook and Flickr services with some tweaks
  this._logger.debug(".handleDrop()");

  // Handle textarea drops
  if (aTargetElement instanceof CI.nsIDOMHTMLTextAreaElement) {
    var dropCallback = function digg_dropCallback(aFlav) {
      var dataObj = {}
      var len = {};
      aFlavours.getTransferData(aFlav, dataObj, len);

      var caretPos = aTargetElement.selectionEnd;
      var currentValue = aTargetElement.value;

      // We'll only add a breadcrumb if there isn't one already present
      var breadcrumb = "";
      var richDnDSvc = CC[FLOCK_RDNDS_CONTRACTID]
                       .getService(CI.flockIRichDNDService);
      if (!richDnDSvc.hasBreadcrumb(aTargetElement)) {
        breadcrumb = richDnDSvc.getBreadcrumb("plain");
      }

      aTargetElement.value = currentValue.substring(0, caretPos)
                           + dataObj.value.QueryInterface(CI.nsISupportsString)
                                    .data.replace(/: /, ":\n")
                           + currentValue.substring(caretPos)
                           + breadcrumb;

      // Set the cursor focus to the textarea in order to activate the
      // CAPTCHA text.
      // c.f. https://bugzilla.flock.com/show_bug.cgi?id=13964
      aTargetElement.focus();
    };

    return this._handleTextareaDrop(CLASS_SHORT_NAME,
                                    this._coopObj.domains,
                                    aTargetElement,
                                    dropCallback);
  }

  // Default handling otherwise
  return false;
};

/**************************************************************************
 * DiggService: private members
 **************************************************************************/

// Get the top Digg stories every X min the service is alive
DiggService.prototype._initRefreshTopStories =
function DiggService__initRefreshTopStories() {
  this._logger.info("._initRefreshTopStories()");

  // Ensure we use a valid refresh interval
  var refreshInterval = FlockPrefsUtils.getIntPref(TOP_STORIES_REFRESH_PREF);
  if (refreshInterval < TOP_STORIES_REFRESH_MIN) {
    refreshInterval = TOP_STORIES_REFRESH_DEFAULT;
    FlockPrefsUtils.setIntPref(TOP_STORIES_REFRESH_PREF, refreshInterval);
  }

  // Call the API to refresh the top stories and store the result in a member
  // variable for later use
  this._topStories = new FlockSvcUtils.newResults();
  this._topStories.setPropertyAsAString(TOP_STORIES_STR, "");
  var inst = this;
  var topStoriesListener = {
    onSuccess: function refreshTopStories_onSuccess(aSubject, aTopic) {
      inst._logger.debug("DiggService: Refreshing top stories: Success!");
      inst._obs.notifyObservers(null, TOP_STORIES_REFRESHED_STR, null);
    },
    onError: function refreshTopStories_onError(aFlockError, aTopic) {
      inst._logger.debug("DiggService: Refreshing top stories: Error!");
      inst._topStories.setPropertyAsAString(TOP_STORIES_STR, "");
      inst._obs.notifyObservers(null, TOP_STORIES_REFRESHED_STR, null);
    }
  };

  var refreshTopStories = {
    notify: function refreshTopStories_notify(aTimer) {
      aTimer.delay = refreshInterval * 1000;
      inst._logger.debug("DiggService: Refreshing top stories...");
      gApi.getTopStories(inst._topStories, topStoriesListener);
    }
  };

  var timer = CC["@mozilla.org/timer;1"].createInstance(CI.nsITimer);
  timer.initWithCallback(refreshTopStories,
                         2000,
                         CI.nsITimer.TYPE_REPEATING_SLACK);
};

DiggService.prototype._refreshAccount =
function DiggService__refreshAccount(aUrn, aPollingListener) {
  this._logger.info("._refreshAccount('" + aUrn + "', ...)");
  var inst = this;
  var account = this.getAccount(aUrn);

  // Make sure we reset our refresh times tracking on change of account.
  if (gLoggedInAccountUrn != aUrn) {
    gLoggedInAccountUrn = aUrn;
    gLastDiggsRefreshTimes = {};
    gLastCommentsRefreshTimes = {};
    this._friendCount = 0;
    this._friendIDMap = {};
  }

  var pplRefreshListener = CC["@flock.com/people-refresh-listener;1"]
                           .createInstance(CI.flockIPeopleRefreshListener);
  pplRefreshListener.init(3, aUrn, aPollingListener, SHORT_INTERVAL);

  function synchFriendsWithServer() {
    // REMOVE locally people removed on the server
    var localFriends = account.enumerateFriends();
    if (!localFriends) {
      // user just deleted the account
      return;
    }
    while (localFriends.hasMoreElements()) {
      var identity = localFriends.getNext();
      if (!inst._friendIDMap[identity.accountId]) {
        inst._logger.debug("Friend " + identity.accountId
                           + " has been deleted on the server");
        identity.destroy();
      }
    }
  }

  // This listener handles getting the list of friends.
  var getFriends_Listener = {
    onSuccess: function getFriends_onSuccess(aSubject, aTopic) {
      inst._logger.debug("getFriends_onSuccess(..., '" + aTopic + "')");
      
      // We now have our list of friends.
      var friends = aSubject.wrappedJSObject.friends;
      inst._friendCount = 0;
      inst._friendIDMap = {};

      var friendsUpdated_Listener = {
        onSuccess: function fs_onSuccess(aSubject, aTopic) {
          synchFriendsWithServer();

          pplRefreshListener.onSuccess(null, "friendsUpdated_Listener");
        },
        onError: function fs_onError(aFlockError, aTopic) {
          var error = CC["@flock.com/error;1"].createInstance(CI.flockIError);
          error.errorCode = CI.flockIError.PEOPLE_FRIEND_REFRESH;
          pplRefreshListener.onError(error, "friendsUpdated_Listener");
        }
      };

      // We have our list of friend so let's go flesh out the data for them.
      inst._updateFriends(account, friends, friendsUpdated_Listener);
    },
    onError: function getFriends_onError(aFlockError, aTopic) {
      inst._logger.error("getFriends_onError(..., '" + aTopic + "', ...)");
      pplRefreshListener.onError(aFlockError, "getFriends_Listener");
    }
  };

  var getUser_Listener = {
    onSuccess: function getUser_onSuccess(aSubject, aTopic) {
      inst._logger.debug("getUser_onSuccess(..., '" + aTopic + "')");
      aSubject.QueryInterface(CI.nsIPropertyBag);
      account.setParam("name", aSubject.getProperty("name"));
      account.setParam("screenName", aSubject.getProperty("screenName"));
      var avatar = "";
      if (aSubject.getProperty("avatar") != DIGG_NO_AVATAR_ICON) {
        avatar = aSubject.getProperty("avatar");
      }
      account.setParam("avatar", avatar);
      account.setParam("URL", aSubject.getProperty("URL"));
      inst._obs.notifyObservers(account, "flock-acct-refresh", "user-info");
      pplRefreshListener.onSuccess(null, "getUser_Listener");
    },
    onError: function userShowApiListener_onError(aFlockError, aTopic) {
      inst._logger.error("getUser_onError(..., '" + aTopic + "', ...)");
      pplRefreshListener.onError(aFlockError, "getUser_Listener");
    }
  };

  var getShouts_Listener = {
    onSuccess: function getShouts_onSuccess(aSubject, aTopic) {
      inst._logger.debug("getShouts_onSuccess(..., '" + aTopic + "')");
      inst._obs.notifyObservers(account, "flock-acct-refresh", "user-info");
      pplRefreshListener.onSuccess(null, "getShouts_Listener");
    },
    onError: function getShouts_onError(aFlockError, aTopic) {
      inst._logger.error("getShouts_onError(..., '" + aTopic + "', ...)");
      pplRefreshListener.onError(aFlockError, "getShouts_Listener");
    }
  };

  var uid = account.getParam("accountId");
  this._getMyShouts(getShouts_Listener, account);
  gApi.getUser(uid, getUser_Listener);
  gApi.getFriends(uid, getFriends_Listener);
};

DiggService.prototype._getMyShouts =
function DiggService__getMyShouts(aListener, aAccount) {
  this._logger.debug("._getMyShouts()");
  var inst = this;

  var hr = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
           .createInstance(CI.nsIXMLHttpRequest);
  hr.onreadystatechange = function getMyShouts_onreadystatechange(aEvent) {
    if (hr.readyState == 4) {
      var results = FlockSvcUtils.newResults();
      if (inst._WebDetective.detectNoDOM("digg", "shouts", "",
                                         hr.responseText, results))
      {
        var shoutCount = results.getPropertyAsAString("shoutCount");
        inst._logger.debug("new shouts - " + shoutCount);
        if (aAccount.getParam("accountMessages") < shoutCount) {
          inst._ppSvc.togglePeopleIcon(true);
        }
        aAccount.setParam("accountMessages", shoutCount);
      } else {
        aAccount.setParam("accountMessages", 0);
      }
      aListener.onSuccess(null, "_getMyShouts");
    }
  };
  var url = this._WebDetective
                .getString(CLASS_SHORT_NAME, "friendActivityURL", null)
                .replace("%accountid%", aAccount.getParam("accountId"));
  hr.open("GET", url);
  hr.send(null);
};

DiggService.prototype._updateFriends =
function DiggService__updateFriends(aAccount, aFriends, aListener) {
  // Depending on how long it has been since the last refresh we update we
  // choose to update each friend individually or we update as a batch.
  var lastFriendRefresh = aAccount.getCustomParam("lastFriendRefresh");
  var oneHrAgo = Math.round(Date.now()/1000) - FRIEND_INTERVAL_FETCH;
  // If a refresh was recently made, do an optimized refresh.
  var optimized = lastFriendRefresh >= oneHrAgo;

  this._processFriends(aAccount, aFriends, aListener, optimized);
  aAccount.setCustomParam("lastFriendRefresh", Math.round(Date.now()/1000));
};

/**
 * void getFriendsActivity(in flockISocialAccount aAccount,
                           in nsIPropertyBag aFriendsIds,
                           in flockIListener aFlockListener);
 * @see flockIDiggAPI#getFriendsActivity
 */
DiggService.prototype.getFriendsActivity =
function DiggService_getFriendsActivity(aAccount, aFriendsIds, aFlockListener)
{
  var rKey = aFriendsIds.length == 1 ? aFriendsIds[0] : aAccount.urn;
  var inst = this;
  var friendsIds = aFriendsIds.join(",");
  var stories = [];
  var friendDiggGetter = {
    listener: {
      onSuccess: function gbfa_fdg_listener_onSuccess(aSubject, aTopic) {
        inst._logger.info("gbfa friendDiggGetter.listener.onSuccess ");

        // Record the timestamp for this query so that we can optimize the next
        // time we make a query for diggs.
        gLastDiggsRefreshTimes[rKey] = aSubject.wrappedJSObject.timestamp;
        for each (var digg in aSubject.wrappedJSObject.diggs) {
          var story = stories[digg.user];
          var lastDate = story ? story.date
                               : inst._friendIDMap[digg.user].lastUpdate;
          if (digg.date > lastDate) {
            stories[digg.user] = digg;
          }
        }
        friendDiggGetter.updateAllUsersStories();
      },
      onError: function gbfa_fdg_listener_onError(aFlockError, aTopic) {
        inst._logger.error("gbfa friendDiggGetter.listener.onError(..., '"
                           + aTopic + "')");
        friendDiggGetter.updateAllUsersStories();
      }
    },
    getDiggs: function gbfa_fdg_getDiggs() {
      if (!aAccount.isAuthenticated()) {
        return;
      }
      var method = "users/" + friendsIds + "/diggs";
      var params = null;
      if (gLastDiggsRefreshTimes[rKey]) {
        params = {
          wrappedJSObject: {
            min_date: gLastDiggsRefreshTimes[rKey]
          }
        };
      }
      gApi.call(method,
                params,
                null,
                CI.flockIWebServiceAPI.GET,
                this.listener);
    },
    updateAllUsersStories: function gbfa_fdg_updateAllUsersStories() {
      for each (friendId in aFriendsIds) {
        if (!aAccount.isAuthenticated()) {
          return;
        }
        inst._updateRecentUrlForUser(stories[friendId],
                                     inst._friendIDMap[friendId],
                                     aFlockListener);
      }
    }
  };
  // Get friends' comments.
  var getFriendComment_Listener = {
    onSuccess: function getFriendComment_onSuccess(aSubject, aTopic) {
      inst._logger.info("getFriendComment_onSuccess ");
      try {
        // Record the timestamp for this query so that we can optimize the next
        // time we make a query for comments.
        gLastCommentsRefreshTimes[rKey] = aSubject.wrappedJSObject.timestamp;
        var comments = aSubject.wrappedJSObject.comments;
        if (comments && parseInt(aSubject.wrappedJSObject.total) > 0) {
          for each (var comment in comments) {
            var story = stories[comment.user];
            var lastDate = story ? story.date 
                                 : inst._friendIDMap[comment.user].lastUpdate;
            if (comment.date > lastDate) {
              stories[comment.user] = comment;
              inst._ppSvc.togglePeopleIcon(true);
            }
            var commentNbs = aAccount.getCustomParam("diggComments");
            aAccount.setCustomParam("diggComments", commentNbs + 1);
          }
        }
        friendDiggGetter.getDiggs();
      } catch (ex) {
        inst._logger.error("getFriendComment_Listener.onSuccess " + ex);
        friendDiggGetter.getDiggs();
      }
    },
    onError: function getFriendComment_onError(aFlockError, aTopic) {
      inst._logger.error("getFriendsActivity "
                         + "getFriendComment_onError('" + aTopic + "')");
      friendDiggGetter.getDiggs();
    }
  };

  var method = "users/" + aFriendsIds + "/comments";
  var params = null;
  if (gLastCommentsRefreshTimes[rKey]) {
    params = {
      wrappedJSObject: {
        min_date: gLastCommentsRefreshTimes[rKey]
      }
    };
  }
  gApi.call(method,
            params,
            null,
            CI.flockIWebServiceAPI.GET,
            getFriendComment_Listener);
};


DiggService.prototype._updateRecentUrlForUser =
function DiggService__updateRecentUrlForUser(aStory, aFriend, aFlockListener)
{
  if (!aStory) {
    aFlockListener.onSuccess(aStory, "_updateRecentUrlForUser");
    return;
  }
  var inst = this;
  var updateRecentUrlForUser_Listener = {
    onSuccess: function updateRecentUrlForUser_onSuccess(aSubject, aTopic) {
      // There could be several stories related to this URL; we will only
      // deal with the most recent one.
      var storyDetails = aSubject.wrappedJSObject.stories[0];
      inst._logger.debug("updateRecentUrlForUser_onSuccess(..., '"
                         + aTopic + "') " + aStory.name);
      if (storyDetails) {
        if (aStory.replies != null) {
          aFriend.lastUpdateType = "commented";
        } else {
          if (storyDetails.user.name == aFriend.accountId) {
            aFriend.lastUpdateType = "submitted";
          } else {
            aFriend.lastUpdateType = "dugg";
            storyDetails.title += " (" + storyDetails.diggs + ")";
          }
          aFriend.unseenMedia++;
        }
        aFriend.statusMessage = storyDetails.title;
        aFriend.statusMessageUrl = addTrackingCode(storyDetails.href);
      }
      aFriend.lastUpdate = aStory.date;  // to trigger the RDF observers and update the side bar
      aFlockListener.onSuccess(aStory, "_updateRecentUrlForUser");
    },
    onError: function updateRecentUrlForUser_onError(aFlockError, aTopic) {
      inst._logger.error("updateRecentUrlForUser_onError('" + aTopic + "')");
      aFlockListener.onError(aFlockError, "");
    }
  };
  gApi.call("stories/" + aStory.story,
            null,
            null,
            CI.flockIWebServiceAPI.GET,
            updateRecentUrlForUser_Listener);
};

DiggService.prototype._updateFriend =
function DiggService__updateFriend(aAccount, aFriend, aListener) {
  // Update friend's Activity.
  var inst = this;
  var activityListener = {
    onSuccess: function activityListener_onSuccess(aSubject, aTopic) {
      inst._logger.info("getFriendActivity_onSuccess(...,'" + aTopic + "')");
      aListener.onSuccess(aSubject, aTopic);
    },
    onError: function activityListener_onError(aFlockError, aTopic) {
      inst._logger.error("getFriendActivity_onError('" + aTopic + "')");
      aListener.onError(aFlockError, aTopic);
    }
  };
  var friendBag = [];
  friendBag.push(aFriend.accountId);
  this.getFriendsActivity(aAccount, friendBag, activityListener);
};

DiggService.prototype._processFriends =
function DiggService__processFriends(aAccount, aFriends, aListener, aOptimized)
{
  var inst = this;
  var processCount = 0;
  var activityFlockListener = {
    onSuccess: function activityFlockListener_onSuccess(aSubject, aTopic) {
      processCount++;
      inst._logger.info("_processFriends on Success(...,'" + aTopic + "') "
                        + processCount + "/" +inst._friendCount);
      if (processCount >= inst._friendCount) {
        aListener.onSuccess(null, "friendsProcessed");
      }
    },
    onError: function activityFlockListener_onError(aFlockError, aTopic) {
      inst._logger.error("_processFriends getFriendActivity_onError('"
                         + aTopic + "')");
    }
  };

  // Break out the friends into 2 lists, once for existing friends and another
  // for new friends.
  var newFriends = [];
  var existingFriends = [];

  for each (var f in aFriends) {
    var friendID = f.getProperty("name");
    var coopFriend = aAccount.coopObj.addPerson({accountId: friendID,
                                                 name: friendID});
    if (coopFriend.screenName == null) {
      // If the person did not exist then set default values.
      coopFriend.screenName = friendID;
      coopFriend.avatar = f.getProperty("avatar");
      coopFriend.lastUpdate = 0; // to trigger the RDF observers and update the side bar
      newFriends.push(coopFriend);
    } else if (aOptimized) {
      newFriends.push(coopFriend);
    } else {
      existingFriends.push(friendID);
    }
    this._friendIDMap[friendID] = coopFriend;
    this._friendCount++;
  }
  if (this._friendCount == 0) {
    aListener.onSuccess(null, "friendsProcessed");
    return;
  }

  // Update existing friends as a batch.
  if (existingFriends.length > 0) {
    this.getFriendsActivity(aAccount, existingFriends, activityFlockListener);
  }

  // Update new friends individually.
  for (i in newFriends) {
    this._updateFriend(aAccount, newFriends[i], activityFlockListener);
  }
};

/*************************************************************************
 * Component: DiggAccount
 *************************************************************************/
function DiggAccount() {
  FlockSvcUtils.getLogger(this);
  this._logger.init("DiggAccount");

  this._WebDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);
  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);

  var wsa = FlockSvcUtils.flockIWebServiceAccount;
  wsa.addDefaultMethod(this, "getAllCustomParams");
  wsa.addDefaultMethod(this, "getCustomParam");
  wsa.addDefaultMethod(this, "getParam");
  wsa.addDefaultMethod(this, "getService");
  wsa.addDefaultMethod(this, "isAuthenticated");
  wsa.addDefaultMethod(this, "logout");
  wsa.addDefaultMethod(this, "setCustomParam");
  wsa.addDefaultMethod(this, "setParam");

  var sa = FlockSvcUtils.flockISocialAccount;
  sa.addDefaultMethod(this, "enumerateFriends");
  sa.addDefaultMethod(this, "getFriendCount");
}

// Use genericComponent() for the goodness it provides (QI, nsIClassInfo, etc),
// but do NOT add this component to the list of constructors passed to
// FlockXPCOMUtils.genericModule().
DiggAccount.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " Account",
  "",
  "",
  DiggAccount,
  0,
  [
    CI.flockIWebServiceAccount,
    CI.flockISocialAccount,
    CI.flockIMediaAccount
  ]
);


/*************************************************************************
 * DiggAccount: flockIWebServiceAccount Implementation
 *************************************************************************/

// readonly attribute AString urn;
DiggAccount.prototype.urn = "";

// DEFAULT: nsIPropertyBag getAllCustomParams();
// DEFAULT: nsIVariant getCustomParam(in AString aParamName);
// DEFAULT: nsIVariant getParam(in AString aParamName);
// DEFAULT: flockILoginWebService getService();
// DEFAULT: boolean isAuthenticated();

/**
 * void keep();
 * @see flockIWebServiceAccount#keep
 */
DiggAccount.prototype.keep =
function DiggAccount_keep() {
  this._logger.debug(".keep()");
  this.coopObj.isTransient = false;
  this._acUtils.makeTempPasswordPermanent("urn:digg:service:"
                                          + this.coopObj.accountId);
};

/**
 * void login(in flockIListener aFlockListener);
 * @see flockIWebServiceAccount#login
 */
DiggAccount.prototype.login =
function DiggAccount_login(aFlockListener) {
  this._logger.debug(".login()");

  this._logger.debug("{flockIWebServiceAccount}.login()");
  // Force refresh on login is done by the people sidebar
  if (aFlockListener) {
    aFlockListener.onSuccess(this, "login");
  }
};

// DEFAULT: void logout(in flockIListener aFlockListener);
// DEFAULT: void setCustomParam(in AString aParamName, in nsIVariant aValue);
// DEFAULT: void setParam(in AString aParamName, in nsIVariant aValue);


/*************************************************************************
 * DiggAccount: flockISocialAccount Implementation
 *************************************************************************/

// readonly attribute boolean hasFriendActions;
DiggAccount.prototype.hasFriendActions = true;

// readonly attribute boolean isMeStatusSupported;
DiggAccount.prototype.isMeStatusSupported = false;

// readonly attribute boolean isFriendStatusSupported;
DiggAccount.prototype.isFriendStatusSupported = true;

// readonly attribute boolean isStatusEditable;
DiggAccount.prototype.isStatusEditable = false;

// readonly attribute boolean isPostLinkSupported;
DiggAccount.prototype.isPostLinkSupported = true;

// readonly attribute boolean isMyMediaFavoritesSupported;
DiggAccount.prototype.isMyMediaFavoritesSupported = false;

// DEFAULT: nsISimpleEnumerator enumerateFriends();
/**
 * AString formatFriendActivityForDisplay(in AString aFriendActivityUrn);
 * @see flockISocialAccount#formatFriendActivityForDisplay
 */
DiggAccount.prototype.formatFriendActivityForDisplay =
function DiggAccount_formatFriendActivityForDisplay(aFriendActivityUrn) {
  this._logger.debug("formatFriendActivityForDisplay("
                     + aFriendActivityUrn  + ")");

  var friendActivity = FlockSvcUtils.getCoop(this)
                                    .get(aFriendActivityUrn);
  var bundle = this.getService().getStringBundle();
  var result = "";

  switch (friendActivity.updateType) {
   case "profile":
     result =
       bundle.GetStringFromName("flock.friendFeed.lastUpdateTypePretty.profile");
     break;
 
   case "commented":
     result = bundle.GetStringFromName("flock.friendFeed.lastUpdateTypePretty.commented")
            + " " + friendActivity.updateValue;
     break;

   case "dugg":
     result = bundle.GetStringFromName("flock.friendFeed.lastUpdateTypePretty.dugg")
            + " " + friendActivity.updateValue;
     break;

   case "submitted":
     result = bundle.GetStringFromName("flock.friendFeed.lastUpdateTypePretty.submitted")
            + " " + friendActivity.updateValue;
     break;
  }

  return flockXMLDecode(result);
};

/**
 * AString formatStatusForDisplay(in AString aStatusMessage);
 * @see flockISocialAccount#formatStatusForDisplay
 */
DiggAccount.prototype.formatStatusForDisplay =
function DiggAccount_formatStatusForDisplay(aStatusMessage) {
  return (aStatusMessage) ? flockXMLDecode(aStatusMessage) : "";
};

/**
 * AString getEditableStatus();
 * @see flockISocialAccount#getEditableStatus
 */
DiggAccount.prototype.getEditableStatus =
function DiggAccount_getEditableStatus() {
  return "";
};

/**
 * AString getFriendActions(in AString aFriendURN);
 * @see flockISocialAccount#getFriendActions
 */
DiggAccount.prototype.getFriendActions =
function DiggAccount_getFriendActions(aFriendURN) {
  this._logger.info(".getFriendActions('" + aFriendURN + "')");

  var actionNames = ["friendShout",
                     "friendDiggs",
                     "friendComments",
                     "friendSubmissions",
                     "friendFavorites",
                     "friendGallery",
                     "friendShareFlock"];

  var bundle = this.getService().getStringBundle();

  var actions = [];
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    for each (var i in actionNames) {
      actions.push({
        label: bundle.GetStringFromName("flock.digg.actions." + i),
        class: i,
        spec: this._WebDetective.getString("digg", i, "")
                  .replace("%friendid%", c_friend.accountId)
      });     
    }
  }
  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  return nsJSON.encode(actions);
};

// DEFAULT: long getFriendCount();

/**
 * AString getMeNotifications();
 * @see flockISocialAccount#getMeNotifications
 */
DiggAccount.prototype.getMeNotifications =
function DiggAccount_getMeNotifications() {
  this._logger.info(".getMeNotifications() " + this.urn);

  var bundle = this.getService().getStringBundle();
  var noties = [];
  var inst = this; 
  function _addNotie(aType, aCount) {
    var stringName = "flock.digg.noties."
                   + aType + "."
                   + ((parseInt(aCount) <= 0) ? "none" : "some");
    noties.push({
      class: aType,
      tooltip: bundle.GetStringFromName(stringName),
      metricsName: aType,
      count: (aCount) ? aCount : 0,
      URL: inst._WebDetective.getString("digg", aType + "_URL", "")
                             .replace("%accountid%", inst.getParam("accountId"))
    });
  }

  var c_params = this.getAllCustomParams();
  _addNotie("meComments", c_params.getProperty("diggComments"));
  _addNotie("meMessages", this.getParam("accountMessages"));
  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  return nsJSON.encode(noties);
};

/**
 * AString getProfileURLForFriend(in AString aFriendURN);
 * @see flockISocialAccount#getProfileURLForFriend
 */
DiggAccount.prototype.getProfileURLForFriend =
function DiggAccount_getProfileURLForFriend(aFriendURN) {
  this._logger.info(".getProfileURLForFriend('" + aFriendURN + "')");
  var url = "";
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    url =  this._WebDetective
               .getString(CLASS_SHORT_NAME, "profileURL", null)
               .replace("%accountid%", c_friend.accountId);
  }
  return url;
};

/**
 * AString getInviteFriendsURL();
 * @see flockISocialAccount#getInviteFriendsURL
 */
DiggAccount.prototype.getInviteFriendsURL =
function DiggAccount_getInviteFriendsURL() {
  this._logger.info(".getInviteFriendsURL()");
  var url = "";
  if (this.coopObj) {
    url = this._WebDetective
              .getString(CLASS_SHORT_NAME, "inviteFriendsURL", null)
              .replace("%accountid%", this.coopObj.accountId);
  }
  return url;
};

/**
 * void postLink(in nsITransferable aTransferable);
 * @see flockISocialAccount#postLink
 */
DiggAccount.prototype.postLink =
function DiggAccount_postLink(aTransferable) {
  var wm = CC["@mozilla.org/appshell/window-mediator;1"]
           .getService(CI.nsIWindowMediator);
  var win = wm.getMostRecentWindow("navigator:browser");
  if (!win) {
    return;
  }
  var url = win.gBrowser.currentURI.spec;
  var browser = win.getBrowser();
  if (url && (!gDuggCurrentUrl ||
              gDuggCurrentUrl.toLowerCase() != url.toLowerCase()))
  {
    // We never recieve digg information for this information, do it now
    var inst = this;
    var diggLink_flockListener = {
      onSuccess: function loc_success(aResult, aTopic) {
        aResult.QueryInterface(CI.nsIPropertyBag);
        url = aResult.getProperty("diggUrl");
        browser.loadOneTab(url, null, null, null, false, false);
      },
      onError: function loc_error(aFlockError, aTopic) {
        inst._logger.error("getDiggInfo_onError(..., '" 
                           + aFlockError + ", ...)");
      }
    }
    gApi.getDiggInformation(url, diggLink_flockListener, false);
  } else if (url) {
    browser.loadOneTab(url, null, null, null, false, false);
  }
};

/**
 * void shareWithFriend(in AString aFriendURN,
 *                      in nsITransferable aTransferable);
 * @see flockISocialAccount#shareWithFriend
 */
DiggAccount.prototype.shareWithFriend =
function DiggAccount_shareWithFriend(aFriendURN, aTransferable) {
  this._logger.info(".shareWithFriend('" + aFriendURN + "')");

  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    var flavors = ["text/x-flock-media",
                   "text/x-moz-url",
                   "text/unicode",
                   "text/html"];

    var message = CC[FLOCK_RDNDS_CONTRACTID]
                  .getService(CI.flockIRichDNDService)
                  .getMessageFromTransferable(aTransferable,
                                              flavors.length,
                                              flavors);
    if (message.body) {
      this._composeMessage(aFriendURN, message.body, true);
    }
  }
};

DiggAccount.prototype._composeMessage =
function DiggAccount__composeMessage(aFriendURN,
                                      aBody,
                                      addBreadCrumb)
{
  var body = aBody;
  var c_friend = this._coop.get(aFriendURN);
  var url = this._WebDetective.getString(CLASS_SHORT_NAME, "friendShout", "")
                .replace("%friendid%", c_friend.accountId);
  url = url.substring(url.indexOf("http"), url.length);
  var wm = CC["@mozilla.org/appshell/window-mediator;1"]
             .getService(CI.nsIWindowMediator);
  var win = wm.getMostRecentWindow("navigator:browser");
  if (win) {
    var browser = win.getBrowser();
    var newTab = browser.loadOneTab(url, null, null, null, false, false);
    var obs = CC["@mozilla.org/observer-service;1"]
              .getService(CI.nsIObserverService);
    var inst = this;
    var observer = {
      observe: function openSendMessageTabForFill_observer(aContent,
                                                           aTopic,
                                                           aContextUrl)
      {
        var contentWindow = newTab.linkedBrowser.docShell
                                  .QueryInterface(CI.nsIInterfaceRequestor)
                                  .getInterface(CI.nsIDOMWindow);
        function insertContent(aWebDicString, aMessage) {
          var xpathquery = inst._WebDetective.getString(CLASS_SHORT_NAME,
                                                        aWebDicString, "");
          var doc = contentWindow.document;
          var formItems = doc.evaluate(xpathquery, doc, null,
                                       CI.nsIDOMXPathResult.ANY_TYPE, null);
          if (formItems) {
            var formItem = formItems.iterateNext();
            if (formItem) {
              if (formItem.hasAttribute("value")) {
                formItem.setAttribute("value", aMessage);
              } else {
                var textNode = doc.createTextNode(aMessage);
                formItem.appendChild(textNode);
              }
              formItem.focus();
            }
          }
        };
        if (contentWindow == aContent) {
          obs.removeObserver(this, "EndDocumentLoad");
          if (addBreadCrumb) {
            // Add breadcrumb to message body
            var breadcrumb = CC[FLOCK_RDNDS_CONTRACTID]
                             .getService(CI.flockIRichDNDService)
                             .getBreadcrumb("plain");
            if (breadcrumb) {
              body += breadcrumb;
            }
          }
          insertContent("diggmessage_sendXPath", body);
        }
      }
    };
    obs.addObserver(observer, "EndDocumentLoad", false);
  }
};

/**
 * void markAllMeNotificationsSeen(in AString aType);
 * @see flockISocialAccount#markAllMeNotificationsSeen
 */
DiggAccount.prototype.markAllMeNotificationsSeen =
function DiggAccount_markAllMeNotificationsSeen(aType) {
  this._logger.debug(".markAllMeNotificationsSeen('" + aType + "')");
  switch (aType) {
    case "meMessages":
      this.setParam("accountMessages", 0);
      break;  
    case "meComments":
      this.setCustomParam("diggComments", 0);
      break;  
    default:
      break;
  }
};

/**
 * void shareFlock(in AString aFriendURN);
 * @see flockISocialAccount#shareFlock
 */
DiggAccount.prototype.shareFlock =
function DiggAccount_shareFlock(aFriendURN) {
  this._logger.info(".shareFlock('" + aFriendURN + "')");

  // Get "Share Flock" message text
  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(SERVICES_PROPERTIES_FILE);
  var body = bundle.GetStringFromName("flock.friendShareFlock.subject");
  try {
    // Ok, this may seem a bit kludgy.  We are omitting the continuation
    // condition in the for-loop, but an exception will be thrown when we try to
    // get a string that doesn't exist, and that will break the loop.  We're
    // doing it this way in order to preserve flexibility for localizations to
    // vary the number of lines in the message.
    for (var i = 0; ; i++) {
      body += "\n"
           + bundle.GetStringFromName("flock.friendShareFlock.message"
                                      + i);
    }
  } catch (ex) {
    // Ignore -- we've hit the end of our message lines
  }
  body = body.replace("%SERVICE", CLASS_TITLE);
  this._composeMessage(aFriendURN, body, false);
};

// void friendAction(in AString aAction, in AString aFriendURN, in AString..
DiggAccount.prototype.friendAction =
function DiggAccount_friendAction(aAction, aFriendURN, aActivityURN) {
  this._logger.error(".friendAction(): NS_ERROR_NOT_IMPLEMENTED");
  throw CR.NS_ERROR_NOT_IMPLEMENTED;
};

/**
 * void setStatus(in AString aStatusMessage, in flockIListener aFlockListener);
 * @see flockISocialAccount#setStatus
 */
DiggAccount.prototype.setStatus =
function DiggAccount_setStatus(aStatusMessage, aFlockListener) {
  throw CR.NS_ERROR_NOT_IMPLEMENTED;
};

DiggAccount.prototype.getFormattedFriendUpdateType =
function DiggAccount_getFormattedFriendUpdateType(aLastUpdateType, aStatus) {
  var bundleSubString = "flock.people.sidebar.article_"
                      + aLastUpdateType;
  this._logger.info("getFormattedFriendUpdateType " + bundleSubString);
  var bundle = this.getService().getStringBundle();
  return bundle.GetStringFromName(bundleSubString);
};


/*************************************************************************
 * XPCOM Support - Module Construction
 *************************************************************************/

// Create array of components.
var componentsArray = [DiggAPI, DiggService];

// Generate a module for XPCOM to find.
var NSGetModule = FlockXPCOMUtils.generateNSGetModule(MODULE_NAME,
                                                      componentsArray);
