Initial import of the code.
[grr] / src / googlereader.cpp
diff --git a/src/googlereader.cpp b/src/googlereader.cpp
new file mode 100644 (file)
index 0000000..5919ca4
--- /dev/null
@@ -0,0 +1,552 @@
+#include <QtWebKit>
+#include <QApplication>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QNetworkCookie>
+#include <QBuffer>
+#include <QTimer>
+#include <QDateTime>
+#include <QDomDocument>
+#include <QDebug>
+
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+
+#include "googlereader.h"
+
+void Feed::updateSubscription(Feed *feed) {
+       title = feed->title;
+       sortid = feed->sortid;
+       firstitemmsec = feed->firstitemmsec;
+       cat_id = feed->cat_id;
+       cat_label = feed->cat_label;
+       subscription_updated = true;
+}
+
+void Feed::fetch(bool cont) {
+       QNetworkRequest request;
+       QByteArray ba = "http://www.google.com/reader/atom/";
+
+       ba.append(QUrl::toPercentEncoding(id));
+       QUrl url = QUrl::fromEncoded(ba);
+
+       if(continuation != "" && cont)
+               url.addEncodedQueryItem("c", continuation.toUtf8());
+
+       request.setUrl(url);
+       reader->getManager()->get(request);
+}
+
+GoogleReader::GoogleReader() {
+       connect(&manager, SIGNAL(finished(QNetworkReply*)),
+               SLOT(downloadFinished(QNetworkReply*)));
+
+       SID = NULL;
+       updateSubscriptionsPending = false;
+       updateUnreadPending = false;
+       SIDPending = false;
+
+       login_url.setUrl("https://www.google.com/accounts/ClientLogin");
+       subscriptions_url.setUrl("http://www.google.com/reader/api/0/subscription/list");
+       unread_url.setUrl("http://www.google.com/reader/api/0/unread-count");
+       edittag_url.setUrl("http://www.google.com/reader/api/0/edit-tag?client=-");
+       token_url.setUrl("http://www.google.com/reader/api/0/token");
+       markallread_url.setUrl("http://www.google.com/reader/api/0/mark-all-as-read?client=-");
+
+       /* Add the virtual 'Starred items' feed */
+       Feed *feed = new Feed(this);
+       feed->id = "user/-/state/com.google/starred";
+       feed->title = "Starred items";
+       feed->special = true;
+       feeds.insert(feed->id, feed);
+       connect(feed, SIGNAL(allReadChanged()), SIGNAL(allReadChanged()));
+}
+
+void GoogleReader::downloadFinished(QNetworkReply *reply) {
+       QUrl url = reply->url();
+
+       /* TODO: Instead of comparing against the url, use the signal from the
+        * QNetworkReply... */
+
+       if (reply->error()) {
+               qDebug() << "Download of" << url << "failed:" << qPrintable(reply->errorString());
+               if(url == login_url) {
+                       SIDPending = false;
+                       emit loginFailed("Incorrect username or password");
+               }
+               else if(url == edittag_url)
+                       getToken();
+               return;
+       }
+       else if(url == login_url) {
+               QByteArray data = reply->readAll();
+               data.remove(0, data.indexOf("SID=", 0) + 4);
+               data.remove(data.indexOf("\n", 0), 1024);
+               SID = strdup(data.data());
+
+               qDebug() << "SID:" << SID;
+
+               manager.cookieJar()->setCookiesFromUrl(
+                       QList<QNetworkCookie>() << QNetworkCookie("SID", SID),
+                       QUrl("http://www.google.com"));
+
+               SIDPending = false;
+
+               getToken();
+
+               /* TODO: Replace this with a proper state machine */
+               if(updateSubscriptionsPending) {
+                       updateSubscriptionsPending = false;
+                       updateSubscriptions();
+               }
+       }
+       else if(url == token_url) {
+               token = reply->readAll();
+               qDebug() << "token:" << token;
+       }
+       else if(url == subscriptions_url) {
+               QByteArray data = reply->readAll();
+               QDomDocument dom;
+               dom.setContent(data);
+               parseSubscriptions(dom);
+               emit updateSubscriptionsComplete();
+
+               /* TODO: Replace this with a proper state machine */
+               if(updateUnreadPending) {
+                       updateUnreadPending = false;
+                       updateUnread();
+               }
+       }
+       else if(url == unread_url) {
+               QByteArray data = reply->readAll();
+               QDomDocument dom;
+               dom.setContent(data);
+               parseUnread(dom);
+               emit updateUnreadComplete();
+       }
+       else if(url == edittag_url) {
+               QByteArray data = reply->readAll();
+               //qDebug() << "Result:" << data;
+       }
+       else if(url == markallread_url) {
+               QByteArray data = reply->readAll();
+               //qDebug() << "Result:" << data;
+       }
+       else {
+               QByteArray data = reply->readAll();
+               QDomDocument dom;
+               dom.setContent(data);
+               parseFeed(dom);
+       }
+
+       reply->deleteLater();
+}
+
+void GoogleReader::parseFeed(QDomDocument dom) {
+       QDomElement set, e;
+       QDomNode n, c;
+       QString continuation, feedsource;
+       Feed *feed = NULL;
+
+       set = dom.firstChildElement();
+
+       for(n = set.firstChild(); !n.isNull(); n = n.nextSibling()) {
+               e = n.toElement();
+               QString name = e.tagName();
+               if(name == "entry") {
+                       Entry entry;
+                       QString content, summary;
+
+                       QString locked = e.attribute("gr:is-read-state-locked", "false");
+                       if(locked == "true")
+                               entry.flags |= ENTRY_FLAG_LOCKED | ENTRY_FLAG_READ;
+
+                       entry.crawled = e.attribute("gr:crawl-timestamp-msec", "0").toULongLong();
+
+                       for(c = n.firstChild(); !c.isNull(); c = c.nextSibling()) {
+                               e = c.toElement();
+                               name = e.tagName();
+                               if(name == "id")
+                                       entry.id = e.text();
+                               else if(name == "title") {
+                                       QWebPage p;
+                                       p.mainFrame()->setHtml(e.text());
+                                       entry.title = p.mainFrame()->toPlainText();
+                               }
+                               else if(name == "published")
+                                       entry.published = QDateTime::fromString(e.text(), "yyyy-MM-dd'T'HH:mm:ss'Z'");
+                               else if(name == "link")
+                                       entry.link = QUrl(e.attribute("href", ""));
+                               else if(name == "source")
+                                       entry.source = e.attribute("gr:stream-id", "");
+                               else if(name == "content")
+                                       content = e.text();
+                               else if(name == "summary")
+                                       summary = e.text();
+                               else if(name == "author") {
+                                       e = c.firstChild().toElement();
+                                       entry.author = e.text();
+                                       if(entry.author == "(author unknown)")
+                                               entry.author = "";
+                               }
+                               else if(name == "category") {
+                                       QString label = e.attribute("label", "");
+                                       if(label == "read")
+                                               entry.flags |= ENTRY_FLAG_READ;
+                                       else if(label == "starred")
+                                               entry.flags |= ENTRY_FLAG_STARRED;
+                               }
+                       }
+
+                       if(content != "")
+                               entry.content = content;
+                       else if(summary != "")
+                               entry.content = summary;
+
+                       if(!feed)
+                               feed = feeds.value(feedsource == "" ? entry.source : feedsource);
+
+                       if(feed) {
+                               entry.feed = feed;
+                               feed->addEntry(new Entry(entry));
+                       }
+               }
+               else if(name == "gr:continuation") {
+                       continuation = e.text();
+               }
+               else if(name == "id") {
+                       if(e.text().endsWith("/state/com.google/starred"))
+                               feedsource = "user/-/state/com.google/starred";
+               }
+       }
+
+       if(feed) {
+               feed->lastUpdated = QDateTime::currentDateTime();
+               feed->continuation = continuation;
+               feed->signalUpdated();
+       }
+}
+
+void GoogleReader::parseSubscriptions(QDomDocument dom) {
+       QDomElement set, e;
+       QDomNode n, c;
+
+       /* Clear the subscription updated flag */
+       QHash<QString, Feed *>::iterator i;
+       for(i = feeds.begin(); i != feeds.end(); ++i)
+               i.value()->subscription_updated = false;
+
+       set = dom.firstChildElement();
+       set = set.firstChildElement("list");
+       set = set.firstChildElement("object");
+
+       for (; !set.isNull(); set = set.nextSiblingElement("object")) {
+               Feed *feed = new Feed(this);
+               Feed *existing_feed;
+
+               for(n = set.firstChild(); !n.isNull(); n = n.nextSibling()) {
+                       e = n.toElement();
+                       QString name = e.attribute("name");
+                       if(name == "id")
+                               feed->id = e.text();
+                       else if(name == "title")
+                               feed->title = e.text();
+                       else if(name == "sortid")
+                               feed->sortid = e.text();
+                       else if(name == "firstitemmsec")
+                               feed->firstitemmsec = e.text();
+                       else if(name == "categories") {
+                               for(c = n.firstChild().firstChild(); !c.isNull(); c = c.nextSibling()) {
+                                       e = c.toElement();
+                                       QString name = e.attribute("name");
+                                       if(name == "id")
+                                               feed->cat_id = e.text();
+                                       else if(name == "label")
+                                               feed->cat_label = e.text();
+                               }
+                       }
+               }
+
+               existing_feed = feeds.value(feed->id);
+               if(existing_feed) {
+                       existing_feed->updateSubscription(feed);
+                       delete(feed);
+                       feed = existing_feed;
+
+               }
+               else {
+                       feed->subscription_updated = true;
+                       feeds.insert(feed->id, feed);
+                       connect(feed, SIGNAL(allReadChanged()), SIGNAL(allReadChanged()));
+               }
+       }
+
+       /* Delete feeds no longer subscribed to  */
+       for(i = feeds.begin(); i != feeds.end(); ++i) {
+               if(i.value()->subscription_updated == false && i.value()->special == false) {
+                       printf("DELETED: %s\n", i.value()->title.toLatin1().data());
+                       i = feeds.erase(i);
+               }
+       }
+
+       lastUpdated = QDateTime::currentDateTime();
+}
+
+void GoogleReader::parseUnread(QDomDocument dom) {
+       QDomElement set, e;
+       QDomNode n, c;
+
+       set = dom.firstChildElement();
+       set = set.firstChildElement("list");
+       set = set.firstChildElement("object");
+
+       for (; !set.isNull(); set = set.nextSiblingElement("object")) {
+               QString id;
+               int count = 0;
+               ulong newestitem = 0;
+
+               for(n = set.firstChild(); !n.isNull(); n = n.nextSibling()) {
+                       e = n.toElement();
+                       QString name = e.attribute("name");
+                       if(name == "id")
+                               id = e.text();
+                       else if(name == "count")
+                               count =  e.text().toInt();
+                       else if(name == "newestItemTimestampUsec")
+                               newestitem = e.text().toULong();
+               }
+
+               Feed *f = feeds.value(id);
+               if(f) {
+                       f->unread = count;
+                       f->newestitem = newestitem;
+                       //qDebug() << f->title << "->" << count;
+               }
+               else {
+                       //printf("%s not found\n", id.toLatin1().data());
+               }
+       }
+
+       lastUpdated = QDateTime::currentDateTime();
+}
+
+void GoogleReader::getSID() {
+
+       if(SIDPending)
+               return;
+
+       SIDPending = true;
+
+       QNetworkRequest request;
+       request.setUrl(login_url);
+
+       buffer.open(QBuffer::ReadWrite | QBuffer::Truncate);
+       buffer.write("Email=");
+       buffer.write(QUrl::toPercentEncoding(login));
+       buffer.write("&Passwd=");
+       buffer.write(QUrl::toPercentEncoding(passwd));
+       buffer.write("&service=reader&source=grms&continue=http://www.google.com");
+
+       //buffer.seek(0);
+       //qDebug() << buffer.readAll();
+
+       buffer.seek(0);
+       manager.post(request, &buffer);
+}
+
+void GoogleReader::getToken() {
+       QNetworkRequest request;
+       request.setUrl(token_url);
+       manager.get(request);
+}
+
+void GoogleReader::updateSubscriptions() {
+       QNetworkRequest request;
+
+       if(updateSubscriptionsPending)
+               return;
+
+       if(!SID) {
+               updateSubscriptionsPending = true;
+               getSID();
+               return;
+       }
+
+       request.setUrl(subscriptions_url);
+       manager.get(request);
+}
+
+void GoogleReader::updateUnread() {
+       QNetworkRequest request;
+
+       if(updateUnreadPending)
+               return;
+
+       if(!SID) {
+               updateUnreadPending = true;
+               getSID();
+               return;
+       }
+
+       request.setUrl(unread_url);
+       manager.get(request);
+}
+
+static bool compareFeedItems(const Feed *f1, const Feed *f2) {
+       if(f1->special && !f2->special)
+               return true;
+
+       if(f2->special && !f1->special)
+               return false;
+
+       if(f1->cat_label == f2->cat_label)
+               return f1->title.toLower() < f2->title.toLower();
+
+       if(f1->cat_label.length() == 0)
+               return false;
+
+       if(f2->cat_label.length() == 0)
+               return true;
+
+       return f1->cat_label.toLower() < f2->cat_label.toLower();
+}
+
+QList<Feed *> GoogleReader::getFeeds() {
+       QList<Feed *> list = feeds.values();
+       qSort(list.begin(), list.end(), compareFeedItems);
+       return list;
+}
+
+void Feed::addEntry(Entry *entry) {
+       entries.insert(entry->id, entry);
+}
+
+void Feed::delEntry(Entry *entry) {
+       entries.remove(entry->id);
+}
+
+void Feed::signalUpdated() {
+       //  TODO: Clean this up
+       emit updateFeedComplete();
+}
+
+void Feed::updateUnread(int i) {
+       bool allRead = (unread == 0);
+
+       unread += i;
+       if(unread <= 0) unread = 0;
+
+       if(allRead != (unread == 0))
+               emit allReadChanged();
+}
+
+void Feed::markRead() {
+       if(unread > 0) {
+               /* Mark all the remaining items read */
+
+               QNetworkRequest request;
+               request.setUrl(reader->markallread_url);
+
+               buffer.open(QBuffer::ReadWrite | QBuffer::Truncate);
+               buffer.write("s=");
+               buffer.write(QUrl::toPercentEncoding(id));
+               //buffer.write("&ts=");
+               //buffer.write(QByteArray::number(oldest));
+               buffer.write("&T=");
+               buffer.write(QUrl::toPercentEncoding(reader->token));
+
+               //buffer.seek(0);
+               //qDebug() << buffer.readAll();
+
+               buffer.seek(0);
+               reader->manager.post(request, &buffer);
+
+               unread = 0;
+
+               /* Go over all the entries and mark them read */
+               QHash<QString, Entry *>::iterator i;
+               for(i = entries.begin(); i != entries.end(); ++i)
+                       i.value()->flags |= ENTRY_FLAG_READ | ENTRY_FLAG_LOCKED;
+       }
+
+       emit allReadChanged();
+}
+
+static bool compareEntryItems(const Entry *e1, const Entry *e2) {
+       return e1->published > e2->published;
+}
+
+QList<Entry *> Feed::getEntries() {
+       QList<Entry *> list = entries.values();
+       qSort(list.begin(), list.end(), compareEntryItems);
+       return list;
+}
+
+/* TODO: Remove the duplicate code in changing stated */
+
+void Entry::markRead(bool mark_read) {
+       /* Check if the read flag differs from the requested state */
+       if(((flags & ENTRY_FLAG_READ) != 0) == mark_read)
+               return;
+
+       /* Cannot mark an item unread if it's locked */
+       if((flags & ENTRY_FLAG_LOCKED) && !mark_read)
+               return;
+
+       QNetworkRequest request;
+       request.setUrl(feed->reader->edittag_url);
+
+       postread.open(QBuffer::ReadWrite | QBuffer::Truncate);
+       postread.write("i=");
+       postread.write(QUrl::toPercentEncoding(id));
+       if(mark_read)
+               postread.write("&a=");
+       else
+               postread.write("&r=");
+       postread.write(QUrl::toPercentEncoding("user/-/state/com.google/read"));
+       postread.write("&ac=edit-tags&T=");
+       postread.write(QUrl::toPercentEncoding(feed->reader->token));
+       postread.seek(0);
+       feed->reader->manager.post(request, &postread);
+
+       feed->updateUnread(mark_read ? -1 : 1);
+
+       if(mark_read)
+               flags |= ENTRY_FLAG_READ;
+       else
+               flags &= ~ENTRY_FLAG_READ;
+}
+
+void Entry::markStar(bool mark_star) {
+       /* Check if the starred flag differs from the requested state */
+       if(((flags & ENTRY_FLAG_STARRED) != 0) == mark_star)
+               return;
+
+       QNetworkRequest request;
+       request.setUrl(feed->reader->edittag_url);
+
+       poststar.open(QBuffer::ReadWrite | QBuffer::Truncate);
+       poststar.write("i=");
+       poststar.write(QUrl::toPercentEncoding(id));
+       if(mark_star)
+               poststar.write("&a=");
+       else
+               poststar.write("&r=");
+       poststar.write(QUrl::toPercentEncoding("user/-/state/com.google/starred"));
+       poststar.write("&ac=edit-tags&T=");
+       poststar.write(QUrl::toPercentEncoding(feed->reader->token));
+       poststar.seek(0);
+       feed->reader->manager.post(request, &poststar);
+
+       Feed *starred = feed->reader->feeds.value("user/-/state/com.google/starred");
+
+       if(mark_star) {
+               starred->addEntry(this);
+               flags |= ENTRY_FLAG_STARRED;
+       }
+       else {
+               starred->delEntry(this);
+               flags &= ~ENTRY_FLAG_STARRED;
+       }
+}