Skip to content

Instantly share code, notes, and snippets.

@dongyuwei
Created March 14, 2012 04:58
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save dongyuwei/2034195 to your computer and use it in GitHub Desktop.
Save dongyuwei/2034195 to your computer and use it in GitHub Desktop.
google reader api for nodejs
/*
This library was developed by Will Honey.
It is licensed under the GPLv3 Open Source License
This library requires the underscore library found at http://documentcloud.github.com/underscore/
This library requires the underscore string library found at http://edtsech.github.com/underscore.string/
This library requires the support of localStorage. Updates could be easily made to change that.
*/
/* jslint adsafe: false, devel: true, regexp: true, browser: true, vars: true, nomen: true, maxerr: 50, indent: 4 */
/* global localStorage, window, reader, _ */
var window = {}; ( function() {"use strict";
//hack start-----------------------------------------------------------
//npm install -g underscore
//npm install -g underscore.string
//npm install -g xmlhttprequest
var _ = require('underscore');
// Import Underscore.string to separate object, because there are conflict functions (include, reverse, contains)
_.str = require('underscore.string');
// Mix in non-conflict functions to Underscore namespace if you want
_.mixin(_.str.exports());
// All functions, include conflict, will be available through _.str object
_.str.include('Underscore.string', 'string'); // => true
var localStorage = {};
var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
var reader = window.reader = {};
//hack end--------------------------------------------------------------
//global constants that will likely be used outside of this file
reader.TAGS = {
"like" : "user/-/state/com.google/like",
"label" : "user/-/label/",
"star" : "user/-/state/com.google/starred",
"read" : "user/-/state/com.google/read",
"fresh" : "user/-/state/com.google/fresh",
"share" : "user/-/state/com.google/broadcast",
"kept-unread" : "user/-/state/com.google/kept-unread",
"reading-list" : "user/-/state/com.google/reading-list"
};
//global variables
reader.is_logged_in = false;
reader.is_initialized = false;
reader.has_loaded_prefs = false;
//constants that will only be used in this file
var CLIENT = "Tibfib",
//base urls
LOGIN_URL = "https://www.google.com/accounts/ClientLogin", BASE_URL = "http://www.google.com/reader/api/0/",
//url paths
PREFERENCES_PATH = "preference/stream/list", STREAM_PATH = "stream/contents/", SUBSCRIPTIONS_PATH = "subscription/", LABEL_PATH = "user/-/label/", TAGS_PATH = "tag/",
//url actions
LIST_SUFFIX = "list", EDIT_SUFFIX = "edit", MARK_ALL_READ_SUFFIX = "mark-all-as-read", TOKEN_SUFFIX = "token", USERINFO_SUFFIX = "user-info", UNREAD_SUFFIX = "unread-count", RENAME_LABEL_SUFFIX = "rename-tag", EDIT_TAG_SUFFIX = "edit-tag";
//managing the feeds
var readerFeeds = [];
reader.setFeeds = function(feeds) {
readerFeeds = feeds;
};
reader.getFeeds = function() {
return readerFeeds;
};
reader.getLabels = function() {
return _(reader.getFeeds()).select(function(feed) {
return feed.isLabel;
});
};
//managing the logged in user
reader.setUser = function(user) {
localStorage.User = JSON.stringify(user);
};
reader.getUser = function() {
return JSON.parse(localStorage.User);
};
//managing the app authentication
var readerAuth = "", readerToken = "";
reader.getAuth = function() {
if(readerAuth !== "undefined") {
return readerAuth;
}
};
reader.setAuth = function(auth) {
readerAuth = auth;
};
//the core ajax function
var requests = [], makeRequest = function(obj, noAuth) {
//make sure we have a method
if(!obj.method) {
obj.method = "GET";
}
//make sure we have a parameters object
if(!obj.parameters) {
obj.parameters = {};
}
//add the necessary parameters to get our requests to function properly
if(obj.method === "GET") {
obj.parameters.ck = Date.now() || new Date().getTime();
obj.parameters.accountType = "GOOGLE";
obj.parameters.service = "reader";
obj.parameters.output = "json";
obj.parameters.client = CLIENT;
}
//if we have a token, add it to the parameters
if(readerToken) {
if(obj.method === "POST") {
//it seems that "GET" requests don't care about your token
obj.parameters.T = readerToken;
}
}
//turn our parameters object into a query string
var queries = [], key, queryString;
for(key in obj.parameters) {
if(obj.parameters.hasOwnProperty(key)) {
queries.push(encodeURIComponent(key) + "=" + encodeURIComponent(obj.parameters[key]));
}
}
queryString = queries.join("&");
//for get requests, attach the queryString
//for post requests, attach just the client constant
var url = (obj.method === "GET") ? (obj.url + "?" + queryString) : (obj.url + "?" + encodeURIComponent("client") + "=" + encodeURIComponent(CLIENT));
var request = new XMLHttpRequest();
request.open(obj.method, url, true);
//set request header
request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
if(reader.getAuth() && !noAuth) {
//this one is important. This is how google does authorization.
request.setRequestHeader("Authorization", "GoogleLogin auth=" + reader.getAuth());
}
var requestIndex = requests.length;
request.onreadystatechange = function() {
if((request.readyState === 4) && request.status === 200) {
if(obj.onSuccess) {
obj.onSuccess(request);
if(requests[requestIndex]) {
delete requests[requestIndex];
}
}
} else if(request.readyState === 4) {
if(obj.method === "POST") {
if(!obj.tried) {
//If it failed and this is a post request, try getting a new token, then do the request again
reader.getToken(function() {
obj.tried = true;
makeRequest(obj);
if(requests[requestIndex]) {
delete requests[requestIndex];
}
}, obj.onFailure);
}
} else {
if(obj.onFailure) {
obj.onFailure(request);
if(requests[requestIndex]) {
delete requests[requestIndex];
}
}
}
if(request.status === 401 && request.statusText === "Unauthorized") {
//Humane is a notification lib.
if(humane) {
humane(request.statusText + ". " + "Try logging in again.");
}
}
console.error(request);
}
};
request.send((obj.method === "POST") ? queryString : "");
requests.push(request);
};
// *************************************
// *
// * Authentication
// *
// *************************************
reader.load = function() {
reader.is_logged_in = false;
reader.is_initialized = true;
//check storage for the tokens we need.
if(localStorage.Auth && localStorage.Auth !== "undefined") {
reader.setAuth(localStorage.Auth);
reader.is_logged_in = true;
}
return (reader.is_logged_in);
};
reader.login = function(email, password, successCallback, failCallback) {
if(email.length === 0 || password.length === 0) {
failCallback("Blank Info...");
}
makeRequest({
method : "GET",
url : LOGIN_URL,
parameters : {
Email : email,
Passwd : password
},
onSuccess : function(transport) {
localStorage.Auth = _(transport.responseText).lines()[2].replace("Auth=", "");
reader.load();
getUserInfo(successCallback);
},
onFailure : function(transport) {
console.error(transport);
failCallback(reader.normalizeError(transport.responseText));
}
});
};
reader.logout = function() {
reader.is_logged_in = false;
localStorage.Auth = undefined;
reader.setUser({});
reader.setAuth("");
reader.setFeeds([]);
};
var getUserInfo = function(successCallback, failCallback) {
makeRequest({
method : "GET",
url : BASE_URL + USERINFO_SUFFIX,
parameters : {},
onSuccess : function(transport) {
reader.setUser(JSON.parse(transport.responseText));
successCallback();
},
onFailure : function(transport) {
console.error(transport);
if(failCallback) {
failCallback(reader.normalizeError(transport.responseText));
}
}
});
};
var getUserPreferences = function(successCallback, failCallback) {
makeRequest({
method : "GET",
url : BASE_URL + PREFERENCES_PATH,
parameters : {},
onSuccess : function(transport) {
reader.has_loaded_prefs = true;
reader.userPrefs = JSON.parse(transport.responseText).streamprefs;
if(successCallback) {
successCallback();
}
},
onFailure : function(transport) {
console.error(transport);
if(failCallback) {
failCallback(reader.normalizeError(transport.responseText));
}
}
});
};
//Get the token
reader.getToken = function(successCallback, failCallback) {
makeRequest({
method : "GET",
url : BASE_URL + TOKEN_SUFFIX,
parameters : {},
onSuccess : function(transport) {
readerToken = transport.responseText;
successCallback();
},
onFailure : function(transport) {
console.error("failed", transport);
if(failCallback) {
failCallback(reader.normalizeError(transport.responseText));
}
}
});
};
// *************************************
// *
// * Loading Feeds
// *
// *************************************
//Get the user's subscribed feeds
reader.loadFeeds = function(successCallback) {
function loadFeeds() {
makeRequest({
method : "GET",
url : BASE_URL + SUBSCRIPTIONS_PATH + LIST_SUFFIX,
onSuccess : function(transport) {
//save feeds in an organized state.
loadTags(function(tags) {
//get unread counts
reader.getUnreadCounts(function(unreadcounts) {
//organize and save feeds
reader.setFeeds(organizeFeeds(JSON.parse(transport.responseText).subscriptions, tags, unreadcounts, reader.userPrefs));
//callback with our feeds
successCallback(reader.getFeeds());
});
});
},
onFailure : function(transport) {
console.error(transport);
}
});
}
if(reader.has_loaded_prefs) {
loadFeeds();
} else {
getUserPreferences(loadFeeds);
}
};
var loadTags = function(successCallback) {
makeRequest({
method : "GET",
url : BASE_URL + TAGS_PATH + LIST_SUFFIX,
onSuccess : function(transport) {
//save feeds in an organized state.
successCallback(JSON.parse(transport.responseText).tags);
},
onFailure : function(transport) {
console.error(transport);
}
});
};
//organizes feeds based on categories/labels.
//this function is ridiculous. like really, holy crap.
var organizeFeeds = function(subscriptions, tags, unreadCounts, userPrefs) {
var uncategorized = [];
//prepare tags
tags.unshift({
title : "All",
id : reader.TAGS["reading-list"],
feeds : subscriptions,
isAll : true,
isSpecial : true
});
tags.pop();
//remove "user/-/state/com.blogger/blogger-following". not exactly future friendly *shrug*
var tagTitleRegExp = /[^\/]+$/i;
_(tags).each(function(tag) {
//give tags a .title
if(!tag.title) {
tag.title = tagTitleRegExp.exec(tag.id)[0];
}
//based on title add unique properties
if(tag.title === "starred") {
tag.title = _(tag.title).capitalize();
tag.isSpecial = true;
} else if(tag.title === "broadcast") {
tag.title = "Shared";
tag.isSpecial = true;
} else if(!tag.isSpecial) {
tag.isLabel = true;
}
tag.feeds = [];
//remove digits from the id
tag.id = reader.correctId(tag.id);
//apply unreadCounts
_(unreadCounts).each(function(unreadCount) {
unreadCount.id = reader.correctId(unreadCount.id);
if(tag.id === unreadCount.id) {
tag.count = unreadCount.count;
tag.newestItemTimestamp = unreadCount.newestItemTimestampUsec;
}
});
});
//process subscriptions
_(subscriptions).each(function(sub) {
//give isFeed property, useful for identifying
sub.isFeed = true;
//replace digits from the id
sub.id = reader.correctId(sub.id);
//apply unread counts
_(unreadCounts).each(function(unreadCount) {
if(sub.id === unreadCount.id) {
sub.count = unreadCount.count;
sub.newestItemTimestamp = unreadCount.newestItemTimestampUsec;
}
});
if(sub.categories.length === 0) {
//if the subscription has no categories, push it onto the uncategorized array
uncategorized.push(sub);
} else {
//otherwise find the category from the tags array and push the sub into its feeds array
_(sub.categories).each(function(tag) {
tag.id = reader.correctId(tag.id);
_(tags).each(function(fullTag) {
if(tag.id === fullTag.id) {
var sub_clone = _(sub).clone();
sub_clone.inside = fullTag.id;
fullTag.feeds.push(sub_clone);
}
});
});
}
});
//replace digits
_(userPrefs).each(function(value, key) {
if(/user\/\d*\//.test(key)) {
userPrefs[reader.correctId(key)] = value;
}
});
//remove tags with no feeds
var tagsWithFeeds = _(tags).reject(function(tag) {
return (tag.feeds.length === 0 && !tag.isSpecial);
});
//order the feeds within tags
_(tagsWithFeeds).each(function(tag) {
//get the ordering id based on the userPrefs
var orderingId = _(userPrefs[tag.id]).detect(function(setting) {
return (setting.id === "subscription-ordering");
});
if(orderingId) {
tag.feeds = _(tag.feeds).sortBy(function(feed) {
if(orderingId.value.indexOf(feed.sortid) === -1) {
//if our sortid isn't there, the feed should be at the back.
return 1000;
}
//return the index of our feed sortid, which will be in multiples of 8 since sortid's are 8 characters long.
return (orderingId.value.indexOf(feed.sortid)) / 8;
});
} //there might be another setting we should follow like "alphabetical" or "most recent". Just a guess.
/*else {
tag.feeds.sort();
}*/
});
//now order ALL feeds and tags
var orderingId = _(userPrefs["user/-/state/com.google/root"]).detect(function(setting) {
return (setting.id === "subscription-ordering");
}) || {
value : ""
};
//our feeds are our tagsWithFeeds + our uncategorized subscriptions
var feeds = [].concat(tagsWithFeeds, uncategorized);
//sort them by sortid
feeds = _(feeds).sortBy(function(feed) {
if(orderingId.value.indexOf(feed.sortid) === -1 && !feed.isSpecial) {
return 1000;
}
return (orderingId.value.indexOf(feed.sortid)) / 8;
});
return feeds;
};
//get unread counts from google reader
//passing true as the second arg gets you an object, extremely useful for notifications
reader.getUnreadCounts = function(successCallback, returnObject) {
makeRequest({
url : BASE_URL + UNREAD_SUFFIX,
onSuccess : function(transport) {
var unreadCounts = JSON.parse(transport.responseText).unreadcounts;
//console.log(transport);
var unreadCountsObj = {};
_(unreadCounts).each(function(obj) {
unreadCountsObj[reader.correctId(obj.id)] = obj.count;
});
reader.unreadCountsObj = unreadCountsObj;
if(returnObject) {
successCallback(unreadCountsObj);
} else {
successCallback(unreadCounts);
}
},
onFailure : function(transport) {
console.error(transport);
}
});
};
//this is a function so we can reduce the amount of ajax calls when setting an article as read. Just manually decrement the counts, don't request new numbers.
reader.decrementUnreadCount = function(feedId, callback) {
_.each(reader.getFeeds(), function(subscription) {
if(subscription.id === feedId || (subscription.isAll)) {
subscription.count -= 1;
} else if(subscription.feeds && subscription.feeds.length > 0) {
_.each(subscription.feeds, function(feed) {
if(feed.id === feedId) {
subscription.count -= 1;
}
});
}
});
callback();
};
// *************************************
// *
// * Editing Feeds
// *
// *************************************
var editFeed = function(params, successCallback) {
if(!params) {
console.error("No params for feed edit");
return;
}
makeRequest({
method : "POST",
url : BASE_URL + SUBSCRIPTIONS_PATH + EDIT_SUFFIX,
parameters : params,
onSuccess : function(transport) {
successCallback(transport.responseText);
},
onFailure : function(transport) {
console.error(transport);
}
});
};
//edit feed title
reader.editFeedTitle = function(feedId, newTitle, successCallback) {
editFeed({
ac : "edit",
t : newTitle,
s : feedId
}, successCallback);
};
reader.editFeedLabel = function(feedId, label, opt, successCallback) {
var obj = {
ac : "edit",
s : feedId
};
if(opt) {
obj.a = label;
} else {
obj.r = label;
}
editFeed(obj, successCallback);
};
reader.editLabelTitle = function(labelId, newTitle, successCallback) {
makeRequest({
method : "POST",
url : BASE_URL + RENAME_LABEL_SUFFIX,
parameters : {
s : LABEL_PATH + labelId,
t : labelId,
dest : LABEL_PATH + newTitle
},
onSuccess : function(transport) {
successCallback(transport.responseText);
},
onFailure : function(transport) {
console.error(transport);
}
});
};
reader.markAllAsRead = function(feedOrLabelId, successCallback) {
//feed or label
makeRequest({
method : "POST",
url : BASE_URL + MARK_ALL_READ_SUFFIX,
parameters : {
s : feedOrLabelId
},
onSuccess : function(transport) {
successCallback(transport.responseText);
},
onFailure : function(transport) {
console.error(transport);
}
});
};
// *************************************
// *
// * Adding a Feed
// *
// *************************************
reader.unsubscribeFeed = function(feedId, successCallback) {
editFeed({
ac : "unsubscribe",
s : feedId
}, successCallback);
};
reader.subscribeFeed = function(feedUrl, successCallback, title) {
editFeed({
ac : "subscribe",
s : "feed/" + feedUrl,
t : title || undefined
}, successCallback);
};
var readerUrlRegex = /(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?\^=%&:\/~\+#]*[\w\-\@?\^=%&\/~\+#])?/;
reader.processFeedInput = function(input, inputType, successCallback, failCallback) {
var url = "https://ajax.googleapis.com/ajax/services/feed/";
if((readerUrlRegex.test(input) || inputType === "url") && inputType !== "keyword") {
url += "load";
} else {
url += "find";
//replace the .com, or .net from the input, since our search doesn't like that
input = input.replace(/\.\w{1,3}\.*\w{0,2}$/ig, "");
}
makeRequest({
url : url,
parameters : {
q : encodeURI(input),
v : "1.0"
},
onSuccess : function(transport) {
var response = JSON.parse(transport.responseText);
if(response.responseStatus === 200) {
if(response.responseData.entries) {
successCallback(response.responseData.entries, "keyword");
} else {
successCallback(response.responseData.feed, "url");
}
} else {
failCallback(response.responseDetails);
}
},
onFailure : function(transport) {
console.error(transport);
}
}, true);
};
// *************************************
// *
// * Loading Items
// *
// *************************************
reader.getItems = function(feedUrl, successCallback, opts) {
var params = opts || {
n : 50
};
params.r = "d";
makeRequest({
method : "GET",
url : BASE_URL + STREAM_PATH + encodeURIComponent(feedUrl),
parameters : params, /*{
//ot=[unix timestamp] : The time from which you want to retrieve items. Only items that have been crawled by Google Reader after this time will be returned.
//r=[d|n|o] : Sort order of item results. d or n gives items in descending date order, o in ascending order.
//xt=[exclude target] : Used to exclude certain items from the feed. For example, using xt=user/-/state/com.google/read will exclude items that the current user has marked as read, or xt=feed/[feedurl] will exclude items from a particular feed (obviously not useful in this request, but xt appears in other listing requests).
},*/
onSuccess : function(transport) {
successCallback(JSON.parse(transport.responseText).items);
},
onFailure : function(transport) {
console.error(transport);
}
});
};
// *************************************
// *
// * Editing Items
// *
// *************************************
reader.setItemTag = function(feed, item, tag, add, successCallback) {
//feed/label id
//item id
//tag in simple form: "like", "read", "share", "label", "star", "kept-unread"
//add === true, or add === false
var params = {
s : feed,
i : item,
async : "true",
ac : "edit-tags"
};
if(add === true) {
params.a = reader.TAGS[tag];
} else {
params.r = reader.TAGS[tag];
}
makeRequest({
method : "POST",
url : BASE_URL + EDIT_TAG_SUFFIX,
parameters : params,
onSuccess : function(transport) {
if(transport.responseText === "OK") {
successCallback(transport.responseText);
}
},
onFailure : function(transport) {
console.error(transport);
}
});
};
// *************************************
// *
// * Useful Utilities
// *
// *************************************
//this function replaces the number id with a dash. Helpful for comparison
var readerIdRegExp = /user\/\d*\//;
reader.correctId = function(id) {
return id.replace(readerIdRegExp, "user\/-\/");
};
//returns url for image to use in the icon
reader.getIconForFeed = function(feedUrl) {
return "http://www.google.com/s2/favicons?domain_url=" + encodeURIComponent(feedUrl);
};
//normalizes error response for logging in
reader.normalizeError = function(inErrorResponse) {
return _(inErrorResponse).lines()[0].replace("Error=", "").replace(/(\w)([A-Z])/g, "$1 $2");
};
}());
//just for test
console.log(window.reader);
window.reader.login('email_name', 'password', function() {
console.log('succ: ',arguments);
window.reader.getUnreadCounts(function(){
console.log('getUnreadCounts: ',arguments);
},true);
}, function() {
console.log('fail: ',arguments);
});
@booyaa
Copy link

booyaa commented Mar 28, 2012

just want to say thank you for nodejs-ifying this! you saved me a buncha work!

@Unitech
Copy link

Unitech commented Jul 8, 2012

awesome. Thank you very much

@haxorjim
Copy link

haxorjim commented Nov 8, 2012

I'm having some trouble getting the setItemTag function to work. Anyone have an example of what the parameters of the call should look like?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment