// 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/FlockXPCOMUtils.jsm");
FlockXPCOMUtils.debug = false;

CU.import("resource:///modules/FlockScheduler.jsm");
CU.import("resource:///modules/FlockSvcUtils.jsm");
CU.import("resource:///modules/FlockXMLUtils.jsm");
CU.import("resource:///modules/FlockPrefsUtils.jsm");

const MODULE_NAME = "Twitter";
const CLASS_NAME = "Flock Twitter Service";
const CLASS_SHORT_NAME = "twitter";
const CLASS_TITLE = "Twitter";
const CLASS_ID = Components.ID("{535BFF20-9154-11DB-B606-0800200C9A66}");
const CONTRACT_ID = "@flock.com/people/twitter;1";

const TS_CLASS_NAME = "Flock Twitter Search Service";
const TS_CLASS_SHORT_NAME = "twittersearch";
const TS_CLASS_TITLE = "Twitter Search";
const TS_CLASS_ID = Components.ID("{A0C6CDB6-7713-4193-BD98-E074078B24F5}");
const TS_CONTRACT_ID = "@flock.com/webservice/twittersearch;1";
const TS_SERVICE_ENABLED_PREF = "flock.service.twittersearch.enabled";

const FLOCK_RICH_DND_CONTRACTID = "@flock.com/rich-dnd-service;1";
const FLOCK_ERROR_CONTRACTID = "@flock.com/error;1";

const SERVICE_ENABLED_PREF = "flock.service.twitter.enabled";

const FAVICON = "chrome://flock/content/services/twitter/favicon.png";

// From nsIXMLHttpRequest.idl
const XMLHTTPREQUEST_READYSTATE_COMPLETED = 4;

const HTTP_CODE_OK = 200;
const HTTP_CODE_FOUND = 302;

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

// This is a workaround for the 401 errors we're getting when attempting to
// authenticate to Twitter's API.  If we get a 401, we'll silently retry up to
// this number of times before giving up and showing the user an error.
const TWITTER_MAX_AUTH_ATTEMPTS = 5;

// Twitter returns messages in pages of 20.
const TWITTER_MESSAGES_PAGE_SIZE = 20;

// Twitter API documentation specifies the maximum length of a status message.
const TWITTER_MAX_STATUS_LENGTH = 140;

const TWITTER_PROPERTIES = "chrome://flock/locale/services/twitter.properties";

var gApi = null;


function getIdentityUrn(aAccountId, aUid) {
  var prefix = "urn:flock:identity:" + CLASS_SHORT_NAME;
  return prefix + aAccountId + ":" + aUid;
}

// Helper function to parse Twitter's date string into seconds since epoch.
// @param  aDateString  A string formatted as: Wed Jan 31 00:16:35 +0000 2007
// @return  The number of seconds since the epoch.
function parseTwitterDate(aDateString) {
  if (!aDateString) {
    return 0;
  }
  // Date.parse() returns milliseconds since epoch. Divide by 1000 for seconds.
  return (Date.parse(aDateString) / 1000);
}

// Load a url in a new tab or window
function loadURIIn(aUri, aWhere) {
  var win = CC["@mozilla.org/appshell/window-mediator;1"]
            .getService(CI.nsIWindowMediator)
            .getMostRecentWindow("navigator:browser");
  if (win) {
    win.openUILinkIn(aUri, aWhere == "newwindow" ? "window" : "tab");
  }
}

/*************************************************************************
 * Component: flockTwitterService
 *************************************************************************/
function flockTwitterService() {
  this._obs = CC["@mozilla.org/observer-service;1"]
              .getService(CI.nsIObserverService);
  this._init();
  this._ppSvc = CC["@flock.com/people-service;1"]
                .getService(CI.flockIPeopleService);

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

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

  FlockSvcUtils.flockIRichContentDropHandler
               .addDefaultMethod(this, "_handleTextareaDrop");
}


/*************************************************************************
 * flockTwitterService: XPCOM Component Creation
 *************************************************************************/

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

// FlockXPCOMUtils.genericModule() categories
flockTwitterService.prototype._xpcom_categories = [
  { category: "wsm-startup" },
  { category: "flockWebService", entry: CLASS_SHORT_NAME },
  { category: "flockRichContentHandler", entry: CLASS_SHORT_NAME }
];


/*************************************************************************
 * flockTwitterService: Private Data and Functions
 *************************************************************************/
/**
 * Helper function to return the value of a child node given its tag name.
 * @param  aSource  The node to be searched.
 * @aPropertyName  The tag name of the node for which we want the value.
 */
flockTwitterService.prototype._getNodeValueByTagName =
function fts__getNodeValueByTagName(aSource, aTagName) {
  this._logger.debug("._getNodeValueByTagName(aSource, " + aTagName + ")");
  try {
    return aSource.getElementsByTagName(aTagName)[0].firstChild.nodeValue;
  } catch (ex) {
    // We didn't find the node or the node had no value.
    return null;
  }
};

// Member variables.
flockTwitterService.prototype._init =
function fts__init() {
  FlockSvcUtils.getLogger(this);
  this._logger.debug(".init()");

  // 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;
  }

  var profiler = CC["@flock.com/profiler;1"].getService(CI.flockIProfiler);
  var evtID = profiler.profileEventStart("twitter-init");

  this._accountClassCtor = flockTwitterAccount;

  if (!gApi) {
    // Initialize API (which also loads the web detective file)
    gApi = new TwitterAPI();
  }
  this._WebDetective = gApi._WebDetective;
  var authenticatedUrn = FlockSvcUtils.initService(this, true);

  this._baseUrn = "urn:twitter";
  this._serviceUrn = this._baseUrn + ":service";
  FlockSvcUtils.getACUtils(this);

  if (authenticatedUrn) {
    // Update auth states
    var authAccount = this._coop.Account.get(authenticatedUrn);
    gApi.setAuthAccount(authAccount.accountId);
  }

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

  profiler.profileEventEnd(evtID, "");
};

