Mercurial > snowl
annotate extension/modules/feed.js @ 75:877a7694445f
update the sources table schema, changing the title column to name and differentiating between machine-processable and human-readable URIs
author | Myk Melez <myk@mozilla.org> |
---|---|
date | Wed, 14 May 2008 22:33:20 -0700 |
parents | a3811857c5dc |
children | 1cbd4c5a511b |
rev | line source |
---|---|
14 | 1 EXPORTED_SYMBOLS = ["SnowlFeed"]; |
2 | |
3 const Cc = Components.classes; | |
4 const Ci = Components.interfaces; | |
5 const Cr = Components.results; | |
6 const Cu = Components.utils; | |
7 | |
45
a3811857c5dc
register the resource alias to the top-level directory rather than the modules directory for consistency with resource://gre/modules/ URLs and so we can use it to load resources from elsewhere in the extension later; this means converting chrome://snowl/module.js URLs into chrome://snowl/modules/module.js URLs
Myk Melez <myk@mozilla.org>
parents:
21
diff
changeset
|
8 Cu.import("resource://snowl/modules/log4moz.js"); |
a3811857c5dc
register the resource alias to the top-level directory rather than the modules directory for consistency with resource://gre/modules/ URLs and so we can use it to load resources from elsewhere in the extension later; this means converting chrome://snowl/module.js URLs into chrome://snowl/modules/module.js URLs
Myk Melez <myk@mozilla.org>
parents:
21
diff
changeset
|
9 Cu.import("resource://snowl/modules/datastore.js"); |
14 | 10 |
11 var SnowlFeedClient = { | |
12 // XXX Make this take a feed ID once it stores the list of subscribed feeds | |
13 // in the datastore. | |
14 refresh: function(aFeedURL) { | |
15 let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); | |
16 | |
17 request.QueryInterface(Ci.nsIDOMEventTarget); | |
18 let t = this; | |
19 request.addEventListener("load", function(aEvent) { t.onLoad(aEvent) }, false); | |
20 request.addEventListener("error", function(aEvent) { t.onError(aEvent) }, false); | |
21 | |
22 request.QueryInterface(Ci.nsIXMLHttpRequest); | |
23 request.open("GET", aFeedURL, true); | |
24 request.send(null); | |
25 }, | |
26 | |
27 onLoad: function(aEvent) { | |
28 let request = aEvent.target; | |
29 | |
30 if (request.responseText.length > 0) { | |
31 let parser = Cc["@mozilla.org/feed-processor;1"]. | |
32 createInstance(Ci.nsIFeedProcessor); | |
33 parser.listener = new SnowlFeed(request.channel.originalURI); | |
34 parser.parseFromString(request.responseText, request.channel.URI); | |
35 } | |
36 }, | |
37 | |
38 onError: function(aEvent) { | |
39 // FIXME: figure out what to do here. | |
21 | 40 Log4Moz.Service.getLogger("Snowl.FeedClient").error("loading feed " + aEvent.target.channel.originalURI.spec); |
14 | 41 } |
42 }; | |
43 | |
44 function SnowlFeed(aID, aURL, aTitle) { | |
45 this.id = aID; | |
46 this.url = aURL; | |
47 this.title = aTitle; | |
48 | |
49 this._log = Log4Moz.Service.getLogger("Snowl.Feed"); | |
50 } | |
51 | |
75
877a7694445f
update the sources table schema, changing the title column to name and differentiating between machine-processable and human-readable URIs
Myk Melez <myk@mozilla.org>
parents:
45
diff
changeset
|
52 // FIXME: make this a subclass of SnowlSource. |
877a7694445f
update the sources table schema, changing the title column to name and differentiating between machine-processable and human-readable URIs
Myk Melez <myk@mozilla.org>
parents:
45
diff
changeset
|
53 |
14 | 54 SnowlFeed.prototype = { |
55 id: null, | |
56 url: null, | |
57 title: null, | |
58 | |
59 _log: null, | |
60 | |
61 QueryInterface: function(aIID) { | |
62 if (aIID.equals(Ci.nsIFeedResultListener) || | |
63 aIID.equals(Ci.nsISupports)) | |
64 return this; | |
65 | |
66 throw Cr.NS_ERROR_NO_INTERFACE; | |
67 }, | |
68 | |
69 // nsIFeedResultListener | |
70 | |
71 handleResult: function(result) { | |
72 // Now that we know we successfully downloaded the feed and obtained | |
73 // a result from it, update the "last refreshed" timestamp. | |
74 this.resetLastRefreshed(this); | |
75 | |
76 let feed = result.doc.QueryInterface(Components.interfaces.nsIFeed); | |
77 | |
78 let currentMessages = []; | |
79 | |
80 SnowlDatastore.dbConnection.beginTransaction(); | |
81 try { | |
82 for (let i = 0; i < feed.items.length; i++) { | |
83 let entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry); | |
84 //entry.QueryInterface(Ci.nsIFeedContainer); | |
85 | |
86 // Figure out the ID for the entry, then check if the entry has already | |
87 // been retrieved. If we can't figure out the entry's ID, then we skip | |
88 // the entry, since its ID is the only way for us to know whether or not | |
89 // it has already been retrieved. | |
90 let universalID; | |
91 try { | |
92 universalID = entry.id || this.generateID(entry); | |
93 } | |
94 catch(ex) { | |
95 this._log.warn(this.title + " couldn't retrieve a message: " + ex); | |
96 continue; | |
97 } | |
98 | |
99 let internalID = this.getInternalIDForExternalID(universalID); | |
100 | |
101 if (internalID) | |
102 this._log.info(this.title + " has message " + universalID); | |
103 else { | |
104 this._log.info(this.title + " adding message " + universalID); | |
105 internalID = this.addMessage(entry, universalID); | |
106 } | |
107 | |
108 currentMessages.push(internalID); | |
109 } | |
110 | |
111 // Update the current flag. | |
112 SnowlDatastore.dbConnection.executeSimpleSQL("UPDATE messages SET current = 0 WHERE sourceID = " + this.id); | |
113 SnowlDatastore.dbConnection.executeSimpleSQL("UPDATE messages SET current = 1 WHERE sourceID = " + this.id + " AND id IN (" + currentMessages.join(", ") + ")"); | |
114 | |
115 SnowlDatastore.dbConnection.commitTransaction(); | |
116 } | |
117 catch(ex) { | |
118 SnowlDatastore.dbConnection.rollbackTransaction(); | |
119 throw ex; | |
120 } | |
121 }, | |
122 | |
123 // nsIFeedTextConstruct::type to MIME media type mappings. | |
124 contentTypes: { html: "text/html", xhtml: "application/xhtml+xml", text: "text/plain" }, | |
125 | |
126 getNewMessages: function() { | |
127 let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); | |
128 | |
129 request.QueryInterface(Ci.nsIDOMEventTarget); | |
130 // FIXME: just pass "this" and make this implement nsIDOMEventListener. | |
131 let t = this; | |
132 request.addEventListener("load", function(aEvent) { t.onLoad(aEvent) }, false); | |
133 request.addEventListener("error", function(aEvent) { t.onError(aEvent) }, false); | |
134 | |
135 request.QueryInterface(Ci.nsIXMLHttpRequest); | |
75
877a7694445f
update the sources table schema, changing the title column to name and differentiating between machine-processable and human-readable URIs
Myk Melez <myk@mozilla.org>
parents:
45
diff
changeset
|
136 dump("about to getNewMessages for " + this.url + "\n"); |
14 | 137 request.open("GET", this.url, true); |
138 request.send(null); | |
139 }, | |
140 | |
141 onLoad: function(aEvent) { | |
142 let request = aEvent.target; | |
143 | |
144 if (request.responseText.length > 0) { | |
145 let parser = Cc["@mozilla.org/feed-processor;1"]. | |
146 createInstance(Ci.nsIFeedProcessor); | |
147 parser.listener = this; | |
148 parser.parseFromString(request.responseText, request.channel.URI); | |
149 } | |
150 }, | |
151 | |
152 onError: function(aEvent) { | |
153 // FIXME: figure out what to do here. | |
154 this._log.error("loading feed " + aEvent.target.channel.originalURI); | |
155 }, | |
156 | |
157 /** | |
158 * Add a message to the datastore for the given feed entry. | |
159 * | |
160 * @param aEntry {nsIFeedEntry} the feed entry | |
161 * @param aUniversalID {string} the universal ID of the feed entry | |
162 */ | |
163 addMessage: function(aEntry, aUniversalID) { | |
164 // Combine the first author's name and email address into a single string | |
165 // that we'll use as the author of the message. | |
166 let author = null; | |
167 if (aEntry.authors.length > 0) { | |
168 let firstAuthor = aEntry.authors.queryElementAt(0, Ci.nsIFeedPerson); | |
169 let name = firstAuthor.name; | |
170 let email = firstAuthor.email; | |
171 if (name) { | |
172 author = name; | |
173 if (email) | |
174 author += " <" + email + ">"; | |
175 } | |
176 else if (email) | |
177 author = email; | |
178 } | |
179 | |
180 // Convert the publication date/time string into a JavaScript Date object. | |
181 let timestamp = aEntry.published ? new Date(aEntry.published) : null; | |
182 | |
183 // Convert the content type specified by nsIFeedTextConstruct, which is | |
184 // either "html", "xhtml", or "text", into an Internet media type. | |
185 let contentType = aEntry.content ? this.contentTypes[aEntry.content.type] : null; | |
186 let contentText = aEntry.content ? aEntry.content.text : null; | |
187 let messageID = this.addSimpleMessage(this.id, aUniversalID, | |
188 aEntry.title.text, author, | |
189 timestamp, aEntry.link, | |
190 contentText, contentType); | |
191 | |
192 // Add metadata. | |
193 let fields = aEntry.QueryInterface(Ci.nsIFeedContainer). | |
194 fields.QueryInterface(Ci.nsIPropertyBag).enumerator; | |
195 while (fields.hasMoreElements()) { | |
196 let field = fields.getNext().QueryInterface(Ci.nsIProperty); | |
197 | |
198 if (field.name == "authors") { | |
199 let values = field.value.QueryInterface(Ci.nsIArray).enumerate(); | |
200 while (values.hasMoreElements()) { | |
201 let value = values.getNext().QueryInterface(Ci.nsIFeedPerson); | |
202 // FIXME: store people records in a separate table with individual | |
203 // columns for each person attribute (i.e. name, email, url)? | |
204 this.addMetadatum(messageID, | |
205 "atom:author", | |
206 value.name && value.email ? value.name + "<" + value.email + ">" | |
207 : value.name ? value.name : value.email); | |
208 } | |
209 } | |
210 | |
211 else if (field.name == "links") { | |
212 let values = field.value.QueryInterface(Ci.nsIArray).enumerate(); | |
213 while (values.hasMoreElements()) { | |
214 let value = values.getNext().QueryInterface(Ci.nsIPropertyBag2); | |
215 // FIXME: store link records in a separate table with individual | |
216 // colums for each link attribute (i.e. href, type, rel, title)? | |
217 this.addMetadatum(messageID, | |
218 "atom:link_" + value.get("rel"), | |
219 value.get("href")); | |
220 } | |
221 } | |
222 | |
223 // For some reason, the values of certain simple fields (like RSS2 guid) | |
224 // are property bags containing the value instead of the value itself. | |
225 // For those, we need to unwrap the extra layer. This strange behavior | |
226 // has been filed as bug 427907. | |
227 else if (typeof field.value == "object") { | |
228 if (field.value instanceof Ci.nsIPropertyBag2) { | |
229 let value = field.value.QueryInterface(Ci.nsIPropertyBag2).get(field.name); | |
230 this.addMetadatum(messageID, field.name, value); | |
231 } | |
232 else if (field.value instanceof Ci.nsIArray) { | |
233 let values = field.value.QueryInterface(Ci.nsIArray).enumerate(); | |
234 while (values.hasMoreElements()) { | |
235 let value = values.getNext().QueryInterface(Ci.nsIPropertyBag2); | |
236 this.addMetadatum(messageID, field.name, value.get(field.name)); | |
237 } | |
238 } | |
239 } | |
240 | |
241 else | |
242 this.addMetadatum(messageID, field.name, field.value); | |
243 } | |
244 | |
245 return messageID; | |
246 }, | |
247 | |
248 /** | |
249 * Convert a string to an array of character codes. | |
250 * | |
251 * @param string {string} the string to convert | |
252 * @returns {array} the array of character codes | |
253 */ | |
254 stringToArray: function(string) { | |
255 var array = []; | |
256 for (let i = 0; i < string.length; i++) | |
257 array.push(string.charCodeAt(i)); | |
258 return array; | |
259 }, | |
260 | |
261 /** | |
262 * Given an entry, generate an ID for it based on a hash of its link, | |
263 * published, and title attributes. Useful for uniquely identifying entries | |
264 * that don't provide their own IDs. | |
265 * | |
266 * @param entry {nsIFeedEntry} the entry for which to generate an ID | |
267 * @returns {string} an ID for the entry | |
268 */ | |
269 generateID: function(entry) { | |
270 let hasher = Cc["@mozilla.org/security/hash;1"]. | |
271 createInstance(Ci.nsICryptoHash); | |
272 hasher.init(Ci.nsICryptoHash.SHA1); | |
273 let identity = this.stringToArray(entry.link.spec + entry.published + entry.title.text); | |
274 hasher.update(identity, identity.length); | |
275 return "urn:" + hasher.finish(true); | |
276 }, | |
277 | |
278 // FIXME: Make the rest of this stuff be part of a superclass from which | |
279 // this class is derived. | |
280 | |
281 /** | |
282 * Get the internal ID of the message with the given external ID. | |
283 * | |
284 * @param aExternalID {string} | |
285 * the external ID of the message | |
286 * | |
287 * @returns {number} | |
288 * the internal ID of the message, or undefined if the message | |
289 * doesn't exist | |
290 */ | |
291 getInternalIDForExternalID: function(aExternalID) { | |
292 return SnowlDatastore.selectInternalIDForExternalID(aExternalID); | |
293 }, | |
294 | |
295 /** | |
296 * Add a message with a single part to the datastore. | |
297 * | |
298 * @param aSourceID {integer} the record ID of the message source | |
299 * @param aUniversalID {string} the universal ID of the message | |
300 * @param aSubject {string} the title of the message | |
301 * @param aAuthor {string} the author of the message | |
302 * @param aTimestamp {Date} the date/time at which the message was sent | |
303 * @param aLink {nsIURI} a link to the content of the message, | |
304 * if the content is hosted on a server | |
305 * @param aContent {string} the content of the message, if the content | |
306 * is included with the message | |
307 * @param aContentType {string} the media type of the content of the message, | |
308 * if the content is included with the message | |
309 * | |
310 * FIXME: allow callers to pass a set of arbitrary metadata name/value pairs | |
311 * that get written to the attributes table. | |
312 * | |
313 * @returns {integer} the internal ID of the newly-created message | |
314 */ | |
315 addSimpleMessage: function(aSourceID, aUniversalID, aSubject, aAuthor, | |
316 aTimestamp, aLink, aContent, aContentType) { | |
317 // Convert the timestamp to milliseconds-since-epoch, which is how we store | |
318 // it in the datastore. | |
319 let timestamp = aTimestamp ? aTimestamp.getTime() : null; | |
320 | |
321 // Convert the link to its string spec, which is how we store it | |
322 // in the datastore. | |
323 let link = aLink ? aLink.spec : null; | |
324 | |
325 let messageID = | |
326 SnowlDatastore.insertMessage(aSourceID, aUniversalID, aSubject, aAuthor, | |
327 timestamp, link); | |
328 | |
329 if (aContent) | |
330 SnowlDatastore.insertPart(messageID, aContent, aContentType); | |
331 | |
332 return messageID; | |
333 }, | |
334 | |
335 addMetadatum: function(aMessageID, aAttributeName, aValue) { | |
336 // FIXME: speed this up by caching the list of known attributes. | |
337 let attributeID = SnowlDatastore.selectAttributeID(aAttributeName) | |
338 || SnowlDatastore.insertAttribute(aAttributeName); | |
339 SnowlDatastore.insertMetadatum(aMessageID, attributeID, aValue); | |
340 }, | |
341 | |
342 /** | |
343 * Reset the last refreshed time for the given source to the current time. | |
344 * | |
345 * XXX should this be setLastRefreshed and take a time parameter | |
346 * to set the last refreshed time to? | |
347 * | |
348 * aSource {SnowlMessageSource} the source for which to set the time | |
349 */ | |
350 resetLastRefreshed: function() { | |
351 let stmt = SnowlDatastore.createStatement("UPDATE sources SET lastRefreshed = :lastRefreshed WHERE id = :id"); | |
352 stmt.params.lastRefreshed = new Date().getTime(); | |
353 stmt.params.id = this.id; | |
354 stmt.execute(); | |
355 } | |
356 | |
357 }; |