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