flockTwitterService.prototype._refreshAccount =
function fts__refreshAccount(aAccount, aPollingListener) {
  if (!aAccount.isAuthenticated()) {
    // If the user is not logged in, return a success without
    // refreshing anything
    aPollingListener.onResult();
    return;
  }
  this._logger.debug("_refreshAccount()");
  var inst = this;
  var coopAcct = aAccount.coopObj;

  // First check if the API rate timeout period has elapsed
  var nextRateReset = coopAcct.get("nextRateReset", true);
  if (!nextRateReset) {
    nextRateReset = 0;
  }
  var currentTime = (new Date()).getTime();
  const ONE_MINUTE = 60 * 1000;
  if (gApi.apiHitsRemaining == null) {
    gApi.apiHitsRemaining = coopAcct.get("apiHitsRemaining", true);
  }
  if (nextRateReset < currentTime) {
    // Rate timeout period has elapsed, so reset it
    nextRateReset = currentTime + (60 * ONE_MINUTE);
    coopAcct.set("nextRateReset", nextRateReset, true);
    var pref = "flock.service.twitter.ratelimit";
    gApi.apiHitsRemaining = FlockPrefsUtils.getIntPref(pref);
    if (!gApi.apiHitsRemaining) {
      gApi.apiHitsRemaining = 100;
    }
    coopAcct.set("apiHitsRemaining", gApi.apiHitsRemaining, true);
  }

  // This boolean keeps track of whether we're allowed to use a discretionary
  // API call on this timer tick.
  var makeDiscretionaryCall = true;

  // Returns TRUE if the time since 'lastRefresh' exceeds 'minutes'
  function needToCall(aLastRefresh, aMinutes) {
    var last = coopAcct.get(aLastRefresh, true);
    if (!last) {
      last = 0;
    }
    if ((currentTime - last) > (ONE_MINUTE * aMinutes)) {
      coopAcct.set(aLastRefresh, Date.now(), true);
      return true;
    }
    return false;
  }

  var openedSidebarTab = this._acUtils.peopleSidebarOpen();
  function isTwitterSidebarOpen() {
    return openedSidebarTab == "all" || openedSidebarTab == inst.contractId;
  }

  function isMyWorldFriendActivityShowing() {
    return inst._ppSvc.numObservers() > (openedSidebarTab ? 1 : 0);
  }

  var pplRefreshListener = CC["@flock.com/people-refresh-listener;1"]
                           .createInstance(CI.flockIPeopleRefreshListener);

  // Make sure account details and friends list get refreshed at least every
  // 30 minutes, or every 15 minutes when the Twitter sidebar is open.
  if (needToCall("lastAccountDetailsRefresh", isTwitterSidebarOpen() ? 15 : 30))
  {
    var localPollingListener = {
      onResult: function localPollingListener_onResult() {
        // We just completed the load of the friends list for this
        // account.  Now we should force another refresh of the account right
        // away in order to perform some additional API calls that required
        // the friends list to be loaded first.
        coopAcct.set("lastTimelineRefresh", Date.now(), true);
        inst._refreshAccount(aAccount, aPollingListener);
      },
      onError: function localPollingListener_onError(aFlockError) {
        coopAcct.set("lastAccountDetailsRefresh", null, true);
        aPollingListener.onError(aFlockError);
      }
    };
    pplRefreshListener.init(3, null, localPollingListener, 0);
    this._refreshAccountDetails(aAccount, pplRefreshListener);
    this._refreshTimeline(aAccount, pplRefreshListener);
    this._refreshFriendList(aAccount, pplRefreshListener);
    return;
  }

  // There are 4 potential API calls below
  pplRefreshListener.init(4, null, aPollingListener, 0);

  // Make sure the @Replies list gets refreshed at least every 30 minutes, or
  // every 5 minutes if the People sidebar is open.
  if (needToCall("lastRepliesRefresh", isTwitterSidebarOpen() ? 5 : 30))
  {
    this._refreshReplies(aAccount, pplRefreshListener);
    makeDiscretionaryCall = false;
  } else {
    pplRefreshListener.onSuccess(null, "skip _refreshReplies");
  }

  // Make sure the DMs list gets refreshed at least every 30 minutes, or every
  // 5 minutes if the People sidebar is open.
  if (needToCall("lastDMRefresh", isTwitterSidebarOpen() ? 5 : 30))
  {
    this._refreshDirectMessages(aAccount, pplRefreshListener);
    makeDiscretionaryCall = false;
  } else {
    pplRefreshListener.onSuccess(null, "skip _refreshDirectMessages");
  }

  function determineRecentlyActiveUI() {
    if (isTwitterSidebarOpen()) {
      var wm = CC["@mozilla.org/appshell/window-mediator;1"]
               .getService(CI.nsIWindowMediator);
      var win = wm.getMostRecentWindow("navigator:browser");
      if (win) {
        var sidedoc = win.document.getElementById("sidebar").contentDocument;
        var psTabs = sidedoc.getElementById("peopleServiceTabs");
        var mecard = sidedoc.getAnonymousElementByAttribute(
                     psTabs, "id", "mecard:" + CONTRACT_ID);
        var friendsBtn = sidedoc.getAnonymousElementByAttribute(
                         mecard, "anonid", "friends-button-container");
        if (friendsBtn && friendsBtn.hasAttribute("selected")) {
          return "sidebar-friends";
        }
        var repliesBtn = sidedoc.getAnonymousElementByAttribute(
                         mecard, "anonid", "replies-button-container");
        if (repliesBtn && repliesBtn.hasAttribute("selected")) {
          return "sidebar-replies";
        }
        var dmessagesBtn = sidedoc.getAnonymousElementByAttribute(
                           mecard, "anonid", "dmessages-button-container");
        if (dmessagesBtn && dmessagesBtn.hasAttribute("selected")) {
          return "sidebar-DMs";
        }
      }
    }
    if (isMyWorldFriendActivityShowing()) {
      return "myworld-timeline";
    }
    return null;
  }
  var recentlyActiveUI = determineRecentlyActiveUI();
  var timelineViewed = (recentlyActiveUI == "sidebar-friends") ||
                       (recentlyActiveUI == "myworld-timeline");

  // Make sure the friends timeline gets refreshed at least every 15 minutes,
  // or every 5 minutes if the People sidebar or Friend Activity widget (on
  // My World) is showing.
  if (needToCall("lastTimelineRefresh", timelineViewed ? 5 : 15)) {
    this._refreshTimeline(aAccount, pplRefreshListener);
    makeDiscretionaryCall = false;
  } else {
    pplRefreshListener.onSuccess(null, "skip _refreshTimeline");
  }

  // We didn't make any calls yet, so we can make a call now that will best
  // contribute to UI responsiveness.
  if (makeDiscretionaryCall) {
    switch (recentlyActiveUI) {
      case "sidebar-friends":
      case "myworld-timeline":
        this._refreshTimeline(aAccount, pplRefreshListener);
        break;
      case "sidebar-replies":
        this._refreshReplies(aAccount, pplRefreshListener);
        break;
      case "sidebar-DMs":
        this._refreshDirectMessages(aAccount, pplRefreshListener);
        break;
      default:
        // This means no Twitter-specific UI is currently being used, so don't
        // bother using up any API requests at this time.  Save them for when
        // the user is actually using the features.
        pplRefreshListener.onSuccess(null,
                                     "skip discretionary call (no active UI)");
        break;
    }
  } else {
    pplRefreshListener.onSuccess(null, "skip discretionary call");
  }

  // Recalculate the poller interval based on rate limit, apiHitsRemaining and
  // time remaining
  var timeRemaining = Math.floor((nextRateReset - currentTime) / 1000);
  coopAcct.set("apiHitsRemaining", gApi.apiHitsRemaining, true);
  var newPollingInterval;
  if (gApi.apiHitsRemaining > 0) {
    newPollingInterval = Math.floor(timeRemaining / gApi.apiHitsRemaining);
  } else {
    newPollingInterval = timeRemaining;
  }
  this._logger.debug(timeRemaining + "s until API rate counter resets");
  this._logger.debug(gApi.apiHitsRemaining + " API calls to make in that time");
  this._logger.debug("Therefore, next refresh in " + newPollingInterval + "s");
  coopAcct.refreshInterval = newPollingInterval;
};

flockTwitterService.prototype._refreshAccountDetails =
function fts__refreshAccountDetails(aAccount, aPeopleRefreshListener) {
  this._logger.debug("_refreshAccountDetails()");
  var inst = this;
  var coopAcct = aAccount.coopObj;

  // This listener handles getting the account owner's information.
  var userShowFlockListener = {
    onSuccess: function userShow_onSuccess(aResult, aTopic) {
      inst._logger.debug("Success for userShow");
      var user = aResult.getElementsByTagName("user")[0];

      // Parse out the name.
      try {
        var name = user.getElementsByTagName("name")[0].firstChild.nodeValue;
        inst._logger.debug("userShow: name: " + name);
        coopAcct.name = name;
      } catch (ex) {
        inst._logger.error("No account name returned.");
      }

      // Parse out the screen name.
      try {
        var screenName = user.getElementsByTagName("screen_name")[0].firstChild
                                                                    .nodeValue;
        inst._logger.debug("userShow: screen_name: " + screenName);
        coopAcct.screenName = screenName;
        coopAcct.URL = inst._WebDetective
                           .getString(CLASS_SHORT_NAME, "userprofile", null)
                           .replace("%accountid%", screenName);
      } catch (ex) {
        inst._logger.error("No screen name returned.");
      }

      // Parse out the avatar.
      try {
        var avatarURL = user.getElementsByTagName("profile_image_url")[0]
                            .firstChild.nodeValue;
        inst._logger.debug("userShow: avatar: " + avatarURL);
        // If avatar returned is the default image, set coop.Account.avatar
        // to null and let the people sidebar code set the Flock common image.
        if (inst._hasDefaultAvatar(avatarURL)) {
          inst._logger.debug("No avatar for account. Setting to null.");
          coopAcct.avatar = null;
        } else {
          coopAcct.avatar = avatarURL;
        }
      } catch (ex) {
        coopAcct.avatar = null;
        inst._logger.error("No avatar returned.");
      }

      // Parse out the status info.
      try {
        var statusXML = aResult.getElementsByTagName("status");
        var dateString = statusXML[0].getElementsByTagName("created_at")[0]
                                     .firstChild.nodeValue;
        var lastStatusMessageUpdateDate = parseTwitterDate(dateString);
        // Update the status if is more recent than the current one.
        if (lastStatusMessageUpdateDate > coopAcct.lastStatusMessageUpdateDate)
        {
          var status = statusXML[0].getElementsByTagName("text")[0].firstChild
                                                                   .nodeValue;
          inst._logger.debug("userShow: status: " + status);
          coopAcct.statusMessage = status;
          coopAcct.lastProfileUpdate = lastStatusMessageUpdateDate;
          coopAcct.lastStatusMessageUpdateDate = lastStatusMessageUpdateDate;
        }
      } catch (ex) {
        inst._logger.error("No status info returned.");
      }
      inst._obs.notifyObservers(aAccount, "flock-acct-refresh", "user-info");
      aPeopleRefreshListener.onSuccess(null, "userShowFlockListener");
    },
    onError: function userShow_onError(aFlockError, aTopic) {
      inst._logger.error("userShowFlockListener onError");
      aPeopleRefreshListener.onError(aFlockError, "userShowFlockListener");
    }
  };
  gApi.userShow(coopAcct.accountId, userShowFlockListener);
};


