// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2008
// 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");

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 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 friends in pages of 100.
const TWITTER_FRIENDS_PAGE_SIZE = 100;

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

/*************************************************************************
 * 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, "getAccount");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getAuthenticatedAccount");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getAccounts");
  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.nsIObserver,
    CI.flockIWebService,
    CI.flockILoginWebService,
    CI.flockIPollingService,
    CI.flockISocialWebService,
    CI.flockIRichContentDropHandler
  ]
  );

// 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._obs.addObserver(this, "xpcom-shutdown", false);

  this._accountClassCtor = flockTwitterAccount;

  FlockSvcUtils.getCoop(this);

  this._baseUrn = "urn:twitter";
  this._serviceUrn = this._baseUrn + ":service";

  this._c_svc = this._coop.get(this._serviceUrn);
  if (!this._c_svc) {
    this._c_svc = new this._coop.Service(this._serviceUrn, {
      name: CLASS_SHORT_NAME,
      desc: CLASS_TITLE
    });
  }
  this._c_svc.serviceId = CONTRACT_ID;
  this._c_svc.logoutOption = true;

  // Note that using FlockSvcUtils.getWD here adds the "_WebDetective"
  // property to the service.
  this._c_svc.domains = FlockSvcUtils.getWD(this)
                        .getString("twitter", "domains", "twitter.com");

  // Initialize API
  gApi = new TwitterAPI();

  // Update auth states
  try {
    if (this._WebDetective.detectCookies("twitter", "loggedoutcookies", null)) {
      this._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
    }
  } catch (ex) {
    this._logger.error("ERROR updating auth states for Twitter: " + ex);
  }

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

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

flockTwitterService.prototype._refreshAccount =
function fts__refreshAccount(aCoopAcct, aPollingListener) {
  var inst = this;

  var pplRefreshListener = CC["@flock.com/people-refresh-listener;1"]
                           .createInstance(CI.flockIPeopleRefreshListener);
  pplRefreshListener.init(4, aCoopAcct.id(), aPollingListener, 0);

  /**
   * {
   *   "status": {
   *     "created_at": "Thu May 17 19:19:39 +0000 2007",
   *     "text": "attending the full staff meeting",
   *     "id": 67511202
   *   },
   *   "url": null,
   *   "followers_count": 0,
   *   "friends_count": 0,
   *   "profile_background_color": "9ae4e8",
   *   "name": "Matthew Willis",
   *   "favourites_count": 0,
   *   "profile_text_color": "000000",
   *   "statuses_count": 1,
   *   "profile_link_color": "0000ff",
   *   "description": null,
   *   "profile_sidebar_fill_color": "e0ff92",
   *   "location": null,
   *   "profile_image_url": "http:\/\/assets0.twitter.com\/images\/default_image.gif?1189634879",
   *   "id": 6117302,
   *   "utc_offset": -14400,
   *   "profile_sidebar_border_color": "87bc44",
   *   "screen_name": "lilmatt",
   *   "protected": false
   * }
   */

  // 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);
        aCoopAcct.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);
        aCoopAcct.screenName = screenName;
        aCoopAcct.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.");
          aCoopAcct.avatar = null;
        } else {
          aCoopAcct.avatar = avatarURL;
        }
      } catch (ex) {
        aCoopAcct.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 = inst._parseTwitterDate(dateString);
        // Update the status if is more recent than the current one.
        if (lastStatusMessageUpdateDate > aCoopAcct.lastStatusMessageUpdateDate) {
          var status = statusXML[0].getElementsByTagName("text")[0].firstChild
                                                                   .nodeValue;
          inst._logger.debug("userShow: status: " + status);
          if (status != aCoopAcct.statusMessage) {
            aCoopAcct.statusMessage = status;
          }
          aCoopAcct.lastProfileUpdate = lastStatusMessageUpdateDate;
          aCoopAcct.lastStatusMessageUpdateDate
            = lastStatusMessageUpdateDate;
        }
      } catch (ex) {
        inst._logger.error("No status info returned.");
      }
      inst._obs.notifyObservers(aCoopAcct.resource(),
                                "flock-acct-refresh",
                                "user-info");
      pplRefreshListener.onSuccess(null, "userShowFlockListener");
    },
    onError: function userShow_onError(aFlockError, aTopic) {
      inst._logger.error("userShowFlockListener onError");
      pplRefreshListener.onError(aFlockError, "userShowFlockListener");
    }
  };

  // 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 (aCoopAcct.isAuthenticated) {
          // REMOVE locally people removed on the server
          var localEnum = aCoopAcct.friendsList.children.enumerate();
          while (localEnum.hasMoreElements()) {
            var identity = localEnum.getNext();
            if (!aResult[identity.accountId]) {
              inst._logger.info("Friend " + identity.accountId
                                + " has been deleted on the server");
              aCoopAcct.friendsList.children.remove(identity);
              identity.destroy();
            }
          }
          // ADD or update existing people
          for (var uid in aResult) {
            inst._addPerson(aResult[uid], aCoopAcct);
            if (aShouldYield()) {
              yield;
              if (!aCoopAcct.isAuthenticated) {
                // Account has just been deleted or logged out
                break;
              }
            }
          }

          var getFriendsTimelineListener = {
            onSuccess: function getFriendsTimelineListener_onSuccess(aResult,
                                                                     aTopic)
            {
              var allStatusesXML = aResult.getElementsByTagName("status");
              for (var i = 0; i < allStatusesXML.length; i++) {
                var statusXML = allStatusesXML[i];
                // Pull out the user info.
                var userXML = statusXML.getElementsByTagName("user");
                var userName = inst._getNodeValueByTagName(userXML[0], "name");
                var userId = inst._getNodeValueByTagName(userXML[0], "id");

                if (!userName || !userId) {
                  // Something wrong here, so skip.
                  inst._logger.error("Unable to parse out user information");
                  continue;
                }

                // Pull out the status info.
                var createdAt = inst._getNodeValueByTagName(statusXML, "created_at");
                var statusMessage = inst._getNodeValueByTagName(statusXML, "text");
                
                if (!statusMessage) {
                  // Something wrong here, so skip.
                  inst._logger.error("Unable to parse out status information");
                  continue;
                }

                inst._logger.debug("statusMessage = " + statusMessage);
                inst._logger.debug("createdAt = " + createdAt);

                var lastStatusMessageUpdateDate
                  = inst._parseTwitterDate(createdAt);
                if (aCoopAcct.accountId == userId) {
                  // We have a status update on the account.
                  if (lastStatusMessageUpdateDate <
                      aCoopAcct.lastStatusMessageUpdateDate) {
                    // Status is old so skip.
                    continue;
                  }

                  aCoopAcct.statusMessage = statusMessage;
                  aCoopAcct.lastUpdateType = "status";
                  aCoopAcct.lastProfileUpdate = lastStatusMessageUpdateDate;
                  aCoopAcct.lastStatusMessageUpdateDate
                    = lastStatusMessageUpdateDate;

                } else {

                  // We have a status update on a friend
                  var identityUrn = inst._getIdentityUrn(aCoopAcct.accountId,
                                                         userId);
                  var identity = inst._coop.get(identityUrn);

                  if (!identity) {
                    // For some reason we aren't tracking this friend so skip.
                    continue;
                  }

                  if (lastStatusMessageUpdateDate < identity.lastUpdate) {
                    // Status is old so skip.
                    continue;
                  }

                  identity.statusMessage = statusMessage;
                  identity.lastUpdateType = "status";
                  identity.lastUpdate = lastStatusMessageUpdateDate;
                }
              }
              pplRefreshListener.onSuccess(null, "getFriendsTimelineListener");
              pplRefreshListener.refreshFinished();
            },
            onError: function getFriendsTimelineListener_onError(aFlockError,
                                                                 aTopic)
            {
              inst._logger.error("getFriendsTimelineListener onError");
              pplRefreshListener.onError(aFlockError, "getFriendsTimelineListener");
            }
          };
          gApi.getFriendsTimeline(getFriendsTimelineListener);
          pplRefreshListener.onSuccess(null, "friendsFlockListener");
        }
      }

      FlockScheduler.schedule(null, 0.05, 10, myWorker);
    },
    onError: function friends_onError(aFlockError, aTopic) {
      inst._logger.error("friendsFlockListener onError");
      pplRefreshListener.onError(aFlockError, "friendsFlockListener");
    }
  };

  // This listener handles getting a count of the user's direct messages.
  var messageCountFlockListener = {
    onSuccess: function messages_onSuccess(aResult, aTopic) {
      inst._logger.debug("messageCountFlockListener: count = " + aResult.count);
      aCoopAcct.accountMessages = aResult.count;

      // Trigger the people icon highlight if the user has messages
      if (aResult.count > 0) {
        inst._ppSvc.togglePeopleIcon(true);
      }
      inst._obs.notifyObservers(aCoopAcct.resource(),
                                "flock-acct-refresh",
                                "user-info");
      pplRefreshListener.onSuccess(null, "messageCountFlockListener");
    },
    onError: function messages_onError(aFlockError, aTopic) {
      inst._logger.error("messageCountFlockListener onError");
      pplRefreshListener.onError(aFlockError, "messageCountFlockListener");
    }
  };

  gApi.userShow(aCoopAcct.accountId, userShowFlockListener);
  gApi.getFriendsStatus(null, friendsFlockListener);
  gApi.getTotalMessageCount(messageCountFlockListener);
};


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

  var lastUpdate = this._parseTwitterDate(aPerson.status.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.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 = aPerson.status.text;
  identity.lastUpdateType = lastUpdateType;
  identity.lastUpdate = lastUpdate; // triggers the RDF observers
};


