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