flockTwitterService.prototype._refreshFriendList =
function fts__refreshFriendList(aAccount, aPeopleRefreshListener) {
  this._logger.debug("_refreshFriendList()");
  var inst = this;
  var coopAcct = aAccount.coopObj;

  // This listener handles getting the user's friends' information.
  var friendsFlockListener = {
    onSuccess: function friends_onSuccess(aResult, aTopic) {
      inst._logger.debug("friendsFlockListener onSuccess");

      // Now that we have all the results we need, update the RDF.
      function myWorker(aShouldYield) {
        if (coopAcct.isAuthenticated) {
          // REMOVE locally people removed on the server
          var localChildren = coopAcct.friendsList.children;
          var localEnum = localChildren.enumerate();
          while (localEnum.hasMoreElements()) {
            var identity = localEnum.getNext();
            if (!aResult[identity.accountId]) {
              inst._logger.info("Friend " + identity.accountId
                                + " has been deleted on the server");
              localChildren.remove(identity);
              identity.destroy();
            }
          }
          // ADD or update existing people
          for (var uid in aResult) {
            var friend = aResult[uid];
            var c_friend = inst._addPerson(friend, friend.status, coopAcct);
            c_friend.set("followsMe", friend.followsMe ? "true" : "false", true);
            if (aShouldYield()) {
              yield;
              if (!coopAcct.isAuthenticated) {
                // Account has just been deleted or logged out
                break;
              }
            }
          }
        }
        aPeopleRefreshListener.onSuccess(null, "_refreshFriendList");
      }

      FlockScheduler.schedule(null, 0.05, 10, myWorker);
    },
    onError: function friends_onError(aFlockError, aTopic) {
      inst._logger.error("friendsFlockListener onError");
      aPeopleRefreshListener.onError(aFlockError, "_refreshFriendList");
    }
  };
  gApi.getFriendsStatus(coopAcct.accountId, friendsFlockListener);
};


flockTwitterService.prototype._refreshTimeline =
function fts__refreshTimeline(aAccount, aPeopleRefreshListener) {
  this._logger.debug("_refreshTimeline()");
  var inst = this;
  var coopAcct = aAccount.coopObj;

  var getFriendsTimelineListener = {
    onSuccess: function getFriendsTimelineListener_onSuccess(aResult, aTopic) {
      for each (var res in aResult) {
        var person = res.user;
        if (coopAcct.accountId == person.id) {
          var statusMessage = res.text;
          inst._logger.debug("statusMessage = " + statusMessage);
          var lastStatusUpdateDate = parseTwitterDate(res.created_at);

          // We have a status update on the account.
          if (lastStatusUpdateDate <= coopAcct.lastStatusMessageUpdateDate) {
            // Status is old so skip.
            continue;
          }

          coopAcct.statusMessage = statusMessage;
          coopAcct.lastUpdateType = "status";
          coopAcct.lastProfileUpdate = lastStatusUpdateDate;
          coopAcct.lastStatusMessageUpdateDate = lastStatusUpdateDate;

        } else {
          // We have a status update on a friend
          inst._addPerson(person, res, coopAcct);
        }
      }
      aPeopleRefreshListener.onSuccess(null, "getFriendsTimelineListener");
      //aPeopleRefreshListener.refreshFinished();
    },
    onError: function getFriendsTimelineListener_onError(aFlockError, aTopic) {
      inst._logger.error("getFriendsTimelineListener onError");
      aPeopleRefreshListener.onError(aFlockError, "getFriendsTimelineListener");
    }
  };
  gApi.getFriendsTimeline(getFriendsTimelineListener);
};


flockTwitterService.prototype._refreshReplies =
function fts__refreshReplies(aAccount, aPeopleRefreshListener) {
  this._logger.debug("_refreshReplies()");
  var inst = this;
  var fts_repliesListener = {
    onSuccess: function fts_repliesListener_onSuccess(aResult, aTopic) {
      inst._logger.debug("_refreshReplies(): onSuccess()");
      aAccount._processTweets(aResult, "reply");
      aPeopleRefreshListener.onSuccess(null, "_refreshReplies");
    },
    onError: function fts_repliesListener_onError(aFlockError, aTopic) {
      inst._logger.error("_refreshReplies(): onError()");
      aPeopleRefreshListener.onError(aFlockError, aTopic);
    }
  };
  var sinceId = aAccount._mostRecentId("reply");
  gApi.getReplies(sinceId, fts_repliesListener);
};


flockTwitterService.prototype._refreshDirectMessages =
function fts__refreshDirectMessages(aAccount, aPeopleRefreshListener) {
  this._logger.debug("_refreshDirectMessages()");
  var inst = this;
  var fts_dmListener = {
    onSuccess: function fts_dmListener_onSuccess(aResult, aTopic) {
      inst._logger.debug("_refreshDirectMessages(): onSuccess()");
      aAccount._processTweets(aResult, "dmessage");
      aPeopleRefreshListener.onSuccess(null, "_refreshDirectMessages");
    },
    onError: function fts_dmListener_onError(aFlockError, aTopic) {
      inst._logger.error("_refreshDirectMessages(): onError()");
      aPeopleRefreshListener.onError(aFlockError, aTopic);
    }
  };
  var sinceId = aAccount._mostRecentId("dmessage");
  gApi.getDirectMessages(sinceId, fts_dmListener);
};


flockTwitterService.prototype._addPerson =
function fts__addPerson(aPerson, aStatus, aCoopAccount) {
  // We include the current accountId in the identity urn to prevent friend
  // collisions if multiple service accounts have the same friend.
  var identityUrn = getIdentityUrn(aCoopAccount.accountId, aPerson.id);
  var identity = this._coop.get(identityUrn);

  var lastUpdate = parseTwitterDate(aStatus.created_at);
  this._logger.debug("Adding person: " + aPerson.id + " - " + aPerson.name);

  // XXX: Since Twitter only timestamps status changes, we need to add
  //      property-wise profile comparison here.
  var lastUpdateType = "status";

  var avatarUrl = null;
  if (!this._hasDefaultAvatar(aPerson.profile_image_url)) {
    avatarUrl = aPerson.profile_image_url;
  }

  var profileURL = this._WebDetective
                       .getString(CLASS_SHORT_NAME, "userprofile", null)
                       .replace("%accountid%", aPerson.screen_name);
  if (identity) {
    if (identity.lastUpdate >= lastUpdate) {
      // no update required.
      return identity;
    }
    identity.name = aPerson.name;
    identity.URL = profileURL;
  } else {
    identity = new this._coop.Identity(
      identityUrn,
      {
        name: aPerson.name,
        serviceId: this.contractId,
        accountId: aPerson.id,
        URL: profileURL
      }
    );
    aCoopAccount.friendsList.children.add(identity);
  }

  // Update data of the coop.Identity object
  identity.avatar = avatarUrl;
  identity.screenName = aPerson.screen_name;
  identity.statusMessage = aStatus.text;
  var url = this._WebDetective
                .getString(CLASS_SHORT_NAME, "statusURL", "");
  identity.statusMessageUrl = url.replace("%screenName%", identity.screenName)
                                 .replace("%statusid%", aStatus.id);
  identity.lastUpdateType = lastUpdateType;
  identity.lastUpdate = lastUpdate; // triggers the RDF observers
  return identity;
};


/**
 * Helper function to determine if the user has customized their avatar based
 * on the passed in URL.
 * @param  aUrl  A string containing the contents of the Twitter user's
 *               "profile_image_url" property.
 * @return  true if the user is still using the default avatar, else false
 */
flockTwitterService.prototype._hasDefaultAvatar =
function fts__hasDefaultAvatar(aUrl) {
  this._logger.debug("_hasDefaultAvatar(" + aUrl + ")");
  var defaultUrl = this._WebDetective.getString("twitter", "noAvatar", "");
  return (aUrl.indexOf(defaultUrl) != -1);
};

/*************************************************************************
 * flockTwitterService: flockIWebService Implementation
 *************************************************************************/

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

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

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

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

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

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

/**
 * @see flockIWebService#addAccount
 */