flockTwitterService.prototype._getIdentityUrn =
function fts__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.
 */
flockTwitterService.prototype._parseTwitterDate =
function fts__parseTwitterDate(aDateString) {
  this._logger.debug("_parseTwitterDate: in: " + aDateString);
  if (!aDateString) {
    return 0;
  }
  // Date.parse() returns milliseconds since epoch. Divide by 1000 for seconds.
  return (Date.parse(aDateString) / 1000);
};


/**
 * 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 accountUrn = "urn:flock:" + this.shortName + ":account:" + aAccountId;
  var account = new this._coop.Account(
    accountUrn, {
      name: name,
      screenName: name,
      serviceId: this.contractId,
      service: this._c_svc,
      accountId: aAccountId,
      isPollable: true,
      isTransient: aIsTransient,
      URL: this._WebDetective.getString(CLASS_SHORT_NAME, "userprofile", null)
               .replace("%accountid%", name),
      refreshInterval: REFRESH_INTERVAL,
      favicon: FAVICON
    });
  this._coop.accounts_root.children.add(account);

  var friendsListUrn = accountUrn + ":friends";
  var friendsList = new this._coop.FriendsList(
    friendsListUrn,
    {
      account: account
    });
  account.friendsList = friendsList;

  // Instantiate account component
  var acct = this.getAccount(account.id());
  if (aFlockListener) {
    aFlockListener.onSuccess(acct, "addAccount");
  }
  return acct;
};

// DEFAULT: flockIWebServiceAccount getAccount(in AString aUrn);
// DEFAULT: nsISimpleEnumerator getAccounts();
// 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._c_svc.domains;
});

// 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,
                                             aAcctURN,
                                             aFlockListener)
{
  this._logger.debug("updateAccountStatusFromDocument('" + aAcctURN + "')");
  if (aAcctURN) {
    this._logger.debug("We're logged in!");
    if (!this._acUtils.ensureOnlyAuthenticatedAccount(aAcctURN)) {
      if (aFlockListener) {
        aFlockListener.onSuccess(this.getAccount(aAcctURN), null);
      }
    }
  } else if (this._WebDetective
                 .detect(this.shortName, "loggedout", aDocument, null) ||
             this._WebDetective
                 .detectCookies(this.shortName, "loggedout", null))
  {
    this._logger.debug("We're logged out!");
    this._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
  } 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(NS_ERROR_NOT_IMPLEMENTED);
};

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._coop.get(aUrn);

  if (refreshItem instanceof this._coop.Account) {
    this._logger.debug("refreshing an Account");
    if (refreshItem.isAuthenticated) {
      this._refreshAccount(refreshItem, 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);
  }
};


/**************************************************************************
 * flockTwitterService: nsIObserver Implementation
 **************************************************************************/

