Mercurial > snowl
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");