flockTwitterService.prototype.addAccount =
function fts_addAccount(aAccountId, aIsTransient, aFlockListener) {
  this._logger.debug(".addAccount('"
                     + aAccountId + "', " + aIsTransient + ", aFlockListener)");

  if (!aAccountId) {
    if (aFlockListener) {
      var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
      // XXX See bug 10749 - flockIError cleanup
      error.errorCode = 9999;
      aFlockListener.onError(error, "addAccount");
    }
    return;
  }

  var pw = this._acUtils.getPassword(this.urn + ":" + aAccountId);
  var name = (pw) ? pw.username : aAccountId;

  var url = this._WebDetective.getString(CLASS_SHORT_NAME, "userprofile", null)
                .replace("%accountid%", name);
  var accountURN =
    this._acUtils.createAccount(this, aAccountId, name, url, aIsTransient);

  // Instantiate account component
  var account = this.getAccount(accountURN);
  account.setParam("service", this._coopObj);
  account.setParam("refreshInterval", REFRESH_INTERVAL);

  if (aFlockListener) {
    aFlockListener.onSuccess(account, "addAccount");
  }
  return account;
};

// DEFAULT: flockIWebServiceAccount getAccount(in AString aUrn);
// DEFAULT: nsIStringEnumerator getAccountUrns();
// DEFAULT: void removeAccount(in AString aUrn);
// DEFAULT: void logout();


/*************************************************************************
 * flockTwitterService: flockILoginWebService Implementation
 *************************************************************************/

// readonly attribute AString domains;
flockTwitterService.prototype.__defineGetter__("domains",
function fts_getdomains() {
  this._logger.debug("Getting property: domains");
  return this._WebDetective.getString("twitter", "domains", "twitter.com");
});

// DEFAULT: boolean docRepresentsSuccessfulLogin(in nsIDOMHTMLDocument aDocument);
// DEFAULT: AString getAccountIDFromDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: flockILoginInfo getCredentialsFromForm(in nsIDOMHTMLFormElement aForm);
// DEFAULT: boolean ownsDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: boolean ownsLoginForm(in nsIDOMHTMLFormElement aForm);

/**
 * @see flockILoginWebService#updateAccountStatusFromDocument
 */
flockTwitterService.prototype.updateAccountStatusFromDocument =
function fts_updateAccountStatusFromDocument(aDocument,
                                             aAccount,
                                             aFlockListener)
{
  if (aAccount) {
    this._logger.debug("We're logged in!");
    if (!this._acUtils.ensureOnlyAuthenticatedAccount(aAccount)) {
      gApi.setAuthAccount(aAccount.getParam("accountId"));
      if (aFlockListener) {
        aFlockListener.onSuccess(aAccount, null);
      }
    }
  } else if (this._WebDetective
                 .detect(this.shortName, "loggedout", aDocument, null))
  {
    this._logger.debug("We're logged out!");
    this._acUtils.markAllAccountsAsLoggedOut(this);
  } else {
    this._logger.debug("We don't match 'loggedout' or 'logged in'");
  }
};

/*************************************************************************
 * flockTwitterService: flockISocialWebService implementation
 *************************************************************************/

// void markAllMediaSeen(in AString aPersonUrn);
flockTwitterService.prototype.markAllMediaSeen =
function fts_markAllMediaSeen(aPersonUrn) {
  // Twitter has no media, so this does nothing and shouldn't be called.
  throw CR.NS_ERROR_NOT_IMPLEMENTED;
};

// nsIDOMElement parseStatus(in nsIDOMDocument aDocument,
//                           in AString aStatusMessage)
flockTwitterService.prototype.parseStatus =
function fts_parseStatus(aDocument, aStatusMessage) {
  //Check if the component has a special regEx for links
  var reStr = this._WebDetective
                  .getString(CLASS_SHORT_NAME,
                             "linkifyStatusRegExp",
                             "(https?:\\/\\/(\\w+:{0,1}\\w*@)?([\\w\\-\\.]+[A-Za-z0-9])|www\\.[\\w\\-\\.]+[A-Za-z0-9])(:[0-9]+)?(\\/[^ ]*[^,. !:;])?");
  var re = new RegExp(reStr, "g");
  function html(aName) {
    return aDocument.createElementNS("http://www.w3.org/1999/xhtml", aName);
  }
  function text(aText) {
    return aDocument.createTextNode(aText);
  }
  var matches = aStatusMessage.match(re);
  var span = html("span");
  span.setAttribute("class", "statusmessage");
  var msg = aStatusMessage;
  for each (let item in matches) {
    var idx = msg.indexOf(item);
    if (idx > 0) {
      span.appendChild(text(msg.substr(0, idx)));
    }
    var a = html("a");
    var url = "";
    switch (msg.charAt(idx)) {
      case "h":
        url = item;
        a.appendChild(text(item));
        break;
      case "w":
        url = "http://" + item;
        a.appendChild(text(item));
        break;
      case "@":
        span.appendChild(text("@"));
        var username = item.substr(1);
        var twitterUrl = this._WebDetective
                             .getString(CLASS_SHORT_NAME, "mainpage", "");
        url = twitterUrl + username;
        a.appendChild(text(username));
        break;
      case "#":
        var searchTwitterUrl = this._WebDetective
                                   .getString(CLASS_SHORT_NAME, "searchPage", "");
        url = searchTwitterUrl + "?q=%23" + item.substr(1);
        a.appendChild(text(item));
        break;
    }
    a.setAttribute("origURL", url);
    a.setAttribute("destURL", url);
    a.setAttribute("onclick", "event.stopPropagation(); return false;");
    a.setAttribute("onmouseup", "loadURI(this.getAttribute('destURL'), event); return false;");
    a.setAttribute("onmouseover", "hoverURI(this, event);");
    a.setAttribute("onmouseout", "hoverURI(null);");
    span.appendChild(a);
    msg = msg.substr(idx + item.length);
  }
  span.appendChild(text(msg));
  return span;
};

flockTwitterService.prototype.maxStatusLength = TWITTER_MAX_STATUS_LENGTH;


/*************************************************************************
 * flockTwitterService: flockIPollingService Implementation
 *************************************************************************/
/**
 * @see flockIPollingService#refresh
 */
flockTwitterService.prototype.refresh =
function fts_refresh(aUrn, aPollingListener) {
  this._logger.debug(".refresh(" + aUrn + ")");
  var refreshItem = this.getAccount(aUrn);

  if (refreshItem.coopObj instanceof this._coop.Account) {
    this._logger.debug("refreshing an Account");
    this._refreshAccount(refreshItem, aPollingListener);
  } else {
    this._logger.error("can't refresh " + aUrn + " (unsupported type)");
    aPollingListener.onError(null);
  }
};


/**************************************************************************
 * flockTwitterService: flockIRichContentDropHandler Implementation
 **************************************************************************/

// boolean handleDrop(in nsITransferable aFlavours,
//                    in nsIDOMHTMLElement aTargetElement);
flockTwitterService.prototype.handleDrop =
function fts_handleDrop(aFlavours, aTargetElement) {
  // Handle textarea drops
  if (aTargetElement instanceof CI.nsIDOMHTMLTextAreaElement) {
    var dropCallback = function twitter_dropCallback(aFlav) {
      // Get URL from dropped text
      var dataObj = {};
      var len = {};
      aFlavours.getTransferData(aFlav, dataObj, len);
      var text = dataObj.value.QueryInterface(CI.nsISupportsString).data;
      var textParts = text.split(": ");
      var url = (textParts.length == 2) ? textParts[1] : text;

      // Find position
      var caretPos = aTargetElement.selectionEnd;
      var currentValue = aTargetElement.value;

      // Add a trailing space so that we don't mangle the url
      var nextChar = currentValue.charAt(caretPos);
      var trailingSpace = ((nextChar == "") ||
                           (nextChar == " ") ||
                           (nextChar == "\n"))
                        ? ""
                        : " ";

      // Put it all together to drop the text into the selection. Note: no
      // breadcrumb due to twitter length constraint.
      aTargetElement.value = currentValue.substring(0, caretPos)
                           + url
                           + trailingSpace
                           + currentValue.substring(caretPos);
    };

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

  // Default handling otherwise
  return false;
};


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

  FlockSvcUtils.getACUtils(this);
  this._WebDetective = this._acUtils
                           .loadWebDetectiveFile("twitter.xml", "res/detect");

  this._logger.debug("constructor");
}


/*************************************************************************
 * TwitterAPI: XPCOM Component Creation
 *************************************************************************/

// 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().
TwitterAPI.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " API",
  "",
  "",
  TwitterAPI,
  0,
  []
  );


/*************************************************************************
 * TwitterAPI: Private data and functions
 *************************************************************************/
TwitterAPI.prototype.setAuthAccount =
function api_setAuthAccount(aAccountId) {
  this.apiHitsRemaining = null;
  var passwordUrn = "urn:twitter:service:" + aAccountId;
  this.creds = this._acUtils.getPassword(passwordUrn);
};

