Only fetch entries we don't already fetched before.
[grr] / src / googlereader.cpp
1 #include <QtWebKit>
2 #include <QApplication>
3 #include <QNetworkRequest>
4 #include <QNetworkReply>
5 #include <QNetworkCookie>
6 #include <QBuffer>
7 #include <QTimer>
8 #include <QDateTime>
9 #include <QDebug>
10
11 #include <qjson/parser.h>
12
13 #include <stdio.h>
14 #include <unistd.h>
15 #include <stdlib.h>
16
17 #include "googlereader.h"
18
19 void Feed::updateSubscription(Feed *feed) {
20         title = feed->title;
21         sortid = feed->sortid;
22         firstitemmsec = feed->firstitemmsec;
23         cat_id = feed->cat_id;
24         cat_label = feed->cat_label;
25         subscription_updated = true;
26 }
27
28 void Feed::fetch(bool cont) {
29         QNetworkRequest request;
30         QByteArray ba = "http://www.google.com/reader/api/0/stream/contents/";
31         ba.append(QUrl::toPercentEncoding(id));
32         QUrl url = QUrl::fromEncoded(ba);
33
34         if(continuation != "" && cont)
35                 url.addEncodedQueryItem("c", continuation.toUtf8());
36
37         if(!cont && updated) {
38                 /* Add 1 to the timestamp, otherwise we get the latest item
39                  * again. Also the order has to be reversed for this to work. */
40                 url.addEncodedQueryItem("ot", QByteArray::number(updated + 1));
41                 url.addEncodedQueryItem("r", "o");
42         }
43
44         request.setUrl(url);
45         reply = reader->getManager()->get(request);
46         connect(reply, SIGNAL(finished()), SLOT(fetchFinished()));
47 }
48
49 void Feed::fetchFinished() {
50         if (reply->error()) {
51                 qDebug() << "Download of" << reply->url() << "failed:" << qPrintable(reply->errorString());
52                 return;
53         }
54
55         QJson::Parser parser;
56         bool ok;
57         QVariantMap result = parser.parse(reply->readAll(), &ok).toMap();
58         QString continuation;
59
60         continuation = result["continuation"].toString();
61         updated = result["updated"].toUInt();
62
63         foreach(QVariant l, result["items"].toList()) {
64                 QVariantMap e = l.toMap();
65                 Entry *entry = new Entry();;
66                 QString content, summary;
67
68                 entry->id = e["id"].toString();
69                 entry->published = QDateTime::fromTime_t(e["published"].toUInt());
70                 entry->author = e["author"].toString();
71                 entry->source = (e["origin"].toMap())["streamId"].toString();
72                 entry->link = (e["alternate"].toMap())["href"].toString();
73
74                 content = (e["content"].toMap())["content"].toString();
75                 summary = (e["summary"].toMap())["content"].toString();
76                 if(content != "") entry->content = content; else entry->content = summary;
77
78                 if(e["isReadStateLocked"].toBool())
79                         entry->flags |= ENTRY_FLAG_LOCKED | ENTRY_FLAG_READ;
80
81                 QWebPage p;
82                 p.mainFrame()->setHtml(e["title"].toString());
83                 entry->title = p.mainFrame()->toPlainText();
84
85                 foreach(QVariant c, e["categories"].toList()) {
86                         QString cat = c.toString();
87                         if(cat.endsWith("/state/com.google/read"))
88                                 entry->flags |= ENTRY_FLAG_READ;
89                         else if(cat.endsWith("/state/com.google/starred"))
90                                 entry->flags |= ENTRY_FLAG_STARRED;
91                         else if(cat.endsWith("/state/com.google/broadcast"))
92                                 entry->flags |= ENTRY_FLAG_SHARED;
93                 }
94
95                 entry->feed = this;
96                 addEntry(entry);
97         }
98
99         lastUpdated = QDateTime::currentDateTime();
100
101         emit updateFeedComplete();
102
103         reply->deleteLater();
104 }
105
106 GoogleReader::GoogleReader() {
107         connect(&manager, SIGNAL(finished(QNetworkReply*)),
108                 SLOT(downloadFinished(QNetworkReply*)));
109
110         SID = NULL;
111         updateSubscriptionsPending = false;
112         updateUnreadPending = false;
113         SIDPending = false;
114
115         login_url.setUrl("https://www.google.com/accounts/ClientLogin");
116         subscriptions_url.setUrl("http://www.google.com/reader/api/0/subscription/list?output=json");
117         unread_url.setUrl("http://www.google.com/reader/api/0/unread-count?output=json");
118         edittag_url.setUrl("http://www.google.com/reader/api/0/edit-tag?client=-");
119         token_url.setUrl("http://www.google.com/reader/api/0/token");
120         markallread_url.setUrl("http://www.google.com/reader/api/0/mark-all-as-read?client=-");
121
122         /* Add the virtual 'Starred items' feed */
123         Feed *feed = new Feed(this);
124         feed->id = "user/-/state/com.google/starred";
125         feed->title = "Starred items";
126         feed->special = 2;
127         feeds.insert(feed->id, feed);
128         connect(feed, SIGNAL(allReadChanged()), SIGNAL(allReadChanged()));
129
130         /* Add the virtual 'Shared items' feed */
131         feed = new Feed(this);
132         feed->id = "user/-/state/com.google/broadcast";
133         feed->title = "Shared items";
134         feed->special = 1;
135         feeds.insert(feed->id, feed);
136         connect(feed, SIGNAL(allReadChanged()), SIGNAL(allReadChanged()));
137 }
138
139 void GoogleReader::downloadFinished(QNetworkReply *reply) {
140         QUrl url = reply->url();
141
142         /* TODO: Instead of comparing against the url, use the signal from the
143          * QNetworkReply... */
144
145         if (reply->error()) {
146                 qDebug() << "Download of" << url << "failed:" << qPrintable(reply->errorString());
147                 if(url == login_url) {
148                         SIDPending = false;
149                         emit loginFailed("Incorrect username or password");
150                 }
151                 else if(url == edittag_url)
152                         getToken();
153                 return;
154         }
155         else if(url == login_url) {
156                 QByteArray data = reply->readAll();
157                 data.remove(0, data.indexOf("SID=", 0) + 4);
158                 data.remove(data.indexOf("\n", 0), 1024);
159                 SID = strdup(data.data());
160
161                 qDebug() << "SID:" << SID;
162
163                 manager.cookieJar()->setCookiesFromUrl(
164                         QList<QNetworkCookie>() << QNetworkCookie("SID", SID),
165                         QUrl("http://www.google.com"));
166
167                 SIDPending = false;
168
169                 getToken();
170
171                 /* TODO: Replace this with a proper state machine */
172                 if(updateSubscriptionsPending) {
173                         updateSubscriptionsPending = false;
174                         updateSubscriptions();
175                 }
176         }
177         else if(url == token_url) {
178                 token = reply->readAll();
179                 qDebug() << "token:" << token;
180         }
181         else if(url == subscriptions_url) {
182                 parseSubscriptions(reply->readAll());
183
184                 /* TODO: Replace this with a proper state machine */
185                 if(updateUnreadPending) {
186                         updateUnreadPending = false;
187                         updateUnread();
188                 }
189         }
190         else if(url == unread_url) {
191                 parseUnread(reply->readAll());
192         }
193         else if(url == edittag_url) {
194                 QByteArray data = reply->readAll();
195                 //qDebug() << "Result:" << data;
196         }
197         else if(url == markallread_url) {
198                 QByteArray data = reply->readAll();
199                 //qDebug() << "Result:" << data;
200         }
201
202         reply->deleteLater();
203 }
204
205 void GoogleReader::parseSubscriptions(QByteArray data) {
206         QJson::Parser parser;
207         bool ok;
208         QVariantMap result = parser.parse(data, &ok).toMap();
209
210         /* Clear the subscription updated flag */
211         QHash<QString, Feed *>::iterator i;
212         for(i = feeds.begin(); i != feeds.end(); ++i)
213                 i.value()->subscription_updated = false;
214
215         foreach(QVariant l, result["subscriptions"].toList()) {
216                 QVariantMap subscription = l.toMap();
217                 Feed *feed = new Feed(this);
218                 Feed *existing_feed;
219
220                 feed->id = subscription["id"].toString();
221                 feed->title = subscription["title"].toString();
222                 feed->sortid = subscription["sortid"].toString();
223                 feed->firstitemmsec = subscription["firstitemmsec"].toString();
224
225                 foreach(QVariant c, subscription["categories"].toList()) {
226                         QVariantMap cat = c.toMap();
227                         feed->cat_id = cat["id"].toString();
228                         feed->cat_label = cat["label"].toString();
229                 }
230
231                 existing_feed = feeds.value(feed->id);
232                 if(existing_feed) {
233                         existing_feed->updateSubscription(feed);
234                         delete(feed);
235                         feed = existing_feed;
236
237                 }
238                 else {
239                         feed->subscription_updated = true;
240                         feeds.insert(feed->id, feed);
241                         connect(feed, SIGNAL(allReadChanged()), SIGNAL(allReadChanged()));
242                 }
243         }
244
245         /* Delete feeds no longer subscribed to  */
246         for(i = feeds.begin(); i != feeds.end(); ++i) {
247                 if(i.value()->subscription_updated == false && i.value()->special == 0) {
248                         printf("DELETED: %s\n", i.value()->title.toLatin1().data());
249                         i = feeds.erase(i);
250                 }
251         }
252
253         lastUpdated = QDateTime::currentDateTime();
254         emit updateSubscriptionsComplete();
255 }
256
257 void GoogleReader::parseUnread(QByteArray data) {
258         QJson::Parser parser;
259         bool ok;
260         QVariantMap result = parser.parse(data, &ok).toMap();
261
262         foreach(QVariant l, result["unreadcounts"].toList()) {
263                 QVariantMap unread = l.toMap();
264                 QString id = unread["id"].toString();
265                 int count = unread["count"].toInt();
266                 ulong newestitem = unread["newestitem"].toUInt();
267
268                 Feed *f = feeds.value(id);
269                 if(f) {
270                         f->unread = count;
271                         f->newestitem = newestitem;
272
273                         /* Not a good idea if it doesn't happen sequentially. */
274                         /* Pre-fetch feeds with unread items */
275                         /* f->fetch(false); */
276                 }
277         }
278
279         lastUpdated = QDateTime::currentDateTime();
280         emit updateUnreadComplete();
281 }
282
283 void GoogleReader::getSID() {
284
285         if(SIDPending)
286                 return;
287
288         SIDPending = true;
289
290         QNetworkRequest request;
291         request.setUrl(login_url);
292
293         buffer.open(QBuffer::ReadWrite | QBuffer::Truncate);
294         buffer.write("Email=");
295         buffer.write(QUrl::toPercentEncoding(login));
296         buffer.write("&Passwd=");
297         buffer.write(QUrl::toPercentEncoding(passwd));
298         buffer.write("&service=reader&source=grms&continue=http://www.google.com");
299
300         //buffer.seek(0);
301         //qDebug() << buffer.readAll();
302
303         buffer.seek(0);
304         manager.post(request, &buffer);
305 }
306
307 void GoogleReader::getToken() {
308         QNetworkRequest request;
309         request.setUrl(token_url);
310         manager.get(request);
311 }
312
313 void GoogleReader::updateSubscriptions() {
314         QNetworkRequest request;
315
316         if(updateSubscriptionsPending)
317                 return;
318
319         if(!SID) {
320                 updateSubscriptionsPending = true;
321                 getSID();
322                 return;
323         }
324
325         request.setUrl(subscriptions_url);
326         manager.get(request);
327 }
328
329 void GoogleReader::updateUnread() {
330         QNetworkRequest request;
331
332         if(updateUnreadPending)
333                 return;
334
335         if(!SID) {
336                 updateUnreadPending = true;
337                 getSID();
338                 return;
339         }
340
341         request.setUrl(unread_url);
342         manager.get(request);
343 }
344
345 static bool compareFeedItems(const Feed *f1, const Feed *f2) {
346         if(f1->special || f2->special)
347                 return f1->special > f2->special;
348
349         if(f1->cat_label == f2->cat_label)
350                 return f1->title.toLower() < f2->title.toLower();
351
352         if(f1->cat_label.length() == 0)
353                 return false;
354
355         if(f2->cat_label.length() == 0)
356                 return true;
357
358         return f1->cat_label.toLower() < f2->cat_label.toLower();
359 }
360
361 QList<Feed *> GoogleReader::getFeeds() {
362         QList<Feed *> list = feeds.values();
363         qSort(list.begin(), list.end(), compareFeedItems);
364         return list;
365 }
366
367 void Feed::addEntry(Entry *entry) {
368         entries.insert(entry->id, entry);
369 }
370
371 void Feed::delEntry(Entry *entry) {
372         entries.remove(entry->id);
373 }
374
375 void Feed::updateUnread(int i) {
376         bool allRead = (unread == 0);
377
378         unread += i;
379         if(unread <= 0) unread = 0;
380
381         if(allRead != (unread == 0))
382                 emit allReadChanged();
383 }
384
385 void Feed::markRead() {
386         if(unread > 0) {
387                 /* Mark all the remaining items read */
388
389                 QNetworkRequest request;
390                 request.setUrl(reader->markallread_url);
391
392                 buffer.open(QBuffer::ReadWrite | QBuffer::Truncate);
393                 buffer.write("s=");
394                 buffer.write(QUrl::toPercentEncoding(id));
395                 //buffer.write("&ts=");
396                 //buffer.write(QByteArray::number(oldest));
397                 buffer.write("&T=");
398                 buffer.write(QUrl::toPercentEncoding(reader->token));
399
400                 //buffer.seek(0);
401                 //qDebug() << buffer.readAll();
402
403                 buffer.seek(0);
404                 reader->manager.post(request, &buffer);
405
406                 unread = 0;
407
408                 /* Go over all the entries and mark them read */
409                 QHash<QString, Entry *>::iterator i;
410                 for(i = entries.begin(); i != entries.end(); ++i)
411                         i.value()->flags |= ENTRY_FLAG_READ | ENTRY_FLAG_LOCKED;
412         }
413
414         emit allReadChanged();
415 }
416
417 static bool compareEntryItems(const Entry *e1, const Entry *e2) {
418         return e1->published > e2->published;
419 }
420
421 QList<Entry *> Feed::getEntries() {
422         QList<Entry *> list = entries.values();
423         qSort(list.begin(), list.end(), compareEntryItems);
424         return list;
425 }
426
427 /* TODO: Remove the duplicate code in changing stated */
428
429 void Entry::markRead(bool mark_read) {
430         /* Check if the read flag differs from the requested state */
431         if(((flags & ENTRY_FLAG_READ) != 0) == mark_read)
432                 return;
433
434         /* Cannot mark an item unread if it's locked */
435         if((flags & ENTRY_FLAG_LOCKED) && !mark_read)
436                 return;
437
438         QNetworkRequest request;
439         request.setUrl(feed->reader->edittag_url);
440
441         postread.open(QBuffer::ReadWrite | QBuffer::Truncate);
442         postread.write("i=");
443         postread.write(QUrl::toPercentEncoding(id));
444         if(mark_read)
445                 postread.write("&a=");
446         else
447                 postread.write("&r=");
448         postread.write(QUrl::toPercentEncoding("user/-/state/com.google/read"));
449         postread.write("&ac=edit-tags&T=");
450         postread.write(QUrl::toPercentEncoding(feed->reader->token));
451         postread.seek(0);
452         feed->reader->manager.post(request, &postread);
453
454         feed->updateUnread(mark_read ? -1 : 1);
455
456         if(mark_read)
457                 flags |= ENTRY_FLAG_READ;
458         else
459                 flags &= ~ENTRY_FLAG_READ;
460 }
461
462 void Entry::markStar(bool mark_star) {
463         /* Check if the starred flag differs from the requested state */
464         if(((flags & ENTRY_FLAG_STARRED) != 0) == mark_star)
465                 return;
466
467         QNetworkRequest request;
468         request.setUrl(feed->reader->edittag_url);
469
470         poststar.open(QBuffer::ReadWrite | QBuffer::Truncate);
471         poststar.write("i=");
472         poststar.write(QUrl::toPercentEncoding(id));
473         if(mark_star)
474                 poststar.write("&a=");
475         else
476                 poststar.write("&r=");
477         poststar.write(QUrl::toPercentEncoding("user/-/state/com.google/starred"));
478         poststar.write("&ac=edit-tags&T=");
479         poststar.write(QUrl::toPercentEncoding(feed->reader->token));
480         poststar.seek(0);
481         feed->reader->manager.post(request, &poststar);
482
483         Feed *starred = feed->reader->feeds.value("user/-/state/com.google/starred");
484
485         if(mark_star) {
486                 starred->addEntry(this);
487                 flags |= ENTRY_FLAG_STARRED;
488         }
489         else {
490                 starred->delEntry(this);
491                 flags &= ~ENTRY_FLAG_STARRED;
492         }
493 }
494
495 void Entry::markShared(bool mark_shared) {
496         /* Check if the shared flag differs from the requested state */
497         if(((flags & ENTRY_FLAG_SHARED) != 0) == mark_shared)
498                 return;
499
500         QNetworkRequest request;
501         request.setUrl(feed->reader->edittag_url);
502
503         postshared.open(QBuffer::ReadWrite | QBuffer::Truncate);
504         postshared.write("i=");
505         postshared.write(QUrl::toPercentEncoding(id));
506         if(mark_shared)
507                 postshared.write("&a=");
508         else
509                 postshared.write("&r=");
510         postshared.write(QUrl::toPercentEncoding("user/-/state/com.google/broadcast"));
511         postshared.write("&ac=edit-tags&T=");
512         postshared.write(QUrl::toPercentEncoding(feed->reader->token));
513         postshared.seek(0);
514         feed->reader->manager.post(request, &postshared);
515
516         Feed *shared = feed->reader->feeds.value("user/-/state/com.google/broadcast");
517
518         if(mark_shared) {
519                 shared->addEntry(this);
520                 flags |= ENTRY_FLAG_SHARED;
521         }
522         else {
523                 shared->delEntry(this);
524                 flags &= ~ENTRY_FLAG_SHARED;
525         }
526 }