changeset 14:74f775701774

refresh sources once per hour
author Myk Melez <myk@mozilla.org>
date Tue, 15 Apr 2008 16:21:19 -0700
parents 8c927c7a7bea
children 737eac0e575b
files extension/content/feedclient.js extension/content/snowl.js extension/content/snowl.xul extension/modules/datastore.js extension/modules/feed.js extension/modules/service.js
diffstat 6 files changed, 457 insertions(+), 385 deletions(-) [+]
line wrap: on
line diff
--- a/extension/content/feedclient.js	Mon Apr 14 19:08:58 2008 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,290 +0,0 @@
-// FIXME: rename this file feed-source.js
-
-var SnowlFeedClient = {
-  // XXX Make this take a feed ID once it stores the list of subscribed feeds
-  // in the datastore.
-  refresh: function(aFeedURL) {
-    let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
-
-    request.QueryInterface(Ci.nsIDOMEventTarget);
-    let t = this;
-    request.addEventListener("load", function(aEvent) { t.onLoad(aEvent) }, false);
-    request.addEventListener("error", function(aEvent) { t.onError(aEvent) }, false);
-
-    request.QueryInterface(Ci.nsIXMLHttpRequest);
-    request.open("GET", aFeedURL, true);
-    request.send(null);
-  },
-
-  onLoad: function(aEvent) {
-    let request = aEvent.target;
-
-    if (request.responseText.length > 0) {
-      let parser = Cc["@mozilla.org/feed-processor;1"].
-                   createInstance(Ci.nsIFeedProcessor);
-      parser.listener = new SnowlFeed(request.channel.originalURI);
-      parser.parseFromString(request.responseText, request.channel.URI);
-    }
-  },
-
-  onError: function(aEvent) {
-    // FIXME: figure out what to do here.
-    Log4Moz.Service.getLogger("Snowl.FeedClient").error("loading feed " + aEvent.target.channel.originalURI);
-  }
-};
-
-// XXX: rename this SnowlFeedSource?
-// XXX: make this inherit from a SnowlMessageSource interface?
-
-function SnowlFeed(aID, aURL, aTitle) {
-  this.id = aID;
-  this.url = aURL;
-  this.title = aTitle;
-
-  this._log = Log4Moz.Service.getLogger("Snowl.Feed");
-}
-
-SnowlFeed.prototype = {
-  id: null,
-  url: null,
-  title: null,
-
-  _log: null,
-
-/*
-  _uri: null,
-  get uri() {
-    return this._uri;
-  },
-
-  get id() {
-    var id = Snowl.datastore.selectSourceID(this.uri.spec);
-    if (!id)
-      return null;
-    // We have an ID, and it won't change over the life of this object,
-    // so memoize it for performance.
-    this.__defineGetter__("id", function() { return id });
-    return this.id;
-  },
-*/
-
-  QueryInterface: function(aIID) {
-    if (aIID.equals(Ci.nsIFeedResultListener) ||
-        aIID.equals(Ci.nsISupports))
-      return this;
-
-    throw Cr.NS_ERROR_NO_INTERFACE;
-  },
-
-  // nsIFeedResultListener
-
-  handleResult: function(result) {
-    // Now that we know we successfully downloaded the feed and obtained
-    // a result from it, update the "last refreshed" timestamp.
-try {
-    SnowlService.resetLastRefreshed(this);
-}
-catch(ex) {
-this._log.info(ex);
-}
-    let feed = result.doc.QueryInterface(Components.interfaces.nsIFeed);
-
-    let currentMessages = [];
-
-    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 universalID;
-        try {
-          universalID = entry.id || this.generateID(entry);
-        }
-        catch(ex) {
-          this._log.warn(this.title + " couldn't retrieve a message: " + ex);
-          continue;
-        }
-
-        let internalID = SnowlService.getInternalIDForExternalID(universalID);
-
-        if (internalID)
-          this._log.info(this.title + " has message " + universalID);
-        else {
-          this._log.info(this.title + " adding message " + universalID);
-          internalID = this.addMessage(entry, universalID);
-        }
-
-        currentMessages.push(internalID);
-      }
-
-      // Update the current flag.
-      SnowlDatastore.dbConnection.executeSimpleSQL("UPDATE messages SET current = 0 WHERE sourceID = " + this.id);
-      SnowlDatastore.dbConnection.executeSimpleSQL("UPDATE messages SET current = 1 WHERE sourceID = " + this.id + " AND id IN (" + currentMessages.join(", ") + ")");
-
-      SnowlDatastore.dbConnection.commitTransaction();
-    }
-    catch(ex) {
-      SnowlDatastore.dbConnection.rollbackTransaction();
-      throw ex;
-    }
-  },
-
-  // nsIFeedTextConstruct::type to MIME media type mappings.
-  contentTypes: { html: "text/html", xhtml: "application/xhtml+xml", text: "text/plain" },
-
-  getNewMessages: function() {
-    let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
-
-    request.QueryInterface(Ci.nsIDOMEventTarget);
-    // FIXME: just pass "this" and make this implement nsIDOMEventListener.
-    let t = this;
-    request.addEventListener("load", function(aEvent) { t.onLoad(aEvent) }, false);
-    request.addEventListener("error", function(aEvent) { t.onError(aEvent) }, false);
-
-    request.QueryInterface(Ci.nsIXMLHttpRequest);
-    request.open("GET", this.url, true);
-    request.send(null);
-  },
-
-  onLoad: function(aEvent) {
-    let request = aEvent.target;
-
-    if (request.responseText.length > 0) {
-      let parser = Cc["@mozilla.org/feed-processor;1"].
-                   createInstance(Ci.nsIFeedProcessor);
-      parser.listener = this;
-      parser.parseFromString(request.responseText, request.channel.URI);
-    }
-  },
-
-  onError: function(aEvent) {
-    // FIXME: figure out what to do here.
-    this._log.error("loading feed " + aEvent.target.channel.originalURI);
-  },
-
-  /**
-   * Add a message to the datastore for the given feed entry.
-   *
-   * @param aEntry        {nsIFeedEntry}  the feed entry
-   * @param aUniversalID  {string}        the universal ID of the feed entry
-   */
-  addMessage: function(aEntry, aUniversalID) {
-    // Combine the first author's name and email address into a single string
-    // that we'll use as the author of the message.
-    let author = null;
-    if (aEntry.authors.length > 0) {
-      let firstAuthor = aEntry.authors.queryElementAt(0, Ci.nsIFeedPerson);
-      let name = firstAuthor.name;
-      let email = firstAuthor.email;
-      if (name) {
-        author = name;
-        if (email)
-          author += " <" + email + ">";
-      }
-      else if (email)
-        author = email;
-    }
-
-    // Convert the publication date/time string into a JavaScript Date object.
-    let timestamp = aEntry.published ? new Date(aEntry.published) : null;
-
-    // Convert the content type specified by nsIFeedTextConstruct, which is
-    // either "html", "xhtml", or "text", into an Internet media type.
-    let contentType = aEntry.content ? this.contentTypes[aEntry.content.type] : null;
-    let contentText = aEntry.content ? aEntry.content.text : null;
-    let messageID = SnowlService.addSimpleMessage(this.id, aUniversalID,
-                                                  aEntry.title.text, author,
-                                                  timestamp, aEntry.link,
-                                                  contentText, contentType);
-
-    // Add metadata.
-    let fields = aEntry.QueryInterface(Ci.nsIFeedContainer).
-                 fields.QueryInterface(Ci.nsIPropertyBag).enumerator;
-    while (fields.hasMoreElements()) {
-      let field = fields.getNext().QueryInterface(Ci.nsIProperty);
-
-      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)?
-          SnowlService.addMetadatum(messageID,
-                                    "atom:author",
-                                    value.name && value.email ? value.name + "<" + value.email + ">"
-                                                              : value.name ? value.name : value.email);
-        }
-      }
-
-      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)?
-          SnowlService.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);
-          SnowlService.addMetadatum(messageID, field.name, value);
-        }
-        else if (field.value instanceof Ci.nsIArray) {
-          let values = field.value.QueryInterface(Ci.nsIArray).enumerate();
-          while (values.hasMoreElements()) {
-            let value = values.getNext().QueryInterface(Ci.nsIPropertyBag2);
-            SnowlService.addMetadatum(messageID, field.name, value.get(field.name));
-          }
-        }
-      }
-
-      else
-        SnowlService.addMetadatum(messageID, field.name, field.value);
-    }
-
-    return messageID;
-  },
-
-  /**
-   * Convert a string to an array of character codes.
-   *
-   * @param string {string} the string to convert
-   * @returns {array} the array of character codes
-   */
-  stringToArray: function(string) {
-    var array = [];
-    for (let i = 0; i < string.length; i++)
-      array.push(string.charCodeAt(i));
-    return array;
-  },
-
-  /**
-   * 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 = this.stringToArray(entry.link.spec + entry.published + entry.title.text);
-    hasher.update(identity, identity.length);
-    return "urn:" + hasher.finish(true);
-  }
-};
--- a/extension/content/snowl.js	Mon Apr 14 19:08:58 2008 -0700
+++ b/extension/content/snowl.js	Tue Apr 15 16:21:19 2008 -0700
@@ -16,23 +16,6 @@
   _initModules: function() {
   },
 
-  getNewMessages: function() {
-    let sources = this.getSources();
-    for each (let source in sources)
-      source.getNewMessages();
-  },
-
-  getSources: function() {
-    let sources = [];
-
-    let rows = SnowlDatastore.selectSources();
-
-    for each (let row in rows)
-      sources.push(new SnowlFeed(row.id, row.url, row.title));
-
-    return sources;
-  },
-
   toggleView: function() {
     let container = document.getElementById("snowlViewContainer");
     let splitter = document.getElementById("snowlViewSplitter");
--- a/extension/content/snowl.xul	Mon Apr 14 19:08:58 2008 -0700
+++ b/extension/content/snowl.xul	Tue Apr 15 16:21:19 2008 -0700
@@ -8,7 +8,6 @@
 
   <!-- <script type="application/x-javascript" src="datastore.js"/> -->
   <script type="application/x-javascript" src="init.js"/>
-  <script type="application/x-javascript" src="feedclient.js"/>
   <script type="application/x-javascript" src="view.js"/>
   <script type="application/x-javascript" src="snowl.js"/>
 
@@ -35,7 +34,7 @@
   <vbox id="appcontent">
     <vbox id="snowlViewContainer" insertbefore="content">
       <toolbar id="snowlViewToolbar" pack="end">
-        <button oncommand="Snowl.getNewMessages()" label="G"/>
+        <button oncommand="SnowlService.refreshAllSources()" label="G"/>
         <button oncommand="SnowlView.switchPosition()" label="S"/>
         <textbox id="snowlFilterTextbox" type="timed" timeout="200" oncommand="SnowlView.onFilter()"/>
       </toolbar>
--- a/extension/modules/datastore.js	Mon Apr 14 19:08:58 2008 -0700
+++ b/extension/modules/datastore.js	Tue Apr 15 16:21:19 2008 -0700
@@ -248,7 +248,7 @@
 
   get _selectSourcesStatement() {
     let statement = this.createStatement(
-      "SELECT id, url, title FROM sources"
+      "SELECT id, url, title, lastRefreshed FROM sources"
     );
     this.__defineGetter__("_selectSourcesStatement", function() { return statement });
     return this._selectSourcesStatement;
@@ -256,10 +256,15 @@
 
   selectSources: function() {
     let sources = [];
+
     try {
       while (this._selectSourcesStatement.step()) {
         let row = this._selectSourcesStatement.row;
-        sources.push({ id: row.id, url: row.url, title: row.title });
+        sources.push({ id: row.id,
+                       url: row.url,
+                       title: row.title,
+                       lastRefreshed: new Date(row.lastRefreshed)
+                     });
       }
     }
     finally {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extension/modules/feed.js	Tue Apr 15 16:21:19 2008 -0700
@@ -0,0 +1,378 @@
+dump("begin importing feed.js\n");
+
+EXPORTED_SYMBOLS = ["SnowlFeed"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://snowl/log4moz.js");
+Cu.import("resource://snowl/datastore.js");
+
+var SnowlFeedClient = {
+  // XXX Make this take a feed ID once it stores the list of subscribed feeds
+  // in the datastore.
+  refresh: function(aFeedURL) {
+    let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
+
+    request.QueryInterface(Ci.nsIDOMEventTarget);
+    let t = this;
+    request.addEventListener("load", function(aEvent) { t.onLoad(aEvent) }, false);
+    request.addEventListener("error", function(aEvent) { t.onError(aEvent) }, false);
+
+    request.QueryInterface(Ci.nsIXMLHttpRequest);
+    request.open("GET", aFeedURL, true);
+    request.send(null);
+  },
+
+  onLoad: function(aEvent) {
+    let request = aEvent.target;
+
+    if (request.responseText.length > 0) {
+      let parser = Cc["@mozilla.org/feed-processor;1"].
+                   createInstance(Ci.nsIFeedProcessor);
+      parser.listener = new SnowlFeed(request.channel.originalURI);
+      parser.parseFromString(request.responseText, request.channel.URI);
+    }
+  },
+
+  onError: function(aEvent) {
+    // FIXME: figure out what to do here.
+    Log4Moz.Service.getLogger("Snowl.FeedClient").error("loading feed " + aEvent.target.channel.originalURI);
+  }
+};
+
+// XXX: rename this SnowlFeedSource?
+// XXX: make this inherit from a SnowlMessageSource interface?
+
+function SnowlFeed(aID, aURL, aTitle) {
+  this.id = aID;
+  this.url = aURL;
+  this.title = aTitle;
+
+  this._log = Log4Moz.Service.getLogger("Snowl.Feed");
+}
+
+SnowlFeed.prototype = {
+  id: null,
+  url: null,
+  title: null,
+
+  _log: null,
+
+/*
+  _uri: null,
+  get uri() {
+    return this._uri;
+  },
+
+  get id() {
+    var id = Snowl.datastore.selectSourceID(this.uri.spec);
+    if (!id)
+      return null;
+    // We have an ID, and it won't change over the life of this object,
+    // so memoize it for performance.
+    this.__defineGetter__("id", function() { return id });
+    return this.id;
+  },
+*/
+
+  QueryInterface: function(aIID) {
+    if (aIID.equals(Ci.nsIFeedResultListener) ||
+        aIID.equals(Ci.nsISupports))
+      return this;
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+  // nsIFeedResultListener
+
+  handleResult: function(result) {
+    // Now that we know we successfully downloaded the feed and obtained
+    // a result from it, update the "last refreshed" timestamp.
+    this.resetLastRefreshed(this);
+
+    let feed = result.doc.QueryInterface(Components.interfaces.nsIFeed);
+
+    let currentMessages = [];
+
+    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 universalID;
+        try {
+          universalID = entry.id || this.generateID(entry);
+        }
+        catch(ex) {
+          this._log.warn(this.title + " couldn't retrieve a message: " + ex);
+          continue;
+        }
+
+        let internalID = this.getInternalIDForExternalID(universalID);
+
+        if (internalID)
+          this._log.info(this.title + " has message " + universalID);
+        else {
+          this._log.info(this.title + " adding message " + universalID);
+          internalID = this.addMessage(entry, universalID);
+        }
+
+        currentMessages.push(internalID);
+      }
+
+      // Update the current flag.
+      SnowlDatastore.dbConnection.executeSimpleSQL("UPDATE messages SET current = 0 WHERE sourceID = " + this.id);
+      SnowlDatastore.dbConnection.executeSimpleSQL("UPDATE messages SET current = 1 WHERE sourceID = " + this.id + " AND id IN (" + currentMessages.join(", ") + ")");
+
+      SnowlDatastore.dbConnection.commitTransaction();
+    }
+    catch(ex) {
+      SnowlDatastore.dbConnection.rollbackTransaction();
+      throw ex;
+    }
+  },
+
+  // nsIFeedTextConstruct::type to MIME media type mappings.
+  contentTypes: { html: "text/html", xhtml: "application/xhtml+xml", text: "text/plain" },
+
+  getNewMessages: function() {
+    let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
+
+    request.QueryInterface(Ci.nsIDOMEventTarget);
+    // FIXME: just pass "this" and make this implement nsIDOMEventListener.
+    let t = this;
+    request.addEventListener("load", function(aEvent) { t.onLoad(aEvent) }, false);
+    request.addEventListener("error", function(aEvent) { t.onError(aEvent) }, false);
+
+    request.QueryInterface(Ci.nsIXMLHttpRequest);
+    request.open("GET", this.url, true);
+    request.send(null);
+  },
+
+  onLoad: function(aEvent) {
+    let request = aEvent.target;
+
+    if (request.responseText.length > 0) {
+      let parser = Cc["@mozilla.org/feed-processor;1"].
+                   createInstance(Ci.nsIFeedProcessor);
+      parser.listener = this;
+      parser.parseFromString(request.responseText, request.channel.URI);
+    }
+  },
+
+  onError: function(aEvent) {
+    // FIXME: figure out what to do here.
+    this._log.error("loading feed " + aEvent.target.channel.originalURI);
+  },
+
+  /**
+   * Add a message to the datastore for the given feed entry.
+   *
+   * @param aEntry        {nsIFeedEntry}  the feed entry
+   * @param aUniversalID  {string}        the universal ID of the feed entry
+   */
+  addMessage: function(aEntry, aUniversalID) {
+    // Combine the first author's name and email address into a single string
+    // that we'll use as the author of the message.
+    let author = null;
+    if (aEntry.authors.length > 0) {
+      let firstAuthor = aEntry.authors.queryElementAt(0, Ci.nsIFeedPerson);
+      let name = firstAuthor.name;
+      let email = firstAuthor.email;
+      if (name) {
+        author = name;
+        if (email)
+          author += " <" + email + ">";
+      }
+      else if (email)
+        author = email;
+    }
+
+    // Convert the publication date/time string into a JavaScript Date object.
+    let timestamp = aEntry.published ? new Date(aEntry.published) : null;
+
+    // Convert the content type specified by nsIFeedTextConstruct, which is
+    // either "html", "xhtml", or "text", into an Internet media type.
+    let contentType = aEntry.content ? this.contentTypes[aEntry.content.type] : null;
+    let contentText = aEntry.content ? aEntry.content.text : null;
+    let messageID = this.addSimpleMessage(this.id, aUniversalID,
+                                                  aEntry.title.text, author,
+                                                  timestamp, aEntry.link,
+                                                  contentText, contentType);
+
+    // Add metadata.
+    let fields = aEntry.QueryInterface(Ci.nsIFeedContainer).
+                 fields.QueryInterface(Ci.nsIPropertyBag).enumerator;
+    while (fields.hasMoreElements()) {
+      let field = fields.getNext().QueryInterface(Ci.nsIProperty);
+
+      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);
+        }
+      }
+
+      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()) {
+            let value = values.getNext().QueryInterface(Ci.nsIPropertyBag2);
+            this.addMetadatum(messageID, field.name, value.get(field.name));
+          }
+        }
+      }
+
+      else
+        this.addMetadatum(messageID, field.name, field.value);
+    }
+
+    return messageID;
+  },
+
+  /**
+   * Convert a string to an array of character codes.
+   *
+   * @param string {string} the string to convert
+   * @returns {array} the array of character codes
+   */
+  stringToArray: function(string) {
+    var array = [];
+    for (let i = 0; i < string.length; i++)
+      array.push(string.charCodeAt(i));
+    return array;
+  },
+
+  /**
+   * 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 = this.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.
+
+  /**
+   * Get the internal ID of the message with the given external ID.
+   *
+   * @param    aExternalID {string}
+   *           the external ID of the message
+   *
+   * @returns  {number}
+   *           the internal ID of the message, or undefined if the message
+   *           doesn't exist
+   */
+  getInternalIDForExternalID: function(aExternalID) {
+    return SnowlDatastore.selectInternalIDForExternalID(aExternalID);
+  },
+
+  /**
+   * Add a message with a single part to the datastore.
+   *
+   * @param aSourceID    {integer} the record ID of the message source
+   * @param aUniversalID {string}  the universal ID of the message
+   * @param aSubject     {string}  the title of the message
+   * @param aAuthor      {string}  the author of the message
+   * @param aTimestamp   {Date}    the date/time at which the message was sent
+   * @param aLink        {nsIURI}  a link to the content of the message,
+   *                               if the content is hosted on a server
+   * @param aContent     {string}  the content of the message, if the content
+   *                               is included with the message
+   * @param aContentType {string}  the media type of the content of the message,
+   *                               if the content is included with the message
+   *
+   * FIXME: allow callers to pass a set of arbitrary metadata name/value pairs
+   * that get written to the attributes table.
+   * 
+   * @returns {integer} the internal ID of the newly-created message
+   */
+  addSimpleMessage: function(aSourceID, aUniversalID, aSubject, aAuthor,
+                             aTimestamp, aLink, aContent, aContentType) {
+    // Convert the timestamp to milliseconds-since-epoch, which is how we store
+    // it in the datastore.
+    let timestamp = aTimestamp ? aTimestamp.getTime() : null;
+
+    // Convert the link to its string spec, which is how we store it
+    // in the datastore.
+    let link = aLink ? aLink.spec : null;
+
+    let messageID =
+      SnowlDatastore.insertMessage(aSourceID, aUniversalID, aSubject, aAuthor,
+                                   timestamp, link);
+
+    if (aContent)
+      SnowlDatastore.insertPart(messageID, aContent, aContentType);
+
+    return messageID;
+  },
+
+  addMetadatum: function(aMessageID, aAttributeName, aValue) {
+    // FIXME: speed this up by caching the list of known attributes.
+    let attributeID = SnowlDatastore.selectAttributeID(aAttributeName)
+                      || SnowlDatastore.insertAttribute(aAttributeName);
+    SnowlDatastore.insertMetadatum(aMessageID, attributeID, aValue);
+  },
+
+  /**
+   * Reset the last refreshed time for the given source to the current time.
+   *
+   * XXX should this be setLastRefreshed and take a time parameter
+   * to set the last refreshed time to?
+   *
+   * aSource {SnowlMessageSource} the source for which to set the time
+   */
+  resetLastRefreshed: function() {
+    let stmt = SnowlDatastore.createStatement("UPDATE sources SET lastRefreshed = :lastRefreshed WHERE id = :id");
+    stmt.params.lastRefreshed = new Date().getTime();
+    stmt.params.id = this.id;
+    stmt.execute();
+  }
+
+};
+
+dump("end importing feed.js\n");
--- a/extension/modules/service.js	Mon Apr 14 19:08:58 2008 -0700
+++ b/extension/modules/service.js	Tue Apr 15 16:21:19 2008 -0700
@@ -1,3 +1,5 @@
+dump("begin service importation\n");
+
 const EXPORTED_SYMBOLS = ["SnowlService"];
 
 const Cc = Components.classes;