/**
 * Call the specified Twitter API method.
 * @param  aFlockListener
 * @param  aMethod
 *    One of the API methods; "users/show/", etc.
 * @param  aParams
 * @param  aRequestType  "GET" or "POST"
 * @param  aPostVars  Array of JS objects to include in the POST body
 * @param  aCount  A variable to increment when a 401 error occurs before
 *                 trying again.  This is handled internally by this
 *                 function.  External callers should set this to null.
 */
TwitterAPI.prototype._call =
function api__call(aFlockListener,
                   aMethod,
                   aParams,
                   aRequestType,
                   aPostVars,
                   aCount,
                   aFormat)
{
  var requestType = aRequestType.toUpperCase();
  var responseFormat = (aFormat ? aFormat : ".json");

  var req = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
            .createInstance(CI.nsIXMLHttpRequest)
            .QueryInterface(CI.nsIJSXMLHttpRequest);

  var params = "";
  var idx = 0;
  for (var p in aParams) {
    // Only use "?" for the first param.  Use "&" after.
    params += (idx == 0) ? "?" : "&";
    params += p + "=" + encodeURIComponent(aParams[p]);
    idx++;
  }

  var url;
  if (aMethod == "search") {
    var searchTwitterUrl = this._WebDetective
                               .getString("twitter", "searchPage", "");
    url = searchTwitterUrl + responseFormat + params;
    req.open(requestType, url, true);
  } else if (aMethod == "trends") {
    // Trends only returns JSON format.
    var url = this._WebDetective.getString("twitter", "trendsURL", "");
    req.open(requestType, url, true);
  } else {
    url = "https://twitter.com/" + aMethod + responseFormat + params;

    // Don't pop nsIAuthPrompts if auth fails
    req.mozBackgroundRequest = true;

    req.open(requestType, url, true, this.creds.username, this.creds.password);
  }

  this._logger.debug("._call() url: " + url);

  if (requestType == "POST") {
    req.setRequestHeader("Content-Type",
                         "application/x-www-form-urlencoded; charset=UTF-8");
  }

  var inst = this;
  req.onreadystatechange = function ORSC(aEvent) {
    inst._logger.debug("._call() ReadyState: " + req.readyState);

    if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      var status = req.status;
      inst._logger.debug("._call() Status: " + status);
      var okStatus = Math.floor(status/100) == 2;

      if (okStatus) {
        inst._logger.debug("._call() response:\n" + req.responseText);
        var result;
        if (responseFormat == ".json") {
          var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
          try {
            result = nsJSON.decode(req.responseText);
          } catch (ex) {
            result = null;
          }
        } else {
          result = req.responseXML;
        }

        if (result) {
          aFlockListener.onSuccess(result, null);
          return;
        } else {
          inst._logger.debug("._call() error: no result");
        }
      } else if (status == 401) {
        // XXX: Workaround for 401 errors we're seeing with Twitter.
        // Loop call if necessary up to TWITTER_MAX_AUTH_ATTEMPTS times.

        // Increment counter
        var count;
        if (!aCount) {
          count = 1;
        } else {
          count = aCount + 1;
        }

        if (count < TWITTER_MAX_AUTH_ATTEMPTS) {
          // Try, try again
          inst._logger.debug("._call() Got a 401. Will try "
                             + (TWITTER_MAX_AUTH_ATTEMPTS - count)
                             + " more time(s).");
          inst._call(aFlockListener, aMethod, aParams, aRequestType, aPostVars,
                     count);
          return;
        }
      }
      if (status == 400) {
        inst.apiHitsRemaining = 0;
      }
      var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
      error.errorCode = okStatus ? CI.flockIError.INVALID_RESPONSE_XML : status;
      var detail = url + ": " + req.responseText;
      error.serviceErrorString = detail;
      aFlockListener.onError(error, null);
    }
  };

  var postBody = "";
  if (aPostVars) {
    for (var v in aPostVars) {
      if (postBody.length) {
        postBody += "&";
      }
      postBody += v + "=" + encodeURIComponent(aPostVars[v]);
    }
  }

  if ((requestType == "POST") && postBody && postBody.length) {
    this._logger.debug("._call() postBody: " + postBody);
    req.send(postBody);
  } else {
    req.send(null);
  }
};


/**
 * Get info (including status) about a user.
 * @param  aUid  Twitter uid of the user to query.
 * @param  aFlockListener
 */
TwitterAPI.prototype.userShow =
function api_userShow(aUid, aFlockListener) {
  this._logger.debug(".userShow(" + aUid + ", aFlockListener)");

  var url = "users/show/" + aUid;
  gApi.apiHitsRemaining--;
  gApi._call(aFlockListener, url, null, "GET", null, null, ".xml");
};


/**
 * direct_messages
 * Returns a list of the 20 most recent direct messages sent to the
 * authenticating user.  The XML and JSON versions include detailed
 * information about the sending and recipient users.
 *
 * URL: http://twitter.com/direct_messages.format
 * Formats: xml, json, rss, atom
 * Parameters:
 *   since     Optional.  Narrows the resulting list of direct messages to just
 *             those sent after the specified HTTP-formatted date.  The same
 *             behavior is available by setting the If-Modified-Since parameter
 *             in your HTTP request.
 *             Ex: http://twitter.com/direct_messages.atom?since=Tue%2C+27+Mar+2007+22%3A55%3A48+GMT
 *   since_id  Optional.  Returns only direct messages with an ID greater than
 *             (that is, more recent than) the specified ID.
 *             Ex: http://twitter.com/direct_messages.xml?since_id=12345
 *   page      Optional.  Retrieves the 20 next most recent direct messages.
 *             Ex: http://twitter.com/direct_messages.xml?page=3
 */
TwitterAPI.prototype.directMessages =
function api_directMessages(aSince, aSinceId, aPage, aFlockListener) {
  this._logger.debug(".directMessages(" + aSince + ", "
                                        + aSinceId + ", "
                                        + aPage + ", "
                                        + "aFlockListener)");
  var params = {};

  if (aSince) {
    params.since = aSince;
  }

  if (aSinceId) {
    params.since_id = aSinceId;
  }

  if (aPage) {
    params.page = aPage;
  }

  var url = "direct_messages";
  gApi.apiHitsRemaining--;
  gApi._call(aFlockListener, url, params, "GET", null, null);
};


/**
 * Get a count of all messages for the authenticated user
 * @param  aFlockListener
 */
