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