зеркало из https://github.com/mozilla/snowl.git
625 строки
23 KiB
JavaScript
625 строки
23 KiB
JavaScript
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* 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.
|
|
*
|
|
* The Original Code is Snowl.
|
|
*
|
|
* The Initial Developer of the Original Code is Mozilla.
|
|
* Portions created by the Initial Developer are Copyright (C) 2008
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Myk Melez <myk@mozilla.org>
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
let EXPORTED_SYMBOLS = ["SnowlTwitter"];
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cr = Components.results;
|
|
const Cu = Components.utils;
|
|
|
|
// modules that come with Firefox
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/ISO8601DateUtils.jsm");
|
|
|
|
// modules that are generic
|
|
Cu.import("resource://snowl/modules/Mixins.js");
|
|
Cu.import("resource://snowl/modules/Observers.js");
|
|
Cu.import("resource://snowl/modules/request.js");
|
|
Cu.import("resource://snowl/modules/URI.js");
|
|
|
|
// modules that are Snowl-specific
|
|
Cu.import("resource://snowl/modules/constants.js");
|
|
Cu.import("resource://snowl/modules/datastore.js");
|
|
Cu.import("resource://snowl/modules/source.js");
|
|
Cu.import("resource://snowl/modules/target.js");
|
|
Cu.import("resource://snowl/modules/identity.js");
|
|
Cu.import("resource://snowl/modules/message.js");
|
|
Cu.import("resource://snowl/modules/utils.js");
|
|
Cu.import("resource://snowl/modules/service.js");
|
|
|
|
// FIXME: make strands.js into a module.
|
|
let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
|
|
loader.loadSubScript("chrome://snowl/content/strands.js");
|
|
|
|
const TYPE = "SnowlTwitter";
|
|
const NAME = "Twitter";
|
|
const MACHINE_URI = URI.get("https://twitter.com");
|
|
// XXX Should this be simply http://twitter.com ?
|
|
const HUMAN_URI = URI.get("http://twitter.com/home");
|
|
|
|
/**
|
|
* The HTTP authentication realm under which to save credentials via the login
|
|
* manager. We save them under a realm whose name we define instead of the one
|
|
* that Twitter provides (currently "Twitter API") because we set our own
|
|
* Authorization header, and that happens before we get a response from Twitter,
|
|
* so we can't depend on the value Twitter sets when it responds, because
|
|
* we don't know it yet.
|
|
*
|
|
* Using our own realm also has the beneficial side effect that users browsing
|
|
* their saved credentials in preferences will see our realm next to
|
|
* the credentials that were saved by Snowl, which seems a better explanation
|
|
* of where the credentials come from than the one Twitter provides.
|
|
*
|
|
* The reason we set our own Authorization header is that Necko assumes users
|
|
* will only be logged into a single account for a given authentication realm
|
|
* in any given session, so it caches credentials and reuses them for all
|
|
* requests to the same realm. Setting the Authorization header ourselves
|
|
* ensures that we determine the credentials being used for our requests,
|
|
* which is necessary to support multiple Twitter accounts.
|
|
*
|
|
* We could have theoretically worked around the problem by putting the username
|
|
* into the URL (i.e. https://username@twitter.com...) and falling back on our
|
|
* notification callback to get the saved credentials, by which point we'd know
|
|
* the authentication realm.
|
|
*
|
|
* But experimentation showed that only worked for serialized requests;
|
|
* concurrent requests (like when we refresh two Twitter accounts at the same
|
|
* time asynchronously) cause Necko to again use the same credentials for both
|
|
* refreshes, even though we've specified different usernames in the URLs
|
|
* for those refreshes.
|
|
*
|
|
* And it had the side-effect that Necko stopped saving credentials at all
|
|
* after the requests completed, so a user with a single account who didn't save
|
|
* their credentials was prompted to enter them every time we refreshed.
|
|
*
|
|
* FIXME: file a bug on this bad behavior of Necko during concurrent requests.
|
|
*
|
|
* We could have also worked around the problem by also injecting the password
|
|
* into the request URLs (i.e. https://username:password@twitter.com...),
|
|
* but then we'd be putting passwords into URLs, which is considered harmful
|
|
* because URLs leak into visible places (like the Error Console).
|
|
*/
|
|
const AUTH_REALM = "Snowl";
|
|
|
|
// This module is based on the API documented at http://apiwiki.twitter.com/.
|
|
|
|
// FIXME: make the constructor accept credentials instead of passing them
|
|
// to the subscribe function.
|
|
|
|
function SnowlTwitter(aID, aName, aMachineURI, aHumanURI, aUsername,
|
|
aLastRefreshed, aImportance, aPlaceID, aAttributes) {
|
|
// Use the given machine URI, if available. We use this in unit tests
|
|
// to point the account to a test server rather than the actual Twitter
|
|
// servers.
|
|
let machineURI = aMachineURI || MACHINE_URI;
|
|
|
|
// FIXME: figure out a better solution than hanging the first mixed in init()
|
|
// method on this object's prototype but calling the second one directly
|
|
// because it didn't actually get mixed in because it already existed!
|
|
this.init(aID, aName, machineURI, HUMAN_URI, aUsername,
|
|
aLastRefreshed, aImportance, aPlaceID, aAttributes);
|
|
SnowlTarget.prototype.init.call(this);
|
|
}
|
|
|
|
SnowlTwitter.prototype = {
|
|
// The constructor property is defined automatically, but we destroy it
|
|
// when we redefine the prototype, so we redefine it here in case we ever
|
|
// need to check it to find out what kind of object an instance is.
|
|
constructor: SnowlTwitter,
|
|
|
|
get _logName() {
|
|
return "Snowl.Twitter." + this.username;
|
|
},
|
|
|
|
|
|
//**************************************************************************//
|
|
// Abstract Class Composition Declarations
|
|
|
|
_classes: [SnowlSource, SnowlTarget],
|
|
|
|
implements: function(cls) {
|
|
return (this._classes.indexOf(cls) != -1);
|
|
},
|
|
|
|
|
|
//**************************************************************************//
|
|
// SnowlSource
|
|
|
|
// The default attributes for this source type. Documentation in source.js.
|
|
attributes: {
|
|
refresh: {
|
|
interval: 1000 * 60 * 3
|
|
},
|
|
retention: {
|
|
deleteDays: 10,
|
|
deleteNumber: 500
|
|
}
|
|
},
|
|
|
|
|
|
//**************************************************************************//
|
|
// SnowlTarget
|
|
|
|
maxMessageLength: 140,
|
|
|
|
// send is defined elsewhere.
|
|
|
|
|
|
//**************************************************************************//
|
|
// Notification Callbacks for Authentication
|
|
|
|
// FIXME: factor this out with the equivalent code in feed.js.
|
|
|
|
// If we prompt the user to authenticate, and the user asks us to remember
|
|
// their password, we store the nsIAuthInformation in this property until
|
|
// the request succeeds, at which point we store it with the login manager.
|
|
_authInfo: null,
|
|
|
|
get _loginManager() {
|
|
let loginManager = Cc["@mozilla.org/login-manager;1"].
|
|
getService(Ci.nsILoginManager);
|
|
this.__defineGetter__("_loginManager", function() loginManager);
|
|
return this._loginManager;
|
|
},
|
|
|
|
/**
|
|
* The saved credentials for this Twitter account, if any.
|
|
* FIXME: we memoize this and never refresh it, which won't do once we have
|
|
* long-lived account objects, so don't memoize this at all (attach it to
|
|
* its request and kill it once the request is done) or invalidate it when
|
|
* the set of credentials changes.
|
|
*/
|
|
get _savedLogin() {
|
|
// XXX Should we be using channel.URI.prePath instead of
|
|
// this.machineURI.prePath in case the old URI redirects us to a new one
|
|
// at a different hostname?
|
|
return this._loginManager.
|
|
findLogins({}, this.machineURI.prePath, null, AUTH_REALM).
|
|
filter(function(login) login.username == this.username, this)
|
|
[0];
|
|
},
|
|
|
|
// nsISupports
|
|
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAuthPrompt2]),
|
|
|
|
// nsIInterfaceRequestor
|
|
|
|
getInterface: function(iid) {
|
|
return this.QueryInterface(iid);
|
|
},
|
|
|
|
// nsIAuthPrompt2
|
|
|
|
promptAuth: function(channel, level, authInfo) {
|
|
this._log.debug("promptAuth: this.name = " + this.name + "; this.username = " + this.username);
|
|
this._log.debug("promptAuth: this.name = " + this.name + "; authInfo.realm = " + authInfo.realm);
|
|
|
|
let args = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
|
|
args.AppendElement({ wrappedJSObject: this });
|
|
args.AppendElement(authInfo);
|
|
|
|
// |result| is how the dialog passes information back to us. It sets two
|
|
// properties on the object:
|
|
// |proceed|, which we return from this function, and which determines
|
|
// whether or not authentication can proceed using the value(s) entered
|
|
// by the user;
|
|
// |remember|, which determines whether or not we save the user's login
|
|
// with the login manager once the request succeeds.
|
|
let result = {};
|
|
args.AppendElement({ wrappedJSObject: result });
|
|
|
|
let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService(Ci.nsIWindowWatcher);
|
|
ww.openWindow(null,
|
|
"chrome://snowl/content/login.xul",
|
|
null,
|
|
"chrome,centerscreen,dialog,modal",
|
|
args);
|
|
|
|
if (result.remember)
|
|
this._authInfo = authInfo;
|
|
else
|
|
this._authInfo = null;
|
|
|
|
return result.proceed;
|
|
},
|
|
|
|
asyncPromptAuth: function(channel, callback, context, level, authInfo) {
|
|
this._log.debug("asyncPromptAuth: this.name = " + this.name + "; this.username = " + this.username);
|
|
this._log.debug("asyncPromptAuth: this.name = " + this.name + "; authInfo.realm = " + authInfo.realm);
|
|
|
|
let args = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
|
|
args.AppendElement({ wrappedJSObject: this });
|
|
args.AppendElement(authInfo);
|
|
|
|
let t = this;
|
|
let okCallback = function(remember) {
|
|
if (remember)
|
|
t._authInfo = authInfo;
|
|
else
|
|
t._authInfo = null;
|
|
callback.onAuthAvailable(context, authInfo);
|
|
}
|
|
args.AppendElement({ wrappedJSObject: okCallback });
|
|
|
|
let cancelCallback = function() {
|
|
t._authInfo = null;
|
|
callback.onAuthCancelled(context, true);
|
|
}
|
|
args.AppendElement({ wrappedJSObject: cancelCallback });
|
|
|
|
let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService(Ci.nsIWindowWatcher);
|
|
let win = ww.openWindow(null,
|
|
"chrome://snowl/content/login-async.xul",
|
|
null,
|
|
"chrome,centerscreen,dialog",
|
|
args);
|
|
|
|
return {
|
|
cancel: function() {
|
|
win.QueryInterface(Ci.nsIDOMWindowInternal).close();
|
|
callback.onAuthCancelled(context, false);
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
//**************************************************************************//
|
|
// Refreshment
|
|
|
|
get _stmtGetMaxExternalID() {
|
|
let statement = SnowlDatastore.createStatement(
|
|
"SELECT MAX(externalID) AS maxID FROM messages WHERE sourceID = :sourceID"
|
|
);
|
|
this.__defineGetter__("_stmtGetMaxExternalID", function() statement);
|
|
return this._stmtGetMaxExternalID;
|
|
},
|
|
|
|
/**
|
|
* Get the maximum external ID of the messages received from this source.
|
|
* Newer messages always have larger integer IDs, so we can query for only
|
|
* new messages by specifying since_id=[max ID] in the refresh request.
|
|
*
|
|
* @returns {Number}
|
|
* the maximum external ID, if any
|
|
*/
|
|
_getMaxExternalID: function() {
|
|
let maxID = null;
|
|
|
|
try {
|
|
this._stmtGetMaxExternalID.params.sourceID = this.id;
|
|
if (this._stmtGetMaxExternalID.step())
|
|
maxID = this._stmtGetMaxExternalID.row["maxID"];
|
|
}
|
|
finally {
|
|
this._stmtGetMaxExternalID.reset();
|
|
}
|
|
|
|
return maxID;
|
|
},
|
|
|
|
/**
|
|
* Refresh the feed, retrieving the latest information in it.
|
|
*
|
|
* @param time {Date} [optional]
|
|
* when the refresh occurs; determines the received time of new
|
|
* messages; we let the caller specify this so a caller refreshing
|
|
* multiple feeds can give their messages the same received time
|
|
*/
|
|
refresh: function(time) {
|
|
if (typeof time == "undefined" || time == null)
|
|
time = new Date();
|
|
|
|
Observers.notify("snowl:refresh:connect:start", this);
|
|
|
|
// URL parameters that modify the return value of the request.
|
|
let params = [];
|
|
|
|
// Retrieve up to 200 messages, the maximum we're allowed to retrieve.
|
|
params.push("count=200");
|
|
|
|
// Retrieve only messages newer than the most recent one already retrieved.
|
|
let (maxID = this._getMaxExternalID()) {
|
|
if (maxID)
|
|
params.push("since_id=" + maxID);
|
|
}
|
|
|
|
let url = this.machineURI.spec.replace(/^(https?:\/\/)/, "$1" + this.username + "@") +
|
|
"statuses/friends_timeline.json?" + params.join("&");
|
|
this._log.debug("refresh: this.name = " + this.name + "; url = " + url);
|
|
|
|
let requestHeaders = {};
|
|
|
|
// If the login manager has saved credentials for this account, provide them
|
|
// to the server. Otherwise, no worries, Necko will automatically call our
|
|
// notification callback, which will prompt the user to enter their credentials.
|
|
if (this._savedLogin) {
|
|
this._log.info("setting Authorization header with username " + this.username);
|
|
let credentials = btoa(this.username + ":" + this._savedLogin.password);
|
|
requestHeaders.Authorization = "Basic " + credentials;
|
|
}
|
|
|
|
let request = new Request({
|
|
url: url,
|
|
notificationCallbacks: this,
|
|
requestHeaders: requestHeaders
|
|
});
|
|
|
|
Observers.notify("snowl:refresh:connect:end", this, request.status);
|
|
|
|
this.attributes.refresh["code"] = request.status;
|
|
this.attributes.refresh["text"] = request.status + " (" + request.statusText + ")";
|
|
if (request.status < 200 || request.status > 299 || request.responseText.length == 0) {
|
|
this.onRefreshError();
|
|
return;
|
|
}
|
|
|
|
// _authInfo only gets set if we prompted the user to authenticate
|
|
// and the user checked the "remember password" box. Since we're here,
|
|
// it means the request succeeded, so we save the login.
|
|
if (this._authInfo) {
|
|
this._saveLogin(this._authInfo);
|
|
this._authInfo = null;
|
|
}
|
|
|
|
Observers.notify("snowl:refresh:get:start", this);
|
|
|
|
let items = JSON.parse(request.responseText);
|
|
this.messages = this._processItems(items, time);
|
|
|
|
this.lastRefreshed = time;
|
|
|
|
Observers.notify("snowl:refresh:get:end", this);
|
|
},
|
|
|
|
|
|
//**************************************************************************//
|
|
// Processing
|
|
|
|
/**
|
|
* Process an array of items (from the server) into an array of messages.
|
|
*
|
|
* @param items {Array} the items to process
|
|
* @param received {Date} when the items were received
|
|
*/
|
|
_processItems: function(items, received) {
|
|
this._log.trace("processing items");
|
|
|
|
let messages = [];
|
|
|
|
for each (let item in items) {
|
|
try {
|
|
let message = this._processItem(item, received);
|
|
messages.push(message);
|
|
}
|
|
catch(ex) {
|
|
this._log.error("couldn't process item " + item.id + ": " + ex);
|
|
}
|
|
}
|
|
|
|
return messages;
|
|
},
|
|
|
|
_processItem: function(item, received) {
|
|
this._log.trace("processing item " + item.id);
|
|
|
|
let message = new SnowlMessage();
|
|
|
|
message.source = this;
|
|
message.externalID = item.id;
|
|
// Place tweet text in subject; avoids requiring parts.content or parts.summary
|
|
// as excerpt, used when no subject; non lazy retrieval of parts from the db
|
|
// in List view is not a perfomance enhancer for large lists.
|
|
message.subject = item.text;
|
|
message.timestamp = new Date(item.created_at);
|
|
message.received = received || new Date();
|
|
message.author = new SnowlIdentity(null,
|
|
this.id,
|
|
item.user.id);
|
|
message.author.person = new SnowlPerson(null,
|
|
item.user.screen_name,
|
|
null,
|
|
item.user.url,
|
|
item.user.profile_image_url);
|
|
|
|
message.content =
|
|
new SnowlMessagePart({
|
|
partType: PART_TYPE_CONTENT,
|
|
content: item.text,
|
|
mediaType: "text/plain"
|
|
});
|
|
|
|
// Add headers.
|
|
message.headers = {};
|
|
for (let [name, value] in Iterator(item)) {
|
|
// FIXME: populate a "recipient" field with in_reply_to_user_id.
|
|
if (name == "user") {
|
|
for (let [uname, uvalue] in Iterator(value))
|
|
message.headers["user:" + uname] = uvalue;
|
|
}
|
|
else
|
|
message.headers[name] = value;
|
|
}
|
|
|
|
return message;
|
|
},
|
|
|
|
// XXX Perhaps factor this out with the identical function in feed.js,
|
|
// although this function supports multiple accounts with the same server
|
|
// and doesn't allow the user to change their username, so maybe that's
|
|
// not possible (or perhaps we can reconcile those differences).
|
|
_saveLogin: function(authInfo) {
|
|
// Create a new login with the auth information we obtained from the user.
|
|
let LoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
|
|
Ci.nsILoginInfo,
|
|
"init");
|
|
// XXX Should we be using channel.URI.prePath in case the old URI
|
|
// redirects us to a new one at a different hostname?
|
|
let newLogin = new LoginInfo(this.machineURI.prePath,
|
|
null,
|
|
AUTH_REALM,
|
|
authInfo.username,
|
|
authInfo.password,
|
|
"",
|
|
"");
|
|
|
|
// If there are credentials with the same username, we replace them.
|
|
// Otherwise, we add the new credentials.
|
|
if (this._savedLogin)
|
|
this._loginManager.modifyLogin(this._savedLogin, newLogin);
|
|
else
|
|
this._loginManager.addLogin(newLogin);
|
|
},
|
|
|
|
|
|
//**************************************************************************//
|
|
// Sending
|
|
|
|
_successCallback: null,
|
|
_errorCallback: null,
|
|
|
|
send: function(content, successCallback, errorCallback) {
|
|
Observers.notify("snowl:send:start", this);
|
|
|
|
let data = "status=" + encodeURIComponent(content) + "&source=snowl";
|
|
// + "&in_reply_to_status_id=" + encodeURIComponent(inReplyToID);
|
|
|
|
this._successCallback = successCallback;
|
|
this._errorCallback = errorCallback;
|
|
|
|
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
|
|
|
|
// FIXME: make a TwitterRequest (or plain Request) object that caches
|
|
// references to the callbacks and the SnowlTwitter instance so we don't
|
|
// have to cache them in the object itself, which could cause problems
|
|
// if we were to persist the instance and send multiple messages through
|
|
// it simultaneously.
|
|
|
|
request.QueryInterface(Ci.nsIDOMEventTarget);
|
|
let (t = this) {
|
|
request.addEventListener("load", function(e) { t.onSendLoad(e) }, false);
|
|
request.addEventListener("error", function(e) { t.onSendError(e) }, false);
|
|
}
|
|
|
|
request.QueryInterface(Ci.nsIXMLHttpRequest);
|
|
request.open("POST", this.machineURI.spec.replace(/^(https?:\/\/)/, "$1" + this.username + "@") +
|
|
"statuses/update.json", true);
|
|
// If the login manager has saved credentials for this account, provide them
|
|
// to the server. Otherwise, no worries, Necko will automatically call our
|
|
// notification callback, which will prompt the user to enter their credentials.
|
|
if (this._savedLogin) {
|
|
let credentials = btoa(this.username + ":" + this._savedLogin.password);
|
|
request.setRequestHeader("Authorization", "Basic " + credentials);
|
|
}
|
|
request.channel.notificationCallbacks = this;
|
|
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
|
request.send(data);
|
|
},
|
|
|
|
onSendLoad: function(event) {
|
|
let request = event.target;
|
|
|
|
// FIXME: the next three chunks of code are the same for multiple
|
|
// load handlers; find some way to factor them out.
|
|
|
|
// If the request failed, let the error handler handle it.
|
|
// XXX Do we need this? Don't such failures call the error handler directly?
|
|
if (request.status < 200 || request.status > 299) {
|
|
this.onSendError(event);
|
|
return;
|
|
}
|
|
|
|
// If the response is empty, assume failure.
|
|
// XXX What's the right way to handle this?
|
|
if (request.responseText.length == 0) {
|
|
this.onSendError(event);
|
|
return;
|
|
}
|
|
|
|
this._log.info("onSendLoad: " + request.responseText);
|
|
|
|
// _authInfo only gets set if we prompted the user to authenticate
|
|
// and the user checked the "remember password" box. Since we're here,
|
|
// it means the request succeeded, so we save the login.
|
|
if (this._authInfo)
|
|
this._saveLogin(this._authInfo);
|
|
|
|
this._processSend(request.responseText);
|
|
|
|
if (this._successCallback)
|
|
this._successCallback();
|
|
|
|
this._resetSend();
|
|
},
|
|
|
|
onSendError: function(event) {
|
|
let request = event.target;
|
|
|
|
// Sometimes an attempt to retrieve status text throws NS_ERROR_NOT_AVAILABLE
|
|
let statusText = "";
|
|
try { statusText = request.statusText } catch(ex) {}
|
|
|
|
this._log.error("onSendError: " + request.status + " (" + statusText + ")");
|
|
|
|
if (this._errorCallback)
|
|
this._errorCallback();
|
|
|
|
this._resetSend();
|
|
},
|
|
|
|
_processSend: function(responseText) {
|
|
let JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
|
|
let item = JSON.decode(responseText);
|
|
let message = this._processItem(item);
|
|
message.persist();
|
|
},
|
|
|
|
_resetSend: function() {
|
|
this._successCallback = null;
|
|
this._errorCallback = null;
|
|
this._authInfo = null;
|
|
}
|
|
};
|
|
|
|
Mixins.meld(SnowlSource.prototype.attributes, true, false, SnowlService._log).
|
|
into(SnowlTwitter.prototype.attributes);
|
|
Mixins.mix(SnowlSource).into(SnowlTwitter);
|
|
Mixins.mix(SnowlSource.prototype).into(SnowlTwitter.prototype);
|
|
Mixins.mix(SnowlTarget).into(SnowlTwitter);
|
|
Mixins.mix(SnowlTarget.prototype).into(SnowlTwitter.prototype);
|
|
SnowlService.addAccountType(SnowlTwitter, SnowlTwitter.prototype.attributes);
|