TwitterAPI.prototype.getTotalMessageCount =
function api_getTotalMessageCount(aFlockListener) {
  this._logger.debug(".getTotalMessageCount(aFlockListener)");

  // We start off by asking for page 1 of messages.
  var page = 1;

  // Message counter
  var count = 0;

  var inst = this;
  var messageLoopFlockListener = {
    onSuccess: function L_onSuccess(aResult, aTopic) {
      // If a full page is returned then go grab another
      inst._logger.debug("Twitter messages returned: " + aResult.length);
      count += aResult.length;
      if (aResult.length == TWITTER_MESSAGES_PAGE_SIZE) {
        inst._logger.debug("Fetching more messages");
        page++;
        inst.directMessages(null, null, page, messageLoopFlockListener);
      } else {
        inst._logger.debug("messageLoopFlockListener() Fetching messages done");
        var result = { count: count };
        aFlockListener.onSuccess(result, aTopic);
      }
    },
    onError: function L_onError(aFlockError, aTopic) {
      inst._logger.debug("messageLoopFlockListener() onError");
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  this.directMessages(null, null, page, messageLoopFlockListener);
};

/**
 * Returns the 20 most recent statuses posted by the authenticating user and
 * that user's friends. This is the equivalent of /home on the Web.
 * @param  aFlockListener
 */
TwitterAPI.prototype.getFriendsTimeline =
function api_getFriendsTimeline(aFlockListener) {
  this._logger.debug(".getFriendsTimeline()");
  var url = "statuses/friends_timeline";
  gApi.apiHitsRemaining--;
  gApi._call(aFlockListener, url, null, "GET", null, null);
};

/**
 * Get a user's friends' updates.
 * @param  aUid  Twitter uid of the user whose friends to view or null to
 *               view the currently authenticated user's friends.
 * @param  aFlockListener
 */
TwitterAPI.prototype.getFriendsStatus =
function api_getFriendsStatus(aUid, aFlockListener) {
  this._logger.debug(".getFriendsStatus(" + aUid + ", aFlockListener)");

  var peopleHash = {};
  var myFollowers;
  var inst = this;

  var params = {};
  // We start off by asking for page 1 of friends.
  params.page = 1;

  var url = "followers/ids";
  var friendsLoopFlockListener = {
    onSuccess: function L_onSuccess(aResult, aTopic) {
      for each (var res in aResult) {
        var id = res.id;
        res.followsMe = myFollowers.indexOf(id) != -1;
        peopleHash[id] = res;

        // If there is no status item then stub one in
        if (!res.status) {
          res.status = { created_at: 0, text: "", id: null};
        }

        inst._logger.debug("Got Twitter person: " + res.name);
      }

      // Keep grabbing pages until there are no more results returned
      inst._logger.debug("Twitter friends returned: " + aResult.length);
      if (aResult.length > 0) {
        params.page++;
        inst._logger.debug("Fetching more friends, now getting page "
                           + params.page);
        gApi.apiHitsRemaining--;
        gApi._call(friendsLoopFlockListener, url, params, "GET", null, null);
      } else {
        inst._logger.debug("friendsLoopFlockListener() Fetching friends done");
        aFlockListener.onSuccess(peopleHash, aTopic);
      }
    },
    onError: aFlockListener.onError
  };
  var myFollowersFlockListener = {
    onSuccess: function F_onSuccess(aResult, aTopic) {
      myFollowers = aResult;
      url = "statuses/friends";
      if (aUid) {
        url += "/" + aUid;
      }
      gApi.apiHitsRemaining--;
      gApi._call(friendsLoopFlockListener, url, null, "GET", null, null);
    },
    onError: aFlockListener.onError
  };

  if (aUid) {
    url += "/" + aUid;
  }
  gApi.apiHitsRemaining--;
  gApi._call(myFollowersFlockListener, url, null, "GET", null, null);
};

TwitterAPI.prototype.getReplies =
function api_getReplies(aSinceId, aFlockListener) {
  this._logger.debug(".getReplies()");
  var inst = this;
  var repliesListener = {
    onSuccess: function repliesListener_onSuccess(aResult, aTopic) {
      inst._logger.debug(".getReplies(): onSuccess()");
      aFlockListener.onSuccess(aResult, aTopic);
    },
    onError: aFlockListener.onError
  };
  gApi.apiHitsRemaining--;
  gApi._call(repliesListener, "statuses/replies", aSinceId, "GET", null, null);
};

TwitterAPI.prototype.getDirectMessages =
function api_getDirectMessages(aSinceId, aFlockListener) {
  this._logger.debug(".getDirectMessages()");
  var inst = this;
  var dmListener = {
    onSuccess: function dmListener_onSuccess(aResult, aTopic) {
      inst._logger.debug(".getDirectMessages(): onSuccess()");
      aFlockListener.onSuccess(aResult, aTopic);
    },
    onError: aFlockListener.onError
  };
  gApi.apiHitsRemaining--;
  gApi._call(dmListener, "direct_messages", aSinceId, "GET", null, null);
};

/**
 * Set the user's status
 * @param  aStatusMessage  a string containing the message to set
 * @param  aFlockListener
 *
 * Notes from Twitter API documentation:
 * -----------------------------------------------------------------------
 * Request must be a POST.
 * URL: http://twitter.com/statuses/update.format
 * Formats: xml, json.
 *   Returns the posted status in requested format when successful.
 * Parameters:
 *   status  Required  The text of your status update.
 *                     Be sure to URL encode as necessary.
 *                     Must not be more than 160 characters and should not
 *                     be more than 140 characters to ensure optimal display
 */
TwitterAPI.prototype.setStatus =
function api_setStatus(aStatusMessage, aInReplyToStatusId, aFlockListener) {
  this._logger.debug(".setStatus(" + aStatusMessage + ", aFlockListener)");

  var message = aStatusMessage.substring(0, TWITTER_MAX_STATUS_LENGTH);

  var postVars = {
    "source": "flock", // This value specified by Alex Payne at Twitter.
    "status": message
  };
  if (aInReplyToStatusId) {
    postVars["in_reply_to_status_id"] = aInReplyToStatusId;
  }
  var url = "statuses/update";
  gApi._call(aFlockListener, url, null, "POST", postVars, null);
};


/*************************************************************************
 * Component: flockTwitterAccount
 *************************************************************************/

function flockTwitterAccount() {
  FlockSvcUtils.getLogger(this);
  this._logger.init("twitterAccount");
  this._ppSvc = CC["@flock.com/people-service;1"]
                .getService(CI.flockIPeopleService);

  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);
  // BC: Do not use FlockSvcUtils.getWD() here, because it would load
  //     the Web detective file a second time
  this._WebDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);

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

  FlockSvcUtils.flockISocialAccount
               .addDefaultMethod(this, "getFriendCount");
  FlockSvcUtils.flockISocialAccount
               .addDefaultMethod(this, "enumerateFriends");
  FlockSvcUtils.flockISocialAccount
               .addDefaultMethod(this, "getInviteFriendsURL");
  FlockSvcUtils.flockISocialAccount
               .addDefaultMethod(this, "getFormattedFriendUpdateType");

  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  this._bundle = sbs.createBundle(TWITTER_PROPERTIES);
}


/*************************************************************************
 * flockTwitterAccount: XPCOM Component Creation
 *************************************************************************/

// 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().
flockTwitterAccount.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " Account",
  "",
  "",
  flockTwitterAccount,
  0,
  [
    CI.flockIWebServiceAccount,
    CI.flockISocialAccount
  ]
  );


/*************************************************************************
 * flockTwitterAccount: flockIWebServiceAccount Implementation
 *************************************************************************/

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

// DEFAULT: void login(in flockIListener aFlockListener);
// DEFAULT: void logout(in flockIListener aFlockListener);

// void keep();
flockTwitterAccount.prototype.keep =
function fta_keep() {
  this._logger.debug(".keep()");
  this.coopObj.isTransient = false;
  var urn = this.urn.replace("account:", "service:").replace("flock:", "");
  this._acUtils.makeTempPasswordPermanent(urn);
};


/*************************************************************************
 * flockTwitterAccount: flockISocialAccount Implementation
 *************************************************************************/
// readonly attribute boolean hasFriendActions;
flockTwitterAccount.prototype.hasFriendActions = true;

// readonly attribute boolean isPostLinkSupported;
flockTwitterAccount.prototype.isPostLinkSupported = false;

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

// readonly attribute boolean isMeStatusSupported;
flockTwitterAccount.prototype.isMeStatusSupported = true;

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

// readonly attribute boolean isStatusEditable;
flockTwitterAccount.prototype.isStatusEditable = true;

flockTwitterAccount.prototype.formatFriendActivityForDisplay =
function flockTwitterAccount_formatFriendActivityForDisplay(aFriendActivityUrn)
{
  this._logger.info(".formatFriendActivityForDisplay()");

  var friendActivity = this._coop.get(aFriendActivityUrn);

  if (friendActivity.updateType == "profile") {
    return this._bundle
               .GetStringFromName("flock.friendFeed.lastUpdateTypePretty.profile");
  } else if (friendActivity.updateType == "status") {
    if (friendActivity.updateValue == "") {
      return this._bundle
                 .GetStringFromName("flock.friendFeed.lastUpdateTypePretty.statusCleared");
    } else {
      var updateValue = (friendActivity.updateValue) ? friendActivity.updateValue : "";
      return this._bundle
                 .GetStringFromName("flock.friendFeed.lastUpdateTypePretty.statusUpdated.prefix")
                                    + " " + updateValue;
    }
  }

  return "";
};

// AString formatStatusForDisplay(in AString aStatusMessage);
flockTwitterAccount.prototype.formatStatusForDisplay =
function fta_formatStatusForDisplay(aStatusMessage) {
  this._logger.debug(".formatStatusForDisplay(" + aStatusMessage + ")");

  var message = (aStatusMessage) ? aStatusMessage : "";

  // Responses from Twitter contain HTML entities.
  message = flockXMLDecode(message);

  return message;
};

// AString getProfileURLForFriend(in AString aFriendUrn);
flockTwitterAccount.prototype.getProfileURLForFriend =
function fta_getProfileURLForFriend(aFriendUrn) {
  this._logger.debug(".getProfileURLForFriend('" + aFriendUrn + "')");

  var url = "";
  var coopFriend = this._coop.get(aFriendUrn);
  var screenName = coopFriend.screenName;

  if (screenName) {
    url = this._WebDetective.getString(CLASS_SHORT_NAME, "friendProfile", "")
                            .replace("%screenName%", screenName);
  }

  this._logger.debug(".getProfileURLForFriend()  url: " + url);
  return url;
};

