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