flockTwitterService.prototype.observe =
function fts_observe(aSubject, aTopic, aState) {
  switch (aTopic) {
    case "xpcom-shutdown":
      this._obs.removeObserver(this, "xpcom-shutdown");
      break;
  }
};


/**************************************************************************
 * 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._c_svc.domains,
                                    aTargetElement,
                                    dropCallback);
  }

  // Default handling otherwise
  return false;
};


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

  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);

  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
 *************************************************************************/
/**
 * 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 url = "https://twitter.com/" + aMethod + responseFormat;
  var error = {};

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

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

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

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

  var svc = CC[CONTRACT_ID].getService(CI.flockILoginWebService);
  var account = svc.getAuthenticatedAccount();
  var coopAccount = this._coop.get(account.urn);
  var passwordUrn = "urn:twitter:service:" + coopAccount.accountId;
  var creds = this._acUtils.getPassword(passwordUrn);

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

  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);

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

        if (result) {
          aFlockListener.onSuccess(result, null);
        } else {
          inst._logger.debug("._call() error: no result");
          error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
          // XXX See bug 10749 - flockIError cleanup
          error.errorCode = CI.flockIError.INVALID_RESPONSE_XML;
          aFlockListener.onError(error, null);
        }
      } 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);
        } else {
          // Ok. Give up.
          inst._logger.debug("._call() HTTP error");
          error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
          error.serviceErrorCode = status;
          error.serviceErrorString = req.responseText;
          aFlockListener.onError(error, null);
        }

      } else {
        // HTTP errors (0 for connection lost)
        inst._logger.debug("._call() HTTP error");
        error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
        error.serviceErrorCode = status;
        error.serviceErrorString = req.responseText;
        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._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._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");
        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._call(aFlockListener, url, null, "GET", null, null, ".xml");
};

/**
 * 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 inst = this;

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

  var url = "statuses/friends";
  var friendsLoopFlockListener = {
    onSuccess: function L_onSuccess(aResult, aTopic) {
      for (var i in aResult) {
        var id = aResult[i].id;
        peopleHash[id] = aResult[i];

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

        inst._logger.debug("Got Twitter person: " + peopleHash[id].name);
      }

      // If a full page is returned then go grab another
      inst._logger.debug("Twitter friends returned: " + aResult.length);
      if (aResult.length == TWITTER_FRIENDS_PAGE_SIZE) {
        params.page++;
        inst._logger.debug("Fetching more friends, now getting page "
                           + params.page);
        gApi._call(friendsLoopFlockListener, url, params, "GET", null, null);
      } else {
        inst._logger.debug("friendsLoopFlockListener() Fetching friends done");
        aFlockListener.onSuccess(peopleHash, aTopic);
      }
    },
    onError: function L_onError(aError, aTopic) {
      inst._logger.debug("friendsLoopFlockListener() onError");
      aFlockListener.onError(aError, aTopic);
    }
  };

  if (aUid) {
    url += "/" + aUid;
  }
  gApi._call(friendsLoopFlockListener, url, null, "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, aFlockListener) {
  this._logger.debug(".setStatus(" + aStatusMessage + ", aFlockListener)");

  // substring() starts at 0 while TWITTER_MAX_STATUS_LENGTH counts from 1.
  var message = aStatusMessage.substring(0, (TWITTER_MAX_STATUS_LENGTH - 1));

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


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

function flockTwitterAccount() {
  FlockSvcUtils.getLogger(this);
  this._logger.init("twitterAccount");

  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);
  // XXX: I should be able to use FlockSvcUtils.getWD() here, but it can
  //      only be called by the service.
  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, "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._coop.get(this.urn).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 = true;

// 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 + ")");

  var inst = this;
  var statusFlockListener = {
    onSuccess: function L_onSuccess(aResult, aTopic) {
      // If the API call succeeded, also set the coop.Account status.
      inst._coop.get(inst.urn).statusMessage = aStatusMessage;

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

      if (aFlockListener) {
        aFlockListener.onSuccess(aResult, "setStatus");
      }
    },
    onError: function L_onError(aFlockError, aTopic) {
      if (aFlockListener) {
        aFlockListener.onError(aFlockError, "setStatus");
      }
    }
  };
  gApi.setStatus(aStatusMessage, statusFlockListener);
};

// AString getEditableStatus();
flockTwitterAccount.prototype.getEditableStatus =
function fta_getEditableStatus() {
  this._logger.debug(".getEditableStatus()");
  var message = this._coop.get(this.urn).statusMessage;
  message = (message) ? flockXMLDecode(message) : "";
  return this.formatStatusForDisplay(message);
};

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

  var noties = [];
  var inst = this;
  function addNotie(aType, aCount) {
    var stringName = "flock." + CLASS_SHORT_NAME + ".noties." + aType + "."
                   + ((parseInt(aCount) <= 0) ? "none" : "some");

    inst._logger.debug("aType: " + aType
                       + " aCount: " + aCount
                       + " name: " + stringName);
    noties.push({
      class: aType,
      tooltip: inst._bundle.GetStringFromName(stringName),
      count: aCount,
      URL: inst._WebDetective.getString(CLASS_SHORT_NAME, aType + "_URL", "")
    });
  }
  var coopAccount = this._coop.get(this.urn);
  addNotie("meMessages", coopAccount.accountMessages);

  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 + "')");
  var c_acct = this._coop.get(this.urn);
  switch (aType) {
    case "meMessages":
      c_acct.accountMessages = 0;
      break;
    default:
      break;
  }
};

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

  var actionNames = ["friendMessage",
                     "friendViewProfile"];

  var actions = [];
  var coopFriend = this._coop.get(aFriendUrn);
  if (coopFriend) {
    var coopAccount = this._coop.get(this.urn);
    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%", coopAccount.accountId)
                  .replace("%friendid%", coopFriend.accountId)
      });
    }
  }

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

// DEFAULT: long getFriendCount();

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

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

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

  sharingAction = this._WebDetective
                      .getString(CLASS_SHORT_NAME,
                                 "shareAction_directMessage", "")
                      .replace("%friendid%", coopFriend.accountId)
                      .replace("%message%", encodeURIComponent(message.body));

  this._logger.debug(".getSharingAction(): " + sharingAction);
  return sharingAction;
};

// AString getPostLinkAction(in nsITransferable aTransferable);
flockTwitterAccount.prototype.getPostLinkAction =
function fta_getPostLinkAction(aTransferable) {
  var postLinkAction = "";
  var url = "";

  if (aTransferable) {
    // Something was dropped onto the "Post Link" button: get the URL from the
    // transferable
    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);

    url = message.body;
  } else {
    // The "Post Link" button was clicked: get the current tab's URL
    var win = CC["@mozilla.org/appshell/window-mediator;1"]
              .getService(CI.nsIWindowMediator)
              .getMostRecentWindow("navigator:browser");
    if (win) {
      url = win.gBrowser.currentURI.spec;
    }
  }

  if (url) {
    postLinkAction = this._WebDetective
                         .getString(CLASS_SHORT_NAME,
                                    "shareAction_publicMessage",
                                    "")
                         .replace("%message%", encodeURIComponent(url));
  }

  return postLinkAction;
};


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


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

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

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