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