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