// void setStatus(in AString aStatusMessage, in flockIListener aFlockListener);
flockTwitterAccount.prototype.setStatus =
function fta_setStatus(aStatusMessage, aFlockListener) {
  this._logger.debug(".setStatus(" + aStatusMessage + ")");

  // Trim whitespace from front and end of string.
  var statusMessage = aStatusMessage.replace(/^\s+|\s+$/g, "");

  if (!aFlockListener) {
    var posSlash = statusMessage.lastIndexOf("/");
    if (posSlash != -1) {
      this.inReplyToStatusId = statusMessage.substring(posSlash + 1);
    }
    return;
  }

  var messageType;
  if (statusMessage.indexOf("RT ") == 0) {
    messageType = "retweet";
  } else if ((statusMessage.indexOf("d ") == 0) ||
             (statusMessage.indexOf("dm ") == 0))
  {
    messageType = "directMessage";
  } else if (this.inReplyToStatusId) {
    messageType = "reply";
  } else {
    messageType = "tweet";
  }

  var inst = this;
  var statusFlockListener = {
    onSuccess: function L_onSuccess(aResult, aTopic) {
      if (messageType != "directMessage") {
        // If the API call succeeded, also set the coop.Account status.
        inst.coopObj.statusMessage = statusMessage;

        // Save the current time in seconds
        var now = new Date().getTime();
        now = Math.round(now / 1000);
        inst.coopObj.lastStatusMessageUpdateDate = now;
      }

      if (aFlockListener) {
        if (messageType == "directMessage") {
          aFlockListener.onSuccess(aResult, "directMessage");
        } else {
          aFlockListener.onSuccess(aResult, "setStatus");
        }
      }

      // Report the type of the Twitter status change:
      // - tweet, retweet, directMessage, reply
      var metrics = CC["@flock.com/metrics-service;1"]
                    .getService(CI.flockIMetricsService);
      metrics.report("TwitterService-TwitterStatusChange", messageType);
    },
    onError: function L_onError(aFlockError, aTopic) {
     if (aFlockListener) {
        if (messageType == "directMessage") {
          aFlockListener.onError(aFlockError, "directMessage");
        } else {
          aFlockListener.onError(aFlockError, "setStatus");
        }
      }
    }
  };
  gApi.setStatus(statusMessage, this.inReplyToStatusId, statusFlockListener);
  this.inReplyToStatusId = null;
};

// AString getEditableStatus();
flockTwitterAccount.prototype.getEditableStatus =
function fta_getEditableStatus() {
  this._logger.debug(".getEditableStatus()");
  var message = this.coopObj.statusMessage;
  return this.formatStatusForDisplay(message);
};

// AString getMeNotifications();
flockTwitterAccount.prototype.getMeNotifications =
function fta_getMeNotifications() {
  this._logger.debug(".getMeNotifications()");

  var noties = [];
  var inst = this;
  function addNotie(aNotieType, aActivityType) {
    var c_acct = inst.coopObj;
    var count = c_acct.get(aActivityType + "Count", true);
    var stringName = "flock." + CLASS_SHORT_NAME + ".noties." + aNotieType
                   + "." + ((parseInt(count, 10) <= 0) ? "none" : "some");
    inst._logger.debug("aNotieType: " + aNotieType
                       + " count: " + count
                       + " name: " + stringName);
    noties.push({
      class: aNotieType,
      tooltip: inst._bundle.GetStringFromName(stringName),
      metricsName: aNotieType,
      count: count,
      URL: "twitterview:" + aActivityType
    });
  }
  addNotie("meMentions", "reply");
  addNotie("meMessages", "dmessage");

  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  return nsJSON.encode(noties);
};

// void markAllMeNotificationsSeen(in AString aType);
flockTwitterAccount.prototype.markAllMeNotificationsSeen =
function fta_markAllMeNotificationsSeen(aType) {
  this._logger.debug(".markAllMeNotificationsSeen('" + aType + "')");
  switch (aType) {
    case "meMessages":
      this.coopObj.accountMessages = 0;
      break;
    default:
      break;
  }
};

// void shareFlock(in AString aFriendURN);
flockTwitterAccount.prototype.shareFlock =
function fta_shareFlock(aFriendURN) {
  // Not implemented.
};

// void friendAction(in AString aAction, in AString aFriendURN, in AString..
flockTwitterAccount.prototype.friendAction =
function fta_friendAction(aAction, aFriendURN, aActivityURN) {
  var url;
  switch (aAction) {
    case "statusComment":
      if (aActivityURN) {
        var activity = this._coop.FriendActivity.get(aActivityURN);
        var updateURL = activity.updateURL;
        var posSlash = updateURL.lastIndexOf("/");
        if (posSlash == -1) {
          break;
        }
        url = this._WebDetective
                  .getString(CLASS_SHORT_NAME, "replyURL", "");
        loadURIIn(url.replace("%screenName%", activity.identity.screenName, "g")
                     .replace("%statusid%", updateURL.substring(posSlash + 1)));
      }
      break;
    case "replyFromTwitterSearch":
      if (aActivityURN) {
        var searchItem = this._coop.get(aActivityURN);
        var screenName = searchItem.fromUsername;
        var statusId = searchItem.itemId;
        url = this._WebDetective
                  .getString(CLASS_SHORT_NAME, "replyURL", "");
        loadURIIn(url.replace("%screenName%", screenName, "g")
                     .replace("%statusid%", statusId));
      }
      break;
    default:
      this._logger.debug("Invalid friendAction: '" + aAction + "'");
      break;
  }
};

// AString getFriendActions(in AString aFriendUrn);
flockTwitterAccount.prototype.getFriendActions =
function fta_getFriendActions(aFriendUrn) {
  this._logger.debug(".getFriendActions('" + aFriendUrn + "')");

  var actions = [];
  var coopFriend = this._coop.get(aFriendUrn);
  if (coopFriend) {
    var actionNames = ["friendRetweet",
                       "friendViewProfile"];
    var followsMe = coopFriend.get("followsMe", true);
    if (followsMe && followsMe != "false") {
      actionNames.splice(1, 0, "friendMessage");
    }

    // Remove re-tweet action if this friend is in the DM view
    var wm = CC["@mozilla.org/appshell/window-mediator;1"]
             .getService(CI.nsIWindowMediator);
    var win = wm.getMostRecentWindow("navigator:browser");
    if (win) {
      var sidebar = win.document.getElementById("sidebar");
      if (sidebar) {
        var sidedoc = sidebar.contentDocument;
        // Check if the Twitter sidebar is open (as opposed to the all view)
        var psTabs = sidedoc.getElementById("peopleServiceTabs");
        if (psTabs && psTabs.selectedServiceId == CONTRACT_ID) {
          // See if the DM button is selected
          var meCard = sidedoc.getElementById("mecard:" + CONTRACT_ID);
          var dmBtn = sidedoc.getAnonymousElementByAttribute(
                      meCard, "anonid", "dmessages-button-container");
          if (dmBtn && dmBtn.hasAttribute("selected")) {
            // Do not re-tweet DMs
            actionNames.splice(0, 1);
          }
        }
      }
    }

    // Format:  urn:flock:identity:userId:friendId:statusId
    // Example: urn:flock:identity:twitter9365042:8980522
    // Example: urn:flock:identity:twitter9365042:9365042:1402135691
    var friendId = aFriendUrn.split(":")[4];
    for each (var action in actionNames) {
      actions.push({
        label: this._bundle.GetStringFromName("flock."
                                              + CLASS_SHORT_NAME
                                              + ".actions." + action),
        class: action,
        spec: this._WebDetective.getString(CLASS_SHORT_NAME, action, "")
                  .replace("%accountid%", this.coopObj.accountId)
                  .replace("%friendid%", friendId)
      });
    }
  }

  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  return nsJSON.encode(actions);
};

// DEFAULT: long getFriendCount();

// void shareWithFriend(in AString aFriendUrn,
//                      in nsITransferable aTransferable);
flockTwitterAccount.prototype.shareWithFriend =
function fta_shareWithFriend(aFriendUrn, aTransferable) {
  this._logger.debug(".shareWithFriend('" + aFriendUrn + "')");

  var sharingAction = "";
  var coopFriend = this._coop.get(aFriendUrn);
  if (!coopFriend) {
    return;
  }

  var flavours = ["text/x-flock-media",
                  "text/x-moz-url",
                  "text/unicode"];

  var message = CC[FLOCK_RICH_DND_CONTRACTID]
                .getService(CI.flockIRichDNDService)
                .getMessageFromTransferable(aTransferable,
                                            flavours.length,
                                            flavours);
  if (!message.body) {
    return;
  }

  // Format:  urn:flock:identity:userId:friendId:statusId
  // Example: urn:flock:identity:twitter9365042:8980522
  // Example: urn:flock:identity:twitter9365042:9365042:1402135691
  var friendId = aFriendUrn.split(":")[4];
  sharingAction = this._WebDetective
                      .getString(CLASS_SHORT_NAME,
                                 "shareAction_directMessage", "")
                      .replace("%friendid%", friendId)
                      .replace("%message%", encodeURIComponent(message.body));

  var specType;
  var sharingUri;
  var actionArray = sharingAction.split(":");
  if (actionArray.length == 1) {
    sharingUri = sharingAction;
  } else {
    specType = actionArray.shift();
    sharingUri = actionArray.join(":");
  }
  loadURIIn(sharingUri, specType);
};