@@ -8,6 +10,7 @@
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://snowl/log4moz.js");
 Cu.import("resource://snowl/datastore.js");
+Cu.import("resource://snowl/feed.js");
 
 const PERMS_FILE      = 0644;
 const PERMS_DIRECTORY = 0755;
@@ -17,6 +20,12 @@
 const SNOWL_HANDLER_URI = "chrome://snowl/content/subscribe.xul?feed=%s";
 const SNOWL_HANDLER_TITLE = "Snowl";
 
+// How often to refresh sources, in milliseconds.
+const REFRESH_INTERVAL = 60 * 60 * 1000; // 60 minutes
+ 
+// How often to check if sources need refreshing, in milliseconds.
+const REFRESH_CHECK_INTERVAL = 60 * 1000; // 60 seconds
+
 let SnowlService = {
   // Preferences Service
   get _prefSvc() {
@@ -48,6 +57,23 @@
   _init: function() {
     this._initLogs();
     this._registerFeedHandler();
+    this._initTimer();
+
+    // FIXME: refresh stale sources on startup in a way that doesn't hang
+    // the UI thread.
+    //this._refreshStaleSources();
+  },
+
+  _timer: null,
+  _initTimer: function() {
+    this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    let callback = {
+      _svc: this,
+      notify: function(aTimer) { this._svc._refreshStaleSources() }
+    };
+    this._timer.initWithCallback(callback,
+                                 REFRESH_CHECK_INTERVAL,
+                                 Ci.nsITimer.TYPE_REPEATING_SLACK);
   },
 
   _initLogs: function() {
@@ -136,19 +162,48 @@
     }
   },
 
-  /**
-   * Reset the last refreshed time for the given source to the current time.
-   *
-   * XXX should this be setLastRefreshed and take a time parameter
-   * to set the last refreshed time to?
-   *
-   * aSource {SnowlMessageSource} the source for which to set the time
-   */
-  resetLastRefreshed: function(aSource) {
-    let stmt = SnowlDatastore.createStatement("UPDATE sources SET lastRefreshed = :lastRefreshed WHERE id = :id");
-    stmt.params.lastRefreshed = new Date().getTime();
-    stmt.params.id = aSource.id;
-    stmt.execute();
+  _refreshStaleSources: function() {
+    this._log.info("refreshing stale sources");
+
+    // XXX Should SnowlDatastore::selectSources return SnowlSource objects,
+    // of which SnowlFeed is a subclass?  Or perhaps selectSources should simply
+    // return a database cursor, and SnowlService::getSources should return
+    // SnowlSource objects?
+    let allSources = SnowlDatastore.selectSources();
+    let now = new Date();
+    let staleSources = [];
+    for each (let source in allSources)
+{
+this._log.info("checking source: " + source.id);
+      if (now - source.lastRefreshed > REFRESH_INTERVAL)
+{
+this._log.info("source: " + source.id + " is stale");
+        staleSources.push(source);
+      }
+    }
+    this._refreshSources(staleSources);
+  },
+
+  refreshAllSources: function() {
+    let sources = SnowlDatastore.selectSources();
+    this._refreshSources(sources);
+  },
+
+  _refreshSources: function(aSources) {
+    for each (let source in aSources) {
+      let feed = new SnowlFeed(source.id, source.url, source.title);
+      feed.getNewMessages();
+
+      // We reset the last refreshed timestamp here even though the refresh
+      // is asynchronous, so we don't yet know whether it has succeeded.
+      // The upside of this approach is that we don't keep trying to refresh
+      // a source that isn't responding, but the downside is that it takes
+      // a long time for us to refresh a source that is only down for a short
+      // period of time.  We should instead keep trying when a source fails,
+      // but with a progressively longer interval (up to the standard one).
+      // FIXME: implement the approach described above.
+      feed.resetLastRefreshed();
+    }
   },
 
   /**
@@ -160,68 +215,10 @@
    */
   hasMessage: function(aUniversalID) {
     return SnowlDatastore.selectHasMessage(aUniversalID);
-  },
-
-  /**
-   * Get the internal ID of the message with the given external ID.
-   *
-   * @param    aExternalID {string}
-   *           the external ID of the message
-   *
-   * @returns  {number}
-   *           the internal ID of the message, or undefined if the message
-   *           doesn't exist
-   */
-  getInternalIDForExternalID: function(aExternalID) {
-    return SnowlDatastore.selectInternalIDForExternalID(aExternalID);
-  },
+  }
 
-  /**
-   * Add a message with a single part to the datastore.
-   *
-   * @param aSourceID    {integer} the record ID of the message source
-   * @param aUniversalID {string}  the universal ID of the message
-   * @param aSubject     {string}  the title of the message
-   * @param aAuthor      {string}  the author of the message
-   * @param aTimestamp   {Date}    the date/time at which the message was sent
-   * @param aLink        {nsIURI}  a link to the content of the message,
-   *                               if the content is hosted on a server
-   * @param aContent     {string}  the content of the message, if the content
-   *                               is included with the message
-   * @param aContentType {string}  the media type of the content of the message,
-   *                               if the content is included with the message
-   *
-   * FIXME: allow callers to pass a set of arbitrary metadata name/value pairs
-   * that get written to the attributes table.
-   * 
-   * @returns {integer} the internal ID of the newly-created message
-   */
-  addSimpleMessage: function(aSourceID, aUniversalID, aSubject, aAuthor,
-                             aTimestamp, aLink, aContent, aContentType) {
-    // Convert the timestamp to milliseconds-since-epoch, which is how we store
-    // it in the datastore.
-    let timestamp = aTimestamp ? aTimestamp.getTime() : null;
-
-    // Convert the link to its string spec, which is how we store it
-    // in the datastore.
-    let link = aLink ? aLink.spec : null;
-
-    let messageID =
-      SnowlDatastore.insertMessage(aSourceID, aUniversalID, aSubject, aAuthor,
-                                   timestamp, link);
-
-    if (aContent)
-      SnowlDatastore.insertPart(messageID, aContent, aContentType);
-
-    return messageID;
-  },
-
-  addMetadatum: function(aMessageID, aAttributeName, aValue) {
-    // FIXME: speed this up by caching the list of known attributes.
-    let attributeID = SnowlDatastore.selectAttributeID(aAttributeName)
-                      || SnowlDatastore.insertAttribute(aAttributeName);
-    SnowlDatastore.insertMetadatum(aMessageID, attributeID, aValue);
-  }
 };
 
 SnowlService._init();
+
+dump("end service importation\n");