3d7ab158442f59e9caffe5605d06826ab996a4aa
[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/atom/";
31         QByteArray ba = "http://www.google.com/reader/api/0/stream/contents/";
32         ba.append(QUrl::toPercentEncoding(id));
33         QUrl url = QUrl::fromEncoded(ba);
34
35         if(continuation != "" && cont)
36                 url.addEncodedQueryItem("c", continuation.toUtf8());
37
38         request.setUrl(url);
39         reader->getManager()->get(request);
40 }
41
42 GoogleReader::GoogleReader() {
43         connect(&manager, SIGNAL(finished(QNetworkReply*)),
44                 SLOT(downloadFinished(QNetworkReply*)));
45
46         SID = NULL;
47         updateSubscriptionsPending = false;
48         updateUnreadPending = false;
49         SIDPending = false;
50
51         login_url.setUrl("https://www.google.com/accounts/ClientLogin");
52         subscriptions_url.setUrl("http://www.google.com/reader/api/0/subscription/list?output=json");
53         unread_url.setUrl("http://www.google.com/reader/api/0/unread-count?output=json");
54         edittag_url.setUrl("http://www.google.com/reader/api/0/edit-tag?client=-");
55         token_url.setUrl("http://www.google.com/reader/api/0/token");
56         markallread_url.setUrl("http://www.google.com/reader/api/0/mark-all-as-read?client=-");
57
58         /* Add the virtual 'Starred items' feed */
59         Feed *feed = new Feed(this);
60         feed->id = "user/-/state/com.google/starred";
61         feed->title = "Starred items";
62         feed->special = 2;
63         feeds.insert(feed->id, feed);
64         connect(feed, SIGNAL(allReadChanged()), SIGNAL(allReadChanged()));
65
66         /* Add the virtual 'Shared items' feed */
67         feed = new Feed(this);
68         feed->id = "user/-/state/com.google/broadcast";
69         feed->title = "Shared items";
70         feed->special = 1;
71         feeds.insert(feed->id, feed);
72         connect(feed, SIGNAL(allReadChanged()), SIGNAL(allReadChanged()));
73 }
74
75 void GoogleReader::downloadFinished(QNetworkReply *reply) {
76         QUrl url = reply->url();
77
78         /* TODO: Instead of comparing against the url, use the signal from the
79          * QNetworkReply... */
80
81         if (reply->error()) {
82                 qDebug() << "Download of" << url << "failed:" << qPrintable(reply->errorString());
83                 if(url == login_url) {
84                         SIDPending = false;
85                         emit loginFailed("Incorrect username or password");
86                 }
87                 else if(url == edittag_url)
88                         getToken();
89                 return;
90         }
91         else if(url == login_url) {
92                 QByteArray data = reply->readAll();
93                 data.remove(0, data.indexOf("SID=", 0) + 4);
94                 data.remove(data.indexOf("\n", 0), 1024);
95                 SID = strdup(data.data());
96
97                 qDebug() << "SID:" << SID;
98
99                 manager.cookieJar()->setCookiesFromUrl(
100                         QList<QNetworkCookie>() << QNetworkCookie("SID", SID),
101                         QUrl("http://www.google.com"));
102
103                 SIDPending = false;
104
105                 getToken();
106
107                 /* TODO: Replace this with a proper state machine */
108                 if(updateSubscriptionsPending) {
109                         updateSubscriptionsPending = false;
110                         updateSubscriptions();
111                 }
112         }
113         else if(url == token_url) {
114                 token = reply->readAll();
115                 qDebug() << "token:" << token;
116         }
117         else if(url == subscriptions_url) {
118                 parseSubscriptions(reply->readAll());
119
120                 /* TODO: Replace this with a proper state machine */
121                 if(updateUnreadPending) {
122                         updateUnreadPending = false;
123                         updateUnread();
124                 }
125         }
126         else if(url == unread_url) {
127                 parseUnread(reply->readAll());
128         }
129         else if(url == edittag_url) {
130                 QByteArray data = reply->readAll();
131                 //qDebug() << "Result:" << data;
132         }
133         else if(url == markallread_url) {
134                 QByteArray data = reply->readAll();
135                 //qDebug() << "Result:" << data;
136         }
137         else {
138                 parseFeed(reply->readAll());
139         }
140
141         reply->deleteLater();
142 }
143
144 void GoogleReader::parseFeed(QByteArray data) {
145         QJson::Parser parser;
146         bool ok;
147         QVariantMap result = parser.parse(data, &ok).toMap();
148         QString continuation, feedsource, id;
149         Feed *feed = NULL;
150
151         id = result["id"].toString();
152         continuation = result["continuation"].toString();
153
154         /* TODO: This hack should not be needed.. Figure out a cleaner way */
155         if(id.endsWith("/state/com.google/starred"))
156                 feedsource = "user/-/state/com.google/starred";
157         else if(id.endsWith("/state/com.google/broadcast"))
158                 feedsource = "user/-/state/com.google/broadcast";
159
160         foreach(QVariant l, result["items"].toList()) {
161                 QVariantMap e = l.toMap();
162                 Entry entry;
163                 QString content, summary;
164
165                 entry.id = e["id"].toString();
166                 entry.published = QDateTime::fromTime_t(e["published"].toUInt());
167                 entry.author = e["author"].toString();
168                 entry.source = (e["origin"].toMap())["streamId"].toString();
169                 entry.link = (e["alternate"].toMap())["href"].toString();
170
171                 content = (e["content"].toMap())["content"].toString();
172                 summary = (e["summary"].toMap())["content"].toString();
173                 if(content != "") entry.content = content; else entry.content = summary;
174
175                 if(e["isReadStateLocked"].toBool())
176                         entry.flags |= ENTRY_FLAG_LOCKED | ENTRY_FLAG_READ;
177
178                 QWebPage p;
179                 p.mainFrame()->setHtml(e["title"].toString());
180                 entry.title = p.mainFrame()->toPlainText();
181
182                 foreach(QVariant c, e["categories"].toList()) {
183                         QString cat = c.toString();
184                         if(cat.endsWith("/state/com.google/read"))
185                                 entry.flags |= ENTRY_FLAG_READ;
186                         else if(cat.endsWith("/state/com.google/starred"))
187                                 entry.flags |= ENTRY_FLAG_STARRED;
188                         else if(cat.endsWith("/state/com.google/broadcast"))
189                                 entry.flags |= ENTRY_FLAG_SHARED;
190                 }
191
192                 if(!feed)
193                         feed = feeds.value(feedsource == "" ? entry.source : feedsource);
194
195                 if(feed) {
196                         entry.feed = feed;
197                         feed->addEntry(new Entry(entry));
198                 }
199         }
200
201         if(feed) {
202                 feed->lastUpdated = QDateTime::currentDateTime();
203                 feed->continuation = continuation;
204                 feed->signalUpdated();
205         }
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         }
277
278         lastUpdated = QDateTime::currentDateTime();
279         emit updateUnreadComplete();
280 }
281
282 void GoogleReader::getSID() {
283
284         if(SIDPending)
285                 return;
286
287         SIDPending = true;
288
289         QNetworkRequest request;
290         request.setUrl(login_url);
291
292         buffer.open(QBuffer::ReadWrite | QBuffer::Truncate);
293         buffer.write("Email=");
294         buffer.write(QUrl::toPercentEncoding(login));
295         buffer.write("&Passwd=");
296         buffer.write(QUrl::toPercentEncoding(passwd));
297         buffer.write("&service=reader&source=grms&continue=http://www.google.com");
298
299         //buffer.seek(0);
300         //qDebug() << buffer.readAll();
301
302         buffer.seek(0);
303         manager.post(request, &buffer);
304 }
305
306 void GoogleReader::getToken() {
307         QNetworkRequest request;
308         request.setUrl(token_url);
309         manager.get(request);
310 }
311
312 void GoogleReader::updateSubscriptions() {
313         QNetworkRequest request;
314
315         if(updateSubscriptionsPending)
316                 return;
317
318         if(!SID) {
319                 updateSubscriptionsPending = true;
320                 getSID();
321                 return;
322         }
323
324         request.setUrl(subscriptions_url);
325         manager.get(request);
326 }
327
328 void GoogleReader::updateUnread() {
329         QNetworkRequest request;
330
331         if(updateUnreadPending)
332                 return;
333
334         if(!SID) {
335                 updateUnreadPending = true;
336                 getSID();
337                 return;
338         }
339
340         request.setUrl(unread_url);
341         manager.get(request);
342 }
343
344 static bool compareFeedItems(const Feed *f1, const Feed *f2) {
345         if(f1->special || f2->special)
346                 return f1->special > f2->special;
347
348         if(f1->cat_label == f2->cat_label)
349                 return f1->title.toLower() < f2->title.toLower();
350
351         if(f1->cat_label.length() == 0)
352                 return false;
353
354         if(f2->cat_label.length() == 0)
355                 return true;
356
357         return f1->cat_label.toLower() < f2->cat_label.toLower();
358 }
359
360 QList<Feed *> GoogleReader::getFeeds() {
361         QList<Feed *> list = feeds.values();
362         qSort(list.begin(), list.end(), compareFeedItems);
363         return list;
364 }
365
366 void Feed::addEntry(Entry *entry) {
367         entries.insert(entry->id, entry);
368 }
369
370 void Feed::delEntry(Entry *entry) {
371         entries.remove(entry->id);
372 }
373
374 void Feed::signalUpdated() {
375         //  TODO: Clean this up
376         emit updateFeedComplete();
377 }
378
379 void Feed::updateUnread(int i) {
380         bool allRead = (unread == 0);
381
382         unread += i;
383         if(unread <= 0) unread = 0;
384
385         if(allRead != (unread == 0))
386                 emit allReadChanged();
387 }
388
389 void Feed::markRead() {
390         if(unread > 0) {
391                 /* Mark all the remaining items read */
392
393                 QNetworkRequest request;
394                 request.setUrl(reader->markallread_url);
395
396                 buffer.open(QBuffer::ReadWrite | QBuffer::Truncate);
397                 buffer.write("s=");
398                 buffer.write(QUrl::toPercentEncoding(id));
399                 //buffer.write("&ts=");
400                 //buffer.write(QByteArray::number(oldest));
401                 buffer.write("&T=");
402                 buffer.write(QUrl::toPercentEncoding(reader->token));
403
404                 //buffer.seek(0);
405                 //qDebug() << buffer.readAll();
406
407                 buffer.seek(0);
408                 reader->manager.post(request, &buffer);
409
410                 unread = 0;
411
412                 /* Go over all the entries and mark them read */
413                 QHash<QString, Entry *>::iterator i;
414                 for(i = entries.begin(); i != entries.end(); ++i)
415                         i.value()->flags |= ENTRY_FLAG_READ | ENTRY_FLAG_LOCKED;
416         }
417
418         emit allReadChanged();
419 }
420
421 static bool compareEntryItems(const Entry *e1, const Entry *e2) {
422         return e1->published > e2->published;
423 }
424
425 QList<Entry *> Feed::getEntries() {
426         QList<Entry *> list = entries.values();
427         qSort(list.begin(), list.end(), compareEntryItems);
428         return list;
429 }
430
431 /* TODO: Remove the duplicate code in changing stated */
432
433 void Entry::markRead(bool mark_read) {
434         /* Check if the read flag differs from the requested state */
435         if(((flags & ENTRY_FLAG_READ) != 0) == mark_read)
436                 return;
437
438         /* Cannot mark an item unread if it's locked */
439         if((flags & ENTRY_FLAG_LOCKED) && !mark_read)
440                 return;
441
442         QNetworkRequest request;
443         request.setUrl(feed->reader->edittag_url);
444
445         postread.open(QBuffer::ReadWrite | QBuffer::Truncate);
446         postread.write("i=");
447         postread.write(QUrl::toPercentEncoding(id));
448         if(mark_read)
449                 postread.write("&a=");
450         else
451                 postread.write("&r=");
452         postread.write(QUrl::toPercentEncoding("user/-/state/com.google/read"));
453         postread.write("&ac=edit-tags&T=");
454         postread.write(QUrl::toPercentEncoding(feed->reader->token));
455         postread.seek(0);
456         feed->reader->manager.post(request, &postread);
457
458         feed->updateUnread(mark_read ? -1 : 1);
459
460         if(mark_read)
461                 flags |= ENTRY_FLAG_READ;
462         else
463                 flags &= ~ENTRY_FLAG_READ;
464 }
465
466 void Entry::markStar(bool mark_star) {
467         /* Check if the starred flag differs from the requested state */
468         if(((flags & ENTRY_FLAG_STARRED) != 0) == mark_star)
469                 return;
470
471         QNetworkRequest request;
472         request.setUrl(feed->reader->edittag_url);
473
474         poststar.open(QBuffer::ReadWrite | QBuffer::Truncate);
475         poststar.write("i=");
476         poststar.write(QUrl::toPercentEncoding(id));
477         if(mark_star)
478                 poststar.write("&a=");
479         else
480                 poststar.write("&r=");
481         poststar.write(QUrl::toPercentEncoding("user/-/state/com.google/starred"));
482         poststar.write("&ac=edit-tags&T=");
483         poststar.write(QUrl::toPercentEncoding(feed->reader->token));
484         poststar.seek(0);
485         feed->reader->manager.post(request, &poststar);
486
487         Feed *starred = feed->reader->feeds.value("user/-/state/com.google/starred");
488
489         if(mark_star) {
490                 starred->addEntry(this);
491                 flags |= ENTRY_FLAG_STARRED;
492         }
493         else {
494                 starred->delEntry(this);
495                 flags &= ~ENTRY_FLAG_STARRED;
496         }
497 }
498
499 void Entry::markShared(bool mark_shared) {
500         /* Check if the shared flag differs from the requested state */
501         if(((flags & ENTRY_FLAG_SHARED) != 0) == mark_shared)
502                 return;
503
504         QNetworkRequest request;
505         request.setUrl(feed->reader->edittag_url);
506
507         postshared.open(QBuffer::ReadWrite | QBuffer::Truncate);
508         postshared.write("i=");
509         postshared.write(QUrl::toPercentEncoding(id));
510         if(mark_shared)
511                 postshared.write("&a=");
512         else
513                 postshared.write("&r=");
514         postshared.write(QUrl::toPercentEncoding("user/-/state/com.google/broadcast"));
515         postshared.write("&ac=edit-tags&T=");
516         postshared.write(QUrl::toPercentEncoding(feed->reader->token));
517         postshared.seek(0);
518         feed->reader->manager.post(request, &postshared);
519
520         Feed *shared = feed->reader->feeds.value("user/-/state/com.google/broadcast");
521
522         if(mark_shared) {
523                 shared->addEntry(this);
524                 flags |= ENTRY_FLAG_SHARED;
525         }
526         else {
527                 shared->delEntry(this);
528                 flags &= ~ENTRY_FLAG_SHARED;
529         }
530 }