basic, primitive, primordial twitter support

This commit is contained in:
Myk Melez 2008-07-23 00:45:57 -07:00
Родитель 74df08b577
Коммит e4219875b3
6 изменённых файлов: 233 добавлений и 342 удалений

Просмотреть файл

@ -4,6 +4,7 @@
-moz-border-radius: 10px;
background-color: -moz-field;
color: -moz-fieldtext;
width: 37em;
}
#title {

Просмотреть файл

@ -27,7 +27,18 @@ function SubscriptionListener(subject, topic, data) {
return;
let statusBox = document.getElementById("statusBox");
let statusText = document.getElementById("statusText");
let statusMessage = document.getElementById("statusMessage");
function setStatus(code, message) {
statusBox.setAttribute("status", code);
while (statusMessage.hasChildNodes())
statusMessage.removeChild(statusMessage.firstChild);
// Append a child node so it wraps if it's too long to fit on one line.
// XXX Is there something we can do so the UI doesn't resize midstream?
statusMessage.appendChild(document.createTextNode(message));
}
let identity = source.name ||
(source.humanURI ? source.humanURI.spec : null) ||
@ -36,31 +47,37 @@ function SubscriptionListener(subject, topic, data) {
switch(topic) {
case "snowl:subscribe:connect:start":
statusBox.setAttribute("status", "active");
statusText.value = "Connecting to " + identity;
setStatus("active", "Connecting to " + identity);
break;
case "snowl:subscribe:connect:end":
if (data < 200 || data > 299) {
statusBox.setAttribute("status", "error");
statusBox.value = "Error connecting to " + identity;
}
else {
// XXX Should we bother setting this when we're going to change it
// to "getting messages" an instant later?
statusBox.setAttribute("status", "complete");
statusBox.value = "Connected to " + identity;
{
let code, message;
if (data < 200 || data > 299) {
code = "error";
message = "Error connecting to " + identity;
if (data == 401) {
message += ": your credentials were not accepted. Please check " +
"your username and password and try again.";
}
}
else {
// Under most circumstances, this message will be replaced immediately
// by the "getting messages" message.
code = "complete";
message = "Connected to " + identity;
}
setStatus(code, message);
}
break;
case "snowl:subscribe:get:start":
statusBox.setAttribute("status", "active");
statusText.value = "Getting messages for " + identity;
setStatus("active", "Getting messages for " + identity);
break;
case "snowl:subscribe:get:progress":
break;
case "snowl:subscribe:get:end":
statusBox.setAttribute("status", "complete");
//statusText.value = "Got messages for " + identity;
statusText.value = "You have subscribed to " + identity;
setStatus("complete", "You have subscribed to " + identity);
break;
}
}
@ -152,17 +169,18 @@ let Subscriber = {
},
subscribeTwitter: function() {
let machineURI = URI.get("https://twitter.com");
let humanURI = URI.get("http://twitter.com/home");
let twitter = new SnowlTwitter(null, "Twitter", machineURI, humanURI);
let credentials = {
username: document.getElementById("twitterUsername").value,
password: document.getElementById("twitterPassword").value,
remember: document.getElementById("twitterRemember").checked
};
let username = document.getElementById("twitterUsername").value;
let password = document.getElementById("twitterPassword").value;
let twitter = new SnowlTwitter();
twitter.verify(username, password, function(response) { alert("twitter verify callback: " + response) });
// FIXME: call this "source" instead of "feed".
this.feed = twitter;
//{"authorized":true}
//Could not authenticate you.
twitter.subscribe(credentials);
},
//**************************************************************************//

Просмотреть файл

@ -75,7 +75,7 @@
</row>
<row align="center">
<box/>
<checkbox id="rememberPassword" label="&rememberPassword.label;"/>
<checkbox id="twitterRemember" label="&rememberPassword.label;"/>
</row>
<row>
<box/>
@ -90,14 +90,15 @@
</tabpanels>
</tabbox>
<vbox>
<hbox id="statusBox" align="center">
<image id="statusIcon"/>
<!-- Give the status text an initial value to reserve space
- in the UI for it (so setting it doesn't resize the UI). -->
<label id="statusText" value=" "/>
</hbox>
</vbox>
<!-- Note: the status box should be align="baseline", but that looks ugly.
- align="center" looks ugly for multi-line messages, but those are rare,
- so this is the best option (for the moment, anyway). -->
<hbox id="statusBox" align="center">
<image id="statusIcon"/>
<!-- FIXME: figure out how to allocate space in the UI for the message
- such that setting it later doesn't resize the UI. -->
<description id="statusMessage" flex="1"/>
</hbox>
<separator class="thin" orient="horizontal"/>

Просмотреть файл

@ -43,6 +43,8 @@ let SnowlDatastore = {
columns: [
"id INTEGER PRIMARY KEY",
"name TEXT NOT NULL",
// XXX Call these URL instead of URI, since they are unambiguously
// locations, not names (and thus never URNs)?
"machineURI TEXT NOT NULL",
"humanURI TEXT",
"lastRefreshed INTEGER",
@ -86,6 +88,9 @@ let SnowlDatastore = {
]
},
// FIXME: call this messageMetadata, since we have one for people, too
// (and might get one for sources in the future).
// XXX Should we call this "properties"?
metadata: {
type: TABLE_TYPE_FULLTEXT,
columns: [
@ -100,7 +105,12 @@ let SnowlDatastore = {
type: TABLE_TYPE_NORMAL,
columns: [
"id INTEGER PRIMARY KEY",
"name TEXT NOT NULL"
// XXX Should we store this info as part of the identity, so that
// a person with multiple identities could retain information from
// all of them and select from it at display time?
"name TEXT NOT NULL",
"homeURL TEXT",
"iconURL TEXT"
]
},
@ -387,6 +397,7 @@ let SnowlDatastore = {
return id;
},
// FIXME: insert the namespace, too, if available.
get _insertAttributeStatement() {
let statement = this.createStatement(
"INSERT INTO attributes (name) VALUES (:name)"

Просмотреть файл

@ -37,11 +37,11 @@ SnowlIdentity.get = function(sourceID, externalID) {
return identity;
};
SnowlIdentity.create = function(sourceID, externalID, name) {
SnowlIdentity.create = function(sourceID, externalID, name, homeURL, iconURL) {
let identity;
let personStatement = SnowlDatastore.createStatement(
"INSERT INTO people (name) VALUES (:name)"
"INSERT INTO people (name, homeURL, iconURL) VALUES (:name, :homeURL, :iconURL)"
);
let identityStatement = SnowlDatastore.createStatement(
@ -51,6 +51,8 @@ SnowlIdentity.create = function(sourceID, externalID, name) {
try {
personStatement.params.name = name;
personStatement.params.homeURL = homeURL;
personStatement.params.iconURL = iconURL;
personStatement.step();
let personID = SnowlDatastore.dbConnection.lastInsertRowID;

Просмотреть файл

@ -19,9 +19,20 @@ Cu.import("resource://snowl/modules/datastore.js");
Cu.import("resource://snowl/modules/source.js");
Cu.import("resource://snowl/modules/identity.js");
function SnowlTwitter(aID, aName, aMachineURI, aHumanURI, aLastRefreshed, aImportance) {
// FIXME: factor this out into a common file.
const PART_TYPE_CONTENT = 1;
const PART_TYPE_SUMMARY = 2;
const NAME = "Twitter";
const MACHINE_URI = URI.get("https://twitter.com");
const HUMAN_URI = URI.get("http://twitter.com/home");
function SnowlTwitter(aID, aLastRefreshed, aImportance) {
// XXX Should we append the username to the NAME const to enable users
// to subscribe to multiple Twitter accounts?
// Call the superclass's constructor to initialize the new instance.
SnowlSource.call(this, aID, aName, aMachineURI, aHumanURI, aLastRefreshed, aImportance);
SnowlSource.call(this, aID, NAME, MACHINE_URI, HUMAN_URI, aLastRefreshed, aImportance);
}
SnowlTwitter.prototype = {
@ -29,11 +40,6 @@ SnowlTwitter.prototype = {
_log: Log4Moz.Service.getLogger("Snowl.Twitter"),
// 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,
// Observer Service
get _obsSvc() {
let obsSvc = Cc["@mozilla.org/observer-service;1"].
@ -42,6 +48,25 @@ SnowlTwitter.prototype = {
return this._obsSvc;
},
//**************************************************************************//
// 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,
// Logins from the login manager that we try in turn until we run out of them
// or one of them works.
// XXX Should we only try the username the user entered when they originally
// subscribed to the source? After all, different usernames could result in
// different content, and it might not be what the user expects.
_logins: null,
_loginIndex: 0,
// nsISupports
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAuthPrompt2]),
@ -54,9 +79,6 @@ SnowlTwitter.prototype = {
// nsIAuthPrompt2
_logins: null,
_loginIndex: 0,
promptAuth: function(channel, level, authInfo) {
// Check saved logins before prompting the user. We get them
// from the login manager and try each in turn until one of them works
@ -93,7 +115,6 @@ SnowlTwitter.prototype = {
let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService(Ci.nsIWindowWatcher);
ww.openWindow(null,
// XXX Should we use commonDialog.xul?
"chrome://snowl/content/login.xul",
null,
"chrome,centerscreen,dialog,modal",
@ -109,63 +130,115 @@ SnowlTwitter.prototype = {
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
},
verify: function(username, password, callback) {
//**************************************************************************//
// Subscription
subscribe: function(credentials) {
Observers.notify(this, "snowl:subscribe:connect:start", null);
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
this._log.info("verify callback: " + callback);
request.QueryInterface(Ci.nsIDOMEventTarget);
let t = this;
request.addEventListener("load", function(evt) { t.onVerifyLoad(evt, callback) }, false);
request.addEventListener("error", function(evt) { t.onVerifyError(evt, callback) }, false);
request.addEventListener("load", function(e) { t.onSubscribeLoad(e) }, false);
request.addEventListener("error", function(e) { t.onSubscribeError(e) }, false);
request.QueryInterface(Ci.nsIXMLHttpRequest);
request.open("GET", this.machineURI.spec + "/account/verify_credentials.json", true);
request.setRequestHeader("Authorization", "Basic " + btoa(username + ":" + password));
// Register a listener for notification callbacks so we handle authentication.
request.channel.notificationCallbacks = this;
// We could just set the Authorization request header here, but then
// we wouldn't get an nsIAuthInformation object through our notification
// callbacks, so we'd have to parse the WWW-Authenticate header ourselves
// to extract the realm to use when saving the credentials to the login
// manager, and WWW-Authenticate header parsing is said to be tricky.
// So instead we define notification callbacks that fill in (and persist)
// an nsIAuthInformation object the first time they are called (subsequent
// attempts fail, though, to avoid an infinite loop with a server that keeps
// rejecting our credentials along with a Mozilla that keeps prompting
// for them).
request.channel.notificationCallbacks = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAuthPrompt2]),
getInterface: function(iid) { return this.QueryInterface(iid) },
_firstAttempt: true,
promptAuth: function(channel, level, authInfo) {
if (!this._firstAttempt) {
if (credentials.remember)
this._authInfo = null;
return false;
}
authInfo.username = credentials.username;
authInfo.password = credentials.password;
if (credentials.remember)
this._authInfo = authInfo;
this._firstAttempt = false;
return true;
},
asyncPromptAuth: function() { throw Cr.NS_ERROR_NOT_IMPLEMENTED }
};
request.send(null);
},
onVerifyLoad: function(event, callback) {
this._log.info("onVerifyLoad callback: " + callback);
onSubscribeLoad: function(event) {
let request = event.target;
// request.responseText should be: {"authorized":true}
this._log.info("onSubscribeLoad: " + request.responseText);
// 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.onVerifyError(event, callback);
this.onSubscribeError(event);
return;
}
// XXX What's the right way to handle this?
if (request.responseText.length == 0) {
this.onVerifyError(event, callback);
this.onSubscribeError(event);
return;
}
callback(request.status + ": " + request.responseText);
return;
Observers.notify(this, "snowl:subscribe:connect:end", request.status);
// _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();
let parser = Cc["@mozilla.org/feed-processor;1"].
createInstance(Ci.nsIFeedProcessor);
parser.listener = { t: this, handleResult: function(r) { this.t.onRefreshResult(r) } };
parser.parseFromString(request.responseText, request.channel.URI);
// Add the source to the database.
// FIXME: factor this out with the identical code in feed.js.
let statement =
SnowlDatastore.createStatement("INSERT INTO sources (name, machineURI, humanURI) " +
"VALUES (:name, :machineURI, :humanURI)");
try {
statement.params.name = this.name;
statement.params.machineURI = this.machineURI.spec;
statement.params.humanURI = this.humanURI.spec;
statement.step();
}
finally {
statement.reset();
}
// Extract the ID of the source from the newly-created database record.
this.id = SnowlDatastore.dbConnection.lastInsertRowID;
// Let observers know about the new source.
this._obsSvc.notifyObservers(null, "sources:changed", null);
this.refresh();
},
onVerifyError: function(event, callback) {
this._log.info("onVerifyError callback: " + callback);
onSubscribeError: function(event) {
let request = event.target;
// request.responseText should be: Could not authenticate you.
this._log.info("onSubscribeError: " + request.responseText);
// Sometimes an attempt to retrieve status text throws NS_ERROR_NOT_AVAILABLE.
let statusText = "";
try {
@ -173,15 +246,18 @@ this._log.info("onVerifyError callback: " + callback);
}
catch(ex) {}
this._log.error("onVerifyError: " + request.status + " (" + statusText + ")");
this._log.error("onSubscribeError: " + request.status + " (" + statusText + ")");
callback(request.responseText);
Observers.notify(this, "snowl:subscribe:connect:end", request.status);
},
//**************************************************************************//
// Refreshment
refresh: function() {
Observers.notify(this, "snowl:subscribe:get:start", null);
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
request.QueryInterface(Ci.nsIDOMEventTarget);
@ -191,11 +267,10 @@ this._log.info("onVerifyError callback: " + callback);
request.QueryInterface(Ci.nsIXMLHttpRequest);
// The feed processor is going to parse the XML, so override the MIME type
// in order to turn off parsing by XMLHttpRequest itself.
request.overrideMimeType("text/plain");
request.open("GET", this.machineURI.spec, true);
// FIXME: use the count parameter to retrieve more messages at once.
// FIXME: use the since or since_id parameter to retrieve only new messages.
// http://groups.google.com/group/twitter-development-talk/web/api-documentation
request.open("GET", this.machineURI.spec + "/statuses/friends_timeline.json", true);
// Register a listener for notification callbacks so we handle authentication.
request.channel.notificationCallbacks = this;
@ -203,19 +278,19 @@ this._log.info("onVerifyError callback: " + callback);
request.send(null);
},
onRefreshLoad: function(aEvent) {
let request = aEvent.target;
onRefreshLoad: function(event) {
let request = event.target;
// 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.onRefreshError(aEvent);
this.onRefreshError(event);
return;
}
// XXX What's the right way to handle this?
if (request.responseText.length == 0) {
this.onRefreshError(aEvent);
this.onRefreshError(event);
return;
}
@ -225,14 +300,11 @@ this._log.info("onVerifyError callback: " + callback);
if (this._authInfo)
this._saveLogin();
let parser = Cc["@mozilla.org/feed-processor;1"].
createInstance(Ci.nsIFeedProcessor);
parser.listener = { t: this, handleResult: function(r) { this.t.onRefreshResult(r) } };
parser.parseFromString(request.responseText, request.channel.URI);
this._processRefresh(request.responseText);
},
onRefreshError: function(aEvent) {
let request = aEvent.target;
onRefreshError: function(event) {
let request = event.target;
// Sometimes an attempt to retrieve status text throws NS_ERROR_NOT_AVAILABLE
let statusText = "";
@ -244,51 +316,37 @@ this._log.info("onVerifyError callback: " + callback);
this._log.error("onRefreshError: " + request.status + " (" + statusText + ")");
},
onRefreshResult: function(aResult) {
Observers.notify(this, "snowl:subscribe:get:start", null);
// Now that we know we successfully downloaded the feed and obtained
_processRefresh: function(responseText) {
// Now that we know we successfully downloaded the source and obtained
// a result from it, update the "last refreshed" timestamp.
this.lastRefreshed = new Date();
let feed = aResult.doc.QueryInterface(Components.interfaces.nsIFeed);
var JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
let messages = JSON.decode(responseText);
let currentMessageIDs = [];
let currentMessages = [];
let messagesChanged = false;
SnowlDatastore.dbConnection.beginTransaction();
try {
for (let i = 0; i < feed.items.length; i++) {
let entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry);
//entry.QueryInterface(Ci.nsIFeedContainer);
// Figure out the ID for the entry, then check if the entry has already
// been retrieved. If we can't figure out the entry's ID, then we skip
// the entry, since its ID is the only way for us to know whether or not
// it has already been retrieved.
let externalID;
try {
externalID = entry.id || this._generateID(entry);
}
catch(ex) {
this._log.warn("couldn't retrieve a message: " + ex);
continue;
}
for each (let message in messages) {
let externalID = message.id;
let internalID = this._getInternalIDForExternalID(externalID);
if (internalID)
if (internalID) {
currentMessages.push(internalID);
continue;
}
messagesChanged = true;
this._log.info(this.name + " adding message " + externalID);
internalID = this._addMessage(feed, entry, externalID);
currentMessageIDs.push(internalID);
internalID = this._addMessage(message);
currentMessages.push(internalID);
}
// Update the current flag.
// XXX Should this affect whether or not messages have changed?
SnowlDatastore.dbConnection.executeSimpleSQL("UPDATE messages SET current = 0 WHERE sourceID = " + this.id);
SnowlDatastore.dbConnection.executeSimpleSQL("UPDATE messages SET current = 1 WHERE id IN (" + currentMessageIDs.join(", ") + ")");
SnowlDatastore.dbConnection.executeSimpleSQL("UPDATE messages SET current = 1 WHERE id IN (" + currentMessages.join(", ") + ")");
SnowlDatastore.dbConnection.commitTransaction();
}
@ -303,138 +361,42 @@ this._log.info("onVerifyError callback: " + callback);
Observers.notify(this, "snowl:subscribe:get:end", null);
},
/**
* Add a message to the datastore for the given feed entry.
*
* @param aFeed {nsIFeed} the feed
* @param aEntry {nsIFeedEntry} the entry
* @param aExternalID {string} the external ID of the entry
*/
_addMessage: function(aFeed, aEntry, aExternalID) {
let authorID = null;
let authors = (aEntry.authors.length > 0) ? aEntry.authors
: (aFeed.authors.length > 0) ? aFeed.authors
: null;
if (authors && authors.length > 0) {
let author = authors.queryElementAt(0, Ci.nsIFeedPerson);
// The external ID for an author is her email address, if provided
// (many feeds don't); otherwise it's her name. For the name, on the
// other hand, we use the name, if provided, but fall back to the
// email address if a name is not provided (which it probably was).
let externalID = author.email || author.name;
let name = author.name || author.email;
_addMessage: function(message) {
// Get an existing identity or create a new one. Creating an identity
// automatically creates a person record with the provided name.
let identity = SnowlIdentity.get(this.id, message.user.id) ||
SnowlIdentity.create(this.id,
message.user.id,
message.user.screen_name,
message.user.url,
message.user.profile_image_url);
// FIXME: update the identity record with the latest info about the person.
//identity.updateProperties(this.machineURI, message.user);
let authorID = identity.personID;
// Get an existing identity or create a new one. Creating an identity
// automatically creates a person record with the provided name.
identity = SnowlIdentity.get(this.id, externalID) ||
SnowlIdentity.create(this.id, externalID, name);
authorID = identity.personID;
}
let timestamp = new Date(message.created_at);
// Pick a timestamp, which is one of (by priority, high to low):
// 1. when the entry was last updated;
// 2. when the entry was published;
// 3. the Dublin Core timestamp associated with the entry;
// XXX Should we separately record when we added the entry so that the user
// can sort in the "order received" and view "when received" separately from
// "when published/updated"?
let timestamp = aEntry.updated ? new Date(aEntry.updated) :
aEntry.published ? new Date(aEntry.published) :
ISO8601DateUtils.parse(aEntry.get("dc:date"));
let messageID = this.addSimpleMessage(this.id, message.id, null, authorID,
timestamp, null);
// FIXME: handle titles that contain markup or are missing.
let messageID = this.addSimpleMessage(this.id, aExternalID,
aEntry.title.text, authorID,
timestamp, aEntry.link);
// Add parts
if (aEntry.content) {
this.addPart(messageID, PART_TYPE_CONTENT, aEntry.content.text,
(aEntry.content.base ? aEntry.content.base.spec : null),
aEntry.content.lang, mediaTypes[aEntry.content.type]);
}
if (aEntry.summary) {
this.addPart(messageID, PART_TYPE_SUMMARY, aEntry.summary.text,
(aEntry.summary.base ? aEntry.summary.base.spec : null),
aEntry.summary.lang, mediaTypes[aEntry.summary.type]);
}
this.addPart(messageID, PART_TYPE_CONTENT, message.text, null, null, "text/plain");
// Add metadata.
let fields = aEntry.QueryInterface(Ci.nsIFeedContainer).
fields.QueryInterface(Ci.nsIPropertyBag).enumerator;
while (fields.hasMoreElements()) {
let field = fields.getNext().QueryInterface(Ci.nsIProperty);
for (let [name, value] in Iterator(message)) {
// Ignore properties we have already handled specially.
// XXX Should we add them anyway, which is redundant info but lets others
// (who don't know about our special treatment) access them?
if (["user", "created_at", "text"].indexOf(name) != -1)
continue;
// FIXME: create people records for these.
if (field.name == "authors") {
let values = field.value.QueryInterface(Ci.nsIArray).enumerate();
while (values.hasMoreElements()) {
let value = values.getNext().QueryInterface(Ci.nsIFeedPerson);
// FIXME: store people records in a separate table with individual
// columns for each person attribute (i.e. name, email, url)?
this._addMetadatum(messageID,
"atom:author",
value.name && value.email ? value.name + "<" + value.email + ">"
: value.name ? value.name : value.email);
}
}
// FIXME: populate a "recipient" field with in_reply_to_user_id.
else if (field.name == "links") {
let values = field.value.QueryInterface(Ci.nsIArray).enumerate();
while (values.hasMoreElements()) {
let value = values.getNext().QueryInterface(Ci.nsIPropertyBag2);
// FIXME: store link records in a separate table with individual
// colums for each link attribute (i.e. href, type, rel, title)?
this._addMetadatum(messageID,
"atom:link_" + value.get("rel"),
value.get("href"));
}
}
// For some reason, the values of certain simple fields (like RSS2 guid)
// are property bags containing the value instead of the value itself.
// For those, we need to unwrap the extra layer. This strange behavior
// has been filed as bug 427907.
else if (typeof field.value == "object") {
if (field.value instanceof Ci.nsIPropertyBag2) {
let value = field.value.QueryInterface(Ci.nsIPropertyBag2).get(field.name);
this._addMetadatum(messageID, field.name, value);
}
else if (field.value instanceof Ci.nsIArray) {
let values = field.value.QueryInterface(Ci.nsIArray).enumerate();
while (values.hasMoreElements()) {
// FIXME: values might not always have this interface.
let value = values.getNext().QueryInterface(Ci.nsIPropertyBag2);
this._addMetadatum(messageID, field.name, value.get(field.name));
}
}
}
else
this._addMetadatum(messageID, field.name, field.value);
this._addMetadatum(messageID, name, value);
}
return messageID;
},
/**
* Given an entry, generate an ID for it based on a hash of its link,
* published, and title attributes. Useful for uniquely identifying entries
* that don't provide their own IDs.
*
* @param entry {nsIFeedEntry} the entry for which to generate an ID
* @returns {string} an ID for the entry
*/
_generateID: function(entry) {
let hasher = Cc["@mozilla.org/security/hash;1"].
createInstance(Ci.nsICryptoHash);
hasher.init(Ci.nsICryptoHash.SHA1);
let identity = stringToArray(entry.link.spec + entry.published + entry.title.text);
hasher.update(identity, identity.length);
return "urn:" + hasher.finish(true);
},
// FIXME: Make the rest of this stuff be part of a superclass from which
// this class is derived.
@ -449,6 +411,9 @@ this._log.info("onVerifyError callback: " + callback);
* doesn't exist
*/
_getInternalIDForExternalID: function(aExternalID) {
// FIXME: external IDs may be source-specific, as some sources
// (like Twitter) don't use globally-unique IDs (unlike feeds, which
// generally do), so handle non-globally unique IDs correctly.
return SnowlDatastore.selectInternalIDForExternalID(aExternalID);
},
@ -511,114 +476,7 @@ this._log.info("onVerifyError callback: " + callback);
SnowlDatastore.insertMetadatum(aMessageID, attributeID, aValue);
},
subscribe: function(callback) {
Observers.notify(this, "snowl:subscribe:connect:start", null);
this._subscribeCallback = callback;
this._log.info("subscribing to " + this.name + " <" + this.machineURI.spec + ">");
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
request = request.QueryInterface(Ci.nsIDOMEventTarget);
let t = this;
request.addEventListener("load", function(e) { t.onSubscribeLoad(e) }, false);
request.addEventListener("error", function(e) { t.onSubscribeError(e) }, false);
request = request.QueryInterface(Ci.nsIXMLHttpRequest);
// The feed processor is going to parse the XML, so override the MIME type
// in order to turn off parsing by XMLHttpRequest itself.
request.overrideMimeType("text/plain");
request.open("GET", this.machineURI.spec, true);
// Register a listener for notification callbacks so we handle authentication.
request.channel.notificationCallbacks = this;
request.send(null);
},
onSubscribeLoad: function(aEvent) {
let request = aEvent.target;
// 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.onSubscribeError(aEvent);
return;
}
// XXX What's the right way to handle this?
if (request.responseText.length == 0) {
this.onRefreshError(aEvent);
return;
}
Observers.notify(this, "snowl:subscribe:connect:end", request.status);
// _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();
let parser = Cc["@mozilla.org/feed-processor;1"].
createInstance(Ci.nsIFeedProcessor);
parser.listener = { t: this, handleResult: function(r) { this.t.onSubscribeResult(r) } };
parser.parseFromString(request.responseText, request.channel.URI);
},
onSubscribeError: function(aEvent) {
let request = aEvent.target;
this._log.error("onSubscribeError: " + request.status + " (" + request.statusText + ")");
Observers.notify(this, "snowl:subscribe:connect:end", request.status);
if (this._subscribeCallback)
this._subscribeCallback();
},
onSubscribeResult: function(aResult) {
try {
let feed = aResult.doc.QueryInterface(Components.interfaces.nsIFeed);
// Extract the name (if we don't already have one) and human URI from the feed.
if (!this.name)
this.name = feed.title.plainText();
this.humanURI = feed.link;
// Add the source to the database.
let statement =
SnowlDatastore.createStatement("INSERT INTO sources (name, machineURI, humanURI) " +
"VALUES (:name, :machineURI, :humanURI)");
try {
statement.params.name = this.name;
statement.params.machineURI = this.machineURI.spec;
statement.params.humanURI = this.humanURI.spec;
statement.step();
}
finally {
statement.reset();
}
// Extract the ID of the source from the newly-created database record.
this.id = SnowlDatastore.dbConnection.lastInsertRowID;
// Let observers know about the new source.
this._obsSvc.notifyObservers(null, "sources:changed", null);
// Refresh the feed to import all its items.
this.onRefreshResult(aResult);
}
catch(ex) {
dump("error on subscribe result: " + ex + "\n");
}
finally {
if (this._subscribeCallback)
this._subscribeCallback();
}
},
// FIXME: factor this out with the identical function in feed.js.
_saveLogin: function() {
let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);