Licensing GPLv3
[quicknewsreader] / qml / QuickNewsReader / content / js / GoogleReaderAPI.js
1 /***
2 ** Copyright 2011 - Tommi Laukkanen (www.substanceofcode.com)
3 ** Copyright (C) 2012 Christophe CHAPUIS <chris.chapuis _at_ gmail _dot_ com>
4 **
5 ** This package is free software; you can redistribute it and/or modify
6 ** it under the terms of the GNU General Public License as published by
7 ** the Free Software Foundation; either version 2 of the License, or
8 ** (at your option) any later version.
9 **
10 ** This package is distributed in the hope that it will be useful,
11 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
12 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 ** GNU General Public License for more details.
14 **
15 ** You should have received a copy of the GNU General Public License
16 ** along with this package; if not, write to the Free Software
17 ** Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
18 **
19 ***/
20
21 var sid = "";
22 var sidToken = "";
23
24 // UI components
25 //var waiting;
26 //var done;
27 //var model;
28 //var tagsModel;
29 //var logo;
30 //var error;
31 //var navigation;
32
33 var continuation = "";
34 var actionID = "";
35 var actionFeedUrl = "";
36 var accessToken = "";
37 var action = "";
38 var tags = "";
39
40 var itemsURL = "";
41
42 function doWebRequest(method, url, params, callback) {
43     var doc = new XMLHttpRequest();
44     //console.log(method + " " + url);
45
46     doc.onreadystatechange = function() {
47         if (doc.readyState == XMLHttpRequest.HEADERS_RECEIVED) {
48             var status = doc.status;
49             if(status!=200) {
50                 showError("Google API returned " + status + " " + doc.statusText);
51             }
52         } else if (doc.readyState == XMLHttpRequest.DONE) {
53             var data;
54             var contentType = doc.getResponseHeader("Content-Type");
55             data = doc.responseText;
56             callback(data);
57         }
58     }
59
60     doc.open(method, url);
61     if(sid.length>0) {
62         //console.log("Authorization GoogleLogin auth=" + sid);
63         doc.setRequestHeader("Authorization", "GoogleLogin auth=" + sid);
64         doc.setRequestHeader("Cookie", "SID=" + sidToken);
65     }
66     if(params.length>0) {
67         //console.log("Sending: " + params);
68         doc.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
69         doc.setRequestHeader("Content-Length", String(params.length));
70         doc.send(params);
71     } else {
72         doc.send();
73     }
74 }
75
76 /** Parse parameter from given URL */
77 function parseAuth(data, parameterName) {
78     var parameterIndex = data.indexOf(parameterName + "=");
79     if(parameterIndex<0) {
80         // We didn't find parameter
81         console.log("Didn't find Auth");
82         addError("Didn't find Auth");
83         return "";
84     }
85     var equalIndex = data.indexOf("=", parameterIndex);
86     if(equalIndex<0) {
87         return "";
88     }
89
90     var lineBreakIndex = data.indexOf("\n", equalIndex+1)
91
92     var value = "";
93     value = data.substring(equalIndex+1, lineBreakIndex);
94     return value;
95 }
96
97 function addError(msg) {
98     console.log(msg)
99     /*
100     model.append({
101                  "title": "Error",
102                  "desc": msg,
103                  "author": "",
104                  "published": "",
105                  "more": true,
106                  "source": ""})
107                  */
108 }
109
110 function login(email, password) {
111     try {
112         // waiting.state = "shown";
113         var url = "https://www.google.com/accounts/ClientLogin?Email=" + encodeURIComponent(email) + "&Passwd=" + encodeURIComponent(password) + "&service=reader";
114         doWebRequest("GET", url, "", parseToken);
115     }catch(err) {
116         showError("Error while logging in.");
117     }
118 }
119
120 function showError(msg) {
121     console.log("ERROR: " + msg)
122 //    waiting.state = "hidden";
123 //    error.reason = msg;
124 //    error.state = "shown";
125 }
126
127 function removeLinks(original) {
128     var txt = original;
129     txt = txt.replace(/<a /g, "<span ");
130     txt = txt.replace(/<\/a>/g, "</span>");
131     return txt;
132 }
133
134 function parseToken(data) {
135     sid = parseAuth(data, "Auth");
136     //console.log("Auth=" + sid);
137     sidToken = parseAuth(data, "SID");
138     //console.log("SID=" + sidToken);
139     // logo.state = "hidden"; //.visible = false;
140     if(sid.length>0) {
141         //navigation.state = "menu";
142         //waiting.state = "hidden";
143
144         WorkerScript.sendMessage({"sid": sid, "sidToken": sidToken});
145
146         //loadUnreadNews();
147     } else {
148         addError("Couldn't parse SID");
149         //waiting.state = "hidden";
150     }
151 }
152
153 function loadSubscriptions() {
154     //waiting.state = "shown";
155     var url = "http://www.google.com/reader/api/0/subscription/list?output=json";
156     doWebRequest("GET", url, "", parseSubscriptions);
157 }
158
159 function parseSubscriptions(data) {
160     //console.log("Subscriptions: " + data);
161
162     var tags = eval("[" + data + "]")[0];
163     for(var i in tags.subscriptions) {
164         var tag = tags.subscriptions[i];
165         var id = tag.id;
166         var title = tag.title;
167
168         WorkerScript.sendMessage({"title": title, "published": '',"tag": tag, "id":id});
169     }
170     //navigation.state = "tags";
171     //waiting.state = "hidden";
172 }
173
174 function loadTags() {
175     //waiting.state = "shown";
176     var url = "http://www.google.com/reader/api/0/tag/list?output=json";
177     doWebRequest("GET", url, "", parseTags);
178 }
179
180 function parseTags(data) {
181     var tags = eval("[" + data + "]")[0];
182     for(var i in tags.tags) {
183         var tag = tags.tags[i];
184         var id = tag.id;
185         var title = id;
186         while(title.indexOf("/")>0) {
187             var index = title.indexOf("/");
188             title = title.substring(index+1);
189         }
190
191         WorkerScript.sendMessage({"title": title, "published": '', "tag": tag, "id":id});
192     }
193     //navigation.state = "tags";
194     //waiting.state = "hidden";
195 }
196
197 function loadAllNews() {
198     itemsURL = "http://www.google.com/reader/api/0/stream/contents/user/-/state/com.google/reading-list";
199     loadNews();
200 }
201
202 function loadUnreadNews() {
203     itemsURL = "http://www.google.com/reader/api/0/stream/contents/user/-/state/com.google/reading-list?xt=user/-/state/com.google/read";
204     loadNews();
205 }
206
207 function loadStarred() {
208     itemsURL = "http://www.google.com/reader/api/0/stream/contents/user/-/state/com.google/starred";
209     loadNews();
210 }
211
212 function loadTaggedItems(tag) {
213     itemsURL = "http://www.google.com/reader/api/0/stream/contents/" + tag;
214     loadNews();
215 }
216
217 function loadSubscriptionItems(subscription) {
218     itemsURL = "http://www.google.com/reader/api/0/stream/contents/" + subscription;
219     loadNews();
220 }
221
222 function loadNews() {
223     try {
224         //waiting.state = "shown";
225         doWebRequest("GET", itemsURL, "", parseNews);
226     } catch(err) {
227         showError("Error while loading news: " + err);
228     }
229 }
230
231 function getNodeValue(node, name) {
232     var nodeValue = "";
233     for(var i=0; i<node.childNodes.length; i++) {
234         var nodeName = node.childNodes[i].nodeName;
235         if(nodeName==name) {
236             nodeValue = node.childNodes[i].firstChild.nodeValue;
237         }
238     }
239     return nodeValue;
240 }
241
242 function parseEntry(item) {
243     var content = ""
244     if(typeof(item.content)!=undefined && item.content!=null) {
245         content = item.content.content;
246     } else if(typeof(item.summary)!=undefined && item.summary!=null) {
247         content = item.summary.content;
248     } else {
249         content = "";
250     }
251     content = removeLinks(content);
252     var milliseconds = parseInt(parseInt(item.published)*1000);
253     var published = new Date(parseInt(milliseconds));
254     var link = item.alternate[0].href;
255     //console.log("Link: " + link);
256     var isRead = true;
257     for(var i in item.categories) {
258         var category = item.categories[i];
259         if(category.indexOf("/reading-list")>0) {
260             isRead = false;
261         }
262     }
263
264
265     WorkerScript.sendMessage({
266                                  "id": item.id,
267                                  "title": item.title,
268                                  "description": content,
269                                  "author": item.origin.title,
270                                  "published": prettyDate(published),
271                                  "more": false,
272                                  "source": item.origin.title,
273                                  "link": link,
274                                  "feedUrl": item.origin.streamId,
275                                  "isRead": isRead
276                     });
277 }
278
279 function parseNews(data) {
280     //try {
281         //console.log("DATA: " + data);
282         var doc = eval("[" + data + "]")[0];
283         if(doc==null || typeof(doc)==undefined) {
284             WorkerScript.sendMessage({
285                          "title": "Error",
286                          "description": "",
287                          "author": "",
288                          "published": "",
289                          "more": true,
290                          "isRead": false,
291                          "source": ""});
292             //waiting.state = "hidden";
293             return;
294         }
295
296         //var moreIndex = model.count - 1;
297
298         continuation = doc.continuation;
299         for(var i in doc.items) {
300             var item = doc.items[i];
301             parseEntry(item);
302         }
303 /*
304         if(moreIndex>0) {
305             if(model.get(moreIndex).title.indexOf("Loading...")>-1) {
306                 model.remove(moreIndex);
307             }
308         }
309
310         model.append({
311                      "title": "Load more...<br/><br/>",
312                      "description": "",
313                      "author": "",
314                      "published": "",
315                      "isRead": false,
316                      "more": true,
317                      "source": ""});
318                      */
319     //}catch(err) {
320     //    addError("Error: " + err);
321     //}
322    // navigation.state = "items";
323    // waiting.state = "hidden";
324 }
325
326 function loadMore() {
327     //waiting.state = "shown";
328     var url = itemsURL;
329     if(itemsURL.indexOf("?")>0) {
330         url += "&";
331     } else {
332         url += "?";
333     }
334     url += "c=" + continuation;
335     doWebRequest("GET", url, "", parseNews);
336 }
337
338 function getToken() {
339     var url = "http://www.google.com/reader/api/0/token";
340     doWebRequest("GET", url, "", parseAccessToken, null);
341 }
342
343 function parseAccessToken(data) {
344     accessToken = data;
345     if(action=="read") {
346         var url = "http://www.google.com/reader/api/0/edit-tag";
347         var dd = "ac=edit-tags&a=user/-/state/com.google/read&i=" + encodeURIComponent(actionID) + "&s=" + encodeURIComponent(actionFeedUrl) + "&T=" + accessToken;
348         doWebRequest("POST", url, dd, showDone, null);
349     } else if(action=="unread") {
350         var url = "http://www.google.com/reader/api/0/edit-tag";
351         var dd = "ac=edit-tags&a=user/-/state/com.google/kept-unread&r=user/-/state/com.google/read&i=" + encodeURIComponent(actionID) + "&s=" + encodeURIComponent(actionFeedUrl) + "&T=" + accessToken;
352         doWebRequest("POST", url, dd, showDone, null);
353     } else if(action=="fav") {
354         var url = "http://www.google.com/reader/api/0/edit-tag?client=-";
355         var dd = "a=user/-/state/com.google/starred&i=" + encodeURIComponent(actionID) + "&s=" + encodeURIComponent(actionFeedUrl) + "&T=" + accessToken;
356         doWebRequest("POST", url, dd, showDone, null);
357     } else if(action=="tags") {
358         var url = "http://www.google.com/reader/api/0/edit-tag?client=-";
359         var dd =
360             "a=user/-/label/" + encodeURIComponent(tags) +
361             "&i=" + encodeURIComponent(actionID) +
362             "&s=" + encodeURIComponent(actionFeedUrl) +
363             "&T=" + accessToken;
364         doWebRequest("POST", url, dd, showDone, null);
365     }
366 }
367
368 function markAsRead(id, feedUrl, showLoadingIndicator) {
369     actionID = id;
370     actionFeedUrl = feedUrl;
371     if(showLoadingIndicator) {
372         waiting.state = "shown";
373     }
374     action = "read";
375     getToken();
376 }
377
378 function markAsUnread(id, feedUrl) {
379     actionID = id;
380     actionFeedUrl = feedUrl;
381     //waiting.state = "shown";
382     action = "unread";
383     //done.status = "Marked as unread";
384     getToken();
385 }
386
387 function addTags(id, feedUrl, newTags) {
388     actionID = id;
389     actionFeedUrl = feedUrl;
390     //waiting.state = "shown";
391     action = "tags";
392     //done.status = "Added tags";
393     tags = newTags;
394     getToken();
395 }
396
397 function markAsFavourite(id, feedUrl) {
398     actionID = id;
399     actionFeedUrl = feedUrl;
400     //waiting.state = "shown";
401     action = "fav";
402     //done.status = "Marked as favourite";
403     getToken();
404 }
405
406 function showDone(data) {
407     console.log("DONE: " + data);
408     //if(waiting.state!="shown") {
409     //    return;
410     //}
411     //waiting.state = "hidden";
412     //done.status = "";
413     if(typeof(data)!=undefined && data!=null) {
414         if(action=="read") {
415             //done.status = "Marked as read " + data;
416         } else if(action=="unread") {
417             //done.status = "Marked as unread " + data;
418         } else {
419             //done.status = "" + data;
420         }
421     }
422     //done.state = "shown";
423 }
424
425 function doNothing(data) {
426     // Nothing...
427 }
428
429 function prettyDate(date){
430     try {
431         var diff = (((new Date()).getTime() - date.getTime()) / 1000);
432         var day_diff = Math.floor(diff / 86400);
433
434         if ( isNaN(day_diff) || day_diff >= 31 ) {
435             //console.log("Days: " + day_diff);
436             return "some time ago";
437         } else if (day_diff < 0) {
438             //console.log("day_diff: " + day_diff);
439             return "just now";
440         }
441
442         return day_diff == 0 && (
443                     diff < 60 && "just now" ||
444                     diff < 120 && "1 minute ago" ||
445                     diff < 3600 && Math.floor( diff / 60 ) + " min ago" ||
446                     diff < 7200 && "1 hour ago" ||
447                     diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") ||
448                 day_diff == 1 && "Yesterday" ||
449                 day_diff < 7 && day_diff + " days ago" ||
450                 day_diff < 31 && Math.ceil( day_diff / 7 ) + " weeks ago";
451         day_diff >= 31 && Math.ceil( day_diff / 30 ) + " months ago";
452     } catch(err) {
453         console.log("Error: " + err);
454         return "some time ago";
455     }
456 }
457
458 // 2011-01-24T18:48:00Z
459 function parseDate(stamp)
460 {
461     try {
462         //console.log("stamp: " + stamp);
463         var parts = stamp.split("T");
464         var day;
465         var time;
466         var hours;
467         var minutes;
468         var seconds = 0;
469         var year;
470         var month;
471
472         var dates = parts[0].split("-");
473         year = parseInt(dates[0]);
474         month = parseInt(dates[1])-1;
475         day = parseInt(dates[2]);
476
477         var times = parts[1].split(":");
478         hours = parseInt(times[0]);
479         minutes = parseInt(times[1]);
480
481         var dt = new Date();
482         dt.setUTCDate(day);
483         dt.setYear(year);
484         dt.setUTCMonth(month);
485         dt.setUTCHours(hours);
486         dt.setUTCMinutes(minutes);
487         dt.setUTCSeconds(seconds);
488
489         //console.log("day: " + day + " year: " + year + " month " + month + " hour " + hours);
490
491         return dt;
492     } catch(err) {
493         console.log("Error while parsing date: " + err);
494         return new Date();
495     }
496 }
497
498 /// ======== WorkerScript related functions ========
499 WorkerScript.onMessage = function(message) {
500
501      // the structure of "message" object is:
502      //   - attribute 'action' --> what to do
503      //   - other attributes: will be passed on to the right function
504
505      if(message.action === 'login') {
506          login(message.email, message.password)
507      }
508      else {
509          sid = message.sid; sidToken = message.sidToken;
510
511          if(message.action === 'getCategoryContent') {
512              // read the feeds in the right category
513              switch(message.category) {
514                  case 0: loadAllNews(); break;
515                  case 1: loadUnreadNews(); break;
516                  case 2: loadStarred(); break;
517                  case 3: loadSubscriptions(); break;
518                  case 4: loadTags(); break;
519              }
520          }
521          else if(message.action === 'getSubscriptionItems') {
522              // read the feeds of that subscription
523              loadSubscriptionItems(message.subscription)
524          }
525          else if(message.action === 'getTaggedItems') {
526              // read the feeds of that subscription
527              loadTaggedItems(message.tag)
528          }
529      }
530 }
531