// void postLink(in nsITransferable aTransferable);
flockTwitterAccount.prototype.postLink =
function fta_postLink(aTransferable) {
  throw CR.NS_ERROR_NOT_IMPLEMENTED;
};


/*************************************************************************
 * flockTwitterAccount Private Data and Functions
 *************************************************************************/

flockTwitterAccount.prototype._processTweets =
function fta__processTweets(aAPIResult, aActivityType) {
  var url = this._WebDetective
                .getString(CLASS_SHORT_NAME, "statusURL", "");
  if (!this.coopObj.otherActivityLog) {
    this.coopObj.otherActivityLog = new this._coop.FriendActivityLog();
  }
  var otherLog = this.coopObj.otherActivityLog.children;
  var count = this.coopObj.get(aActivityType + "Count", true);
  if (!count) {
    count = "0";
  }
  count = parseInt(count, 10);
  for each (let item in aAPIResult) {
    var user = (item.user) ? item.user : item.sender;
    var idUrn = getIdentityUrn(this.coopObj.accountId, user.id);
    var identity = this._coop.get(idUrn);
    var tweetUrn = idUrn + ":" + item.id;
    var tweet = this._coop.get(tweetUrn);
    if (!tweet) {
      var updateValues = {
        updateType: aActivityType,
        updateDate: new Date(item.created_at),
        updateValue: item.text,
        updateURL: url.replace("%screenName%", user.screen_name)
                      .replace("%statusid%", item.id)
      };
      if (identity) {
        tweet = new this._coop.FriendActivity(tweetUrn, updateValues);
        tweet.identity = identity;
      } else {
        tweet = new this._coop.NonFriendActivity(tweetUrn, updateValues);
        tweet.avatar = user.profile_image_url;
        tweet.name = item.name;
        tweet.screenName = user.screen_name;
      }
      otherLog.insertSortedOn("http://flock.com/rdf#updateDate", tweet, 100);
      count++;
    }
  }
  if (count > 0) {
    this._logger.debug("Twitter Toggle");
    this._ppSvc.togglePeopleIcon(true);
  }
  this.coopObj.set(aActivityType + "Count", count, true);
};

flockTwitterAccount.prototype._mostRecentId =
function fta__mostRecentId(aActivityType) {
  var otherActivityLog = this.coopObj.otherActivityLog;
  if (otherActivityLog) {
    var childEnum = otherActivityLog.children.enumerateBackwards();
    while (childEnum.hasMoreElements()) {
      var child = childEnum.getNext();
      if (child.updateType == aActivityType) {
        return { since_id: child.id().split(":").pop() };
      }
    }
  }
  return null;
};

/*************************************************************************
 * Component: flockTwitterSearch
 *************************************************************************/
function flockTwitterSearch() {
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "url");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getStringBundle");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "isHidden");
  if (!gApi) {
    // Initialize API (which also loads the web detective file)
    gApi = new TwitterAPI();
  }
  this._init();
}


/*************************************************************************
 * flockTwitterSearch: XPCOM Component Creation
 *************************************************************************/

flockTwitterSearch.prototype = new FlockXPCOMUtils.genericComponent(
  TS_CLASS_NAME,
  TS_CLASS_ID,
  TS_CONTRACT_ID,
  flockTwitterSearch,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.flockITwitterSearch,
    CI.flockIWebService,
    CI.flockIPollingService
  ]
  );

// FlockXPCOMUtils.genericModule() categories
flockTwitterSearch.prototype._xpcom_categories = [
  { category: "flockWebService", entry: TS_CLASS_SHORT_NAME }
];


/*************************************************************************
 * flockTwitterSearch: Private Data and Functions
 *************************************************************************/

// Member variables.
flockTwitterSearch.prototype._init =
function ftss__init() {
  FlockSvcUtils.getLogger(this);
  this._logger.debug(".init()");

  // 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(TS_SERVICE_ENABLED_PREF) &&
     !prefService.getBoolPref(TS_SERVICE_ENABLED_PREF))
  {
    this._logger.debug(TS_SERVICE_ENABLED_PREF + " is FALSE! Not initializing");
    this.deleteCategories();
    return;
  }

  var profiler = CC["@flock.com/profiler;1"].getService(CI.flockIProfiler);
  var evtID = profiler.profileEventStart("twitter-init");

  FlockSvcUtils.initService(this);

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

  profiler.profileEventEnd(evtID, "");
};

/*************************************************************************
 * flockTwitterSearch: flockITwitterSearch Implementation
 *************************************************************************/

flockTwitterSearch.prototype.getTrends =
function ftss_getTrends(aFlockListener) {
  this._logger.debug(".getTrends()");

  var trendsListener = {
    onSuccess: function trendsListener_onSuccess(aSubject, aTopic) {
      var trends = [];
      for each (var item in aSubject.trends) {
        for each (var trend in item) {
          trends.push(trend);
        }
      }

      aFlockListener.onSuccess({ wrappedJSObject: { trends: trends } },
                               "getTrends");
    },
    onError: function trendsListener_onError(aFlockError, aTopic) {
      aFlockListener.onError(aFlockError, "getTrends");
    }
  };

  gApi._call(trendsListener, "trends", {}, "GET", null, null, ".json");
};

/*************************************************************************
 * flockTwitterSearch: flockIWebService Implementation
 *************************************************************************/

// readonly attribute AString title;
flockTwitterSearch.prototype.title = TS_CLASS_TITLE;

// readonly attribute AString shortName;
flockTwitterSearch.prototype.shortName = TS_CLASS_SHORT_NAME;

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

// DEFAULT: readonly attribute AString url;

// readonly attribute AString contractId;
flockTwitterSearch.prototype.contractId = TS_CONTRACT_ID;

// readonly attribute AString urn;
flockTwitterSearch.prototype.urn = "urn:" + TS_CLASS_SHORT_NAME + ":service";

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


/*************************************************************************
 * flockTwitterSearch: flockIPollingService Implementation
 *************************************************************************/

flockTwitterSearch.prototype.refresh =
function ftss_refresh(aUrn, aPollingListener) {
  this._logger.debug(".refresh(" + aUrn + ")");

  var searchCoopobj = this._coop.get(aUrn);
  if (!searchCoopobj instanceof this._coop.TwitterSearch) {
    this._logger.error("can't refresh " + aUrn + " (unsupported type)");
    aPollingListener.onError(null);
    return;
  }

  var searchText = searchCoopobj.searchTerm;
  this._logger.debug("searchText: " + searchText);

  var inst = this;
  var params = {
    "q": searchText
  };

  var searchListener = {
    onSuccess: function searchListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("searchListener() onSuccess");
      var newIdList = [];
      var numInserted = 0;

      for each (var item in aSubject.results) {
        var coopId = "urn:flock:twitter:searchitem:" + item.id;
        newIdList.push(coopId);
        var searchItem = inst._coop.TwitterSearchItem.get(coopId);
        if (!searchItem) {
          searchItem = new inst._coop.TwitterSearchItem(coopId,
            {
              itemId: item.id,
              itemText: item.text,
              fromUserId: item.from_user_id,
              fromUsername: item.from_user,
              toUserId: item.to_user_id,
              toUsername: item.to_user,
              avatar: item.profile_image_url,
              createDate: new Date(item.created_at)
            }
          );
        }
        if (!searchCoopobj.children.has(searchItem)) {
          searchCoopobj.children.insertAt(searchItem, ++numInserted);
        }
      }

      // Clear old search items in RDF
      var childrenEnum = searchCoopobj.children.enumerate();
      while (childrenEnum.hasMoreElements()) {
        var searchItem = childrenEnum.getNext();
        if (newIdList.indexOf(searchItem.id()) == -1) {
          searchItem.destroy();
        }
      }
      aPollingListener.onResult();
    },
    onError: function searchListener_onError(aFlockError, aTopic) {
      inst._logger.debug("searchListener() onError");
      aPollingListener.onError(aFlockError);
    }
  };

  gApi._call(searchListener, "search", params, "GET", null, null, ".json");
};


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

// Create array of components.
var componentsArray = [flockTwitterService, flockTwitterSearch];

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