Introduction
This article starts where the previous one (Consuming Google (Reader) with .NET: Part 1 - Authentication) stopped.
This is an implementation of Google Reader in .NET.
Before getting started, it's important to know that there is a site with plenty of information on the Google Reader API: http://code.google.com/p/pyrfeed/wiki/GoogleReaderAPI. But what we'll also be doing is using Fiddler to reverse engineer the Google Reader API.
Reverse Engineering
When you visit Google Reader via your browser, you're basically always sending GET
and POST
requests. Using Fiddler, you can then view the content of those requests.
Let's say we want to subscribe to http://sandrinodimattia.net/Blog. When you're logged in to Google Reader, you can press the Add Subscription button, enter the URL and press Add. If you do this while Fiddler is open, you'll see the following:
After pressing the Add button, you can see that the URL /reader/api/0/subscription/quickadd was visited and 2 fields were posted (quickadd
and T
). And for each available action in Google Reader, you can use Fiddler to view the URL and the post fields that are hidden underneath.
If you take a closer look at the screenshot, you see a field called T
, this is a token. It identifies your session, but expires quickly. That's why you'll see that our code requests a new token for each new request.
Adding POST to GoogleSession
In the last article, we created the GoogleSession
class. This class helps us in getting data from Google. But now that we have to make POST
requests, we'll also need to send data to Google. That's why we'll add the following method to our GoogleSession
class:
public void PostRequest(string url, params GoogleParameter[] postFields)
{
string formattedParameters = string.Empty;
foreach (var par in postFields.Where(o => o != null))
formattedParameters += string.Format("{0}={1}&", par.Name, par.Value);
formattedParameters = formattedParameters.TrimEnd('&');
formattedParameters += String.Format("&{0}={1}", "T", GetToken());
ASCIIEncoding ascii = new ASCIIEncoding();
byte[] encodedPostData = ascii.GetBytes(
String.Format(formattedParameters));
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.ContentLength = encodedPostData.Length;
request.Headers.Add("Authorization", "GoogleLogin auth=" + auth);
using (Stream newStream = request.GetRequestStream())
newStream.Write(encodedPostData, 0, encodedPostData.Length);
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
if (response.StatusCode != HttpStatusCode.OK)
throw new LoginFailedException(
response.StatusCode, response.StatusDescription);
}
This function uses a URL and GoogleParameters
to build a POST
request. And the token we talked about is also automatically included in the post fields. Thanks to this function, we'll be able to send POST
requests to Google Reader with ease.
The GoogleService Base Class
Before really getting started, we'll just go and create a base class we might want to re-use when implementing other Google services. It encapsulates the authentication mechanism and the GoogleSession
class.
public abstract class GoogleService : IDisposable
{
protected GoogleSession session;
protected GoogleService
(string service, string username, string password, string source)
{
string auth = ClientLogin.GetAuthToken(service, username, password, source);
this.session = new GoogleSession(auth);
}
public void Dispose()
{
if (session != null)
session.Dispose();
}
}
Item Types
When reading from Google Reader, you'll be working with 2 different types of data. Pure XML and syndication items. Syndication items (SyndicationItem
in .NET) are actually items that come from a feed. For example, if you visit this site you'll get a list of your read items in the shape of an atom feed: http://www.google.com/reader/atom/user/-/state/com.google/read. Each item in this feed is a SyndicationItem
.
For both types, we'll be using some basic base classes:
public abstract class GoogleSyndicationItem
{
internal GoogleSyndicationItem(SyndicationItem item)
{
if (item != null)
{
LoadItem(item);
}
}
protected abstract void LoadItem(SyndicationItem item);
public string GetTextSyndicationContent(SyndicationContent content)
{
TextSyndicationContent txt = content as TextSyndicationContent;
if (txt != null)
return txt.Text;
else
return "";
}
}
public abstract class GoogleXmlItem : SyndicationItem
{
internal GoogleXmlItem(XElement item)
{
if (item != null)
{
LoadItem(item);
}
}
protected abstract void LoadItem(XElement item);
protected IEnumerable<XElement> GetDescendants
(XElement item, string descendant, string attribute, string attributeValue)
{
return item.Descendants(descendant).Where(o => o.Attribute(attribute) != null &&
o.Attribute(attribute).Value == attributeValue);
}
protected XElement GetDescendant(XElement item, string descendant,
string attribute, string attributeValue)
{
return GetDescendants(item, descendant, attribute, attributeValue).First();
}
protected string GetDescendantValue
(XElement item, string descendant, string attribute, string attributeValue)
{
var desc = GetDescendant(item, descendant, attribute, attributeValue);
if (desc != null)
return desc.Value;
else
return "";
}
}
And for almost every type of data in Google Reader, I've also created a class:
Feed
(the URL to a feed with the title of that feed) ReaderItem
(you could say this is an article, a blog post, ...) State
(this is the state of an item in Google Reader, like read, starred, ...) Subscription
(a subscription in Google reader is a feed you subscribed to)
Here is the implementation for subscription:
public class Subscription : GoogleXmlItem
{
public string Id { get; set; }
public string Title { get; set; }
public string Url { get; set; }
public List<string> Categories { get; set; }
internal Subscription(XElement item)
: base(item)
{
}
protected override void LoadItem(XElement item)
{
Categories = new List<string>();
Id = GetDescendantValue(item, "string", "name", "id");
Title = GetDescendantValue(item, "string", "name", "title");
if (Id.Contains('/'))
Url = Id.Substring(Id.IndexOf('/') + 1, Id.Length - Id.IndexOf('/') - 1);
var catList = GetDescendant(item, "list", "name", "categories");
if (catList != null && catList.HasElements)
{
var categories = GetDescendants(item, "string", "name", "label");
Categories.AddRange(categories.Select(o => o.Value));
}
}
}
URLs Everywhere
Like mentioned before, the Google Reader API is based on URLs and GET
/POST
requests. To organise this, we've also got a few classes regarding URLs:
ReaderUrl
: A single class containing all the required URLs and paths ReaderCommand
: Enum
representing common tasks (like adding a subscription) ReaderCommandFormatter
: Class containing extension methods for ReaderCommand
to convert these enum
values to actual Google Reader URLs
public static class ReaderUrl
{
public const string AtomUrl = "https://www.google.com/reader/atom/";
public const string ApiUrl = "https://www.google.com/reader/api/0/";
public const string FeedUrl = AtomUrl + "feed/";
public const string StatePath = "user/-/state/com.google/";
public const string StateUrl = AtomUrl + StatePath;
public const string LabelPath = "user/-/label/";
public const string LabelUrl = AtomUrl + LabelPath;
}
public enum ReaderCommand
{
SubscriptionAdd,
SubscriptionEdit,
SubscriptionList,
TagAdd,
TagEdit,
TagList,
TagRename,
TagDelete
}
public static class ReaderCommandFormatter
{
public static string GetFullUrl(this ReaderCommand comm)
{
switch (comm)
{
case ReaderCommand.SubscriptionAdd:
return GetFullApiUrl("subscription/quickadd");
case ReaderCommand.SubscriptionEdit:
return GetFullApiUrl("subscription/edit");
case ReaderCommand.SubscriptionList:
return GetFullApiUrl("subscription/list");
case ReaderCommand.TagAdd:
return GetFullApiUrl("edit-tag");
case ReaderCommand.TagEdit:
return GetFullApiUrl("edit-tag");
case ReaderCommand.TagList:
return GetFullApiUrl("tag/list");
case ReaderCommand.TagRename:
return GetFullApiUrl("rename-tag");
case ReaderCommand.TagDelete:
return GetFullApiUrl("disable-tag");
default:
return "";
}
}
private static string GetFullApiUrl(string append)
{
return String.Format("{0}{1}", ReaderUrl.ApiUrl, append);
}
}
And Finally... ReaderService
Finally there's the implementation of the most common tasks in Google Reader:
public class ReaderService : GoogleService
{
private string username;
public ReaderService(string username, string password, string source)
: base("reader", username, password, source)
{
this.username = username;
}
#region Feed
public IEnumerable<ReaderItem> GetFeedContent(string feedUrl, int limit)
{
try
{
return GetItemsFromFeed(String.Format("{0}{1}",
ReaderUrl.FeedUrl, System.Uri.EscapeDataString(feedUrl)), limit);
}
catch (WebException wex)
{
HttpWebResponse rsp = wex.Response as HttpWebResponse;
if (rsp != null && rsp.StatusCode == HttpStatusCode.NotFound)
throw new FeedNotFoundException(feedUrl);
else
throw;
}
}
#endregion
#region Subscription
public void AddSubscription(string feed)
{
PostRequest(ReaderCommand.SubscriptionAdd,
new GoogleParameter("quickadd", feed));
}
public void TagSubscription(string feed, string folder)
{
PostRequest(ReaderCommand.SubscriptionEdit,
new GoogleParameter("a", ReaderUrl.LabelPath + folder),
new GoogleParameter("s", "feed/" + feed),
new GoogleParameter("ac", "edit"));
}
public List<Subscription> GetSubscriptions()
{
string xml = session.GetSource(ReaderCommand.SubscriptionList.GetFullUrl());
return XElement.Parse(xml).Elements
("list").Elements("object").Select(o => new Subscription(o)).ToList();
}
#endregion
#region Tags
public void AddTags(ReaderItem item, params string[] tags)
{
int arraySize = tags.Length + item.Tags.Count + 2;
GoogleParameter[] parameters = new GoogleParameter[arraySize];
parameters[0] = new GoogleParameter("s", "feed/" + item.Feed.Url);
parameters[1] = new GoogleParameter("i", item.Id);
int nextParam = 2;
for (int i = 0; i < item.Tags.Count; i++)
parameters[nextParam++] = new GoogleParameter
("a", ReaderUrl.LabelPath + item.Tags[i]);
for (int i = 0; i < tags.Length; i++)
parameters[nextParam++] = new GoogleParameter
("a", ReaderUrl.LabelPath + tags[i]);
PostRequest(ReaderCommand.TagAdd, parameters);
}
public void RenameTag(string tag, string newName)
{
PostRequest(ReaderCommand.TagRename,
new GoogleParameter("s", ReaderUrl.LabelPath + tag),
new GoogleParameter("t", tag),
new GoogleParameter("dest", ReaderUrl.LabelPath + newName));
}
public void RemoveTag(string tag)
{
PostRequest(ReaderCommand.TagDelete,
new GoogleParameter("s", ReaderUrl.LabelPath + tag),
new GoogleParameter("t", tag));
}
public void RemoveTag(ReaderItem item, string tag)
{
PostRequest(ReaderCommand.TagEdit,
new GoogleParameter("r", ReaderUrl.LabelPath + tag),
new GoogleParameter("s", "feed/" + item.Feed.Url),
new GoogleParameter("i", item.Id));
}
public List<string> GetTags()
{
string xml = session.GetSource(ReaderCommand.TagList.GetFullUrl());
var tagElements = from t in XElement.Parse(xml).Elements
("list").Descendants("string")
where t.Attribute("name").Value == "id"
where t.Value.Contains("/label/")
select t;
List<string> tags = new List<string>();
foreach (XElement element in tagElements)
{
string tag = element.Value.Substring(element.Value.LastIndexOf('/') + 1,
element.Value.Length - element.Value.LastIndexOf('/') - 1);
tags.Add(tag);
}
return tags;
}
public IEnumerable<ReaderItem> GetTagItems(string tag, int limit)
{
return GetItemsFromFeed(String.Format("{0}{1}",
ReaderUrl.LabelPath, System.Uri.EscapeDataString(tag)), limit);
}
#endregion
#region State
public void AddState(ReaderItem item, State state)
{
PostRequest(ReaderCommand.TagEdit,
new GoogleParameter("a",
ReaderUrl.StatePath + StateFormatter.ToString(state)),
new GoogleParameter("i", item.Id),
new GoogleParameter("s", "feed/" + item.Feed.Url));
}
public void RemoveState(ReaderItem item, State state)
{
PostRequest(ReaderCommand.TagEdit,
new GoogleParameter("r",
ReaderUrl.StatePath + StateFormatter.ToString(state)),
new GoogleParameter("i", item.Id),
new GoogleParameter("s", "feed/" + item.Feed.Url));
}
public IEnumerable<ReaderItem> GetStateItems(State state, int limit)
{
return GetItemsFromFeed(String.Format("{0}{1}",
ReaderUrl.StateUrl, StateFormatter.ToString(state)), limit);
}
#endregion
private void PostRequest(ReaderCommand command, params GoogleParameter[] postFields)
{
session.PostRequest(ReaderCommandFormatter.GetFullUrl(command), postFields);
}
private IEnumerable<ReaderItem> GetItemsFromFeed(string url, int limit)
{
SyndicationFeed feed = session.GetFeed(url,
new GoogleParameter("n", limit.ToString()));
return feed.Items.Select<SyndicationItem, ReaderItem>(o => new ReaderItem(o));
}
}
And as you can see, the ReaderService
does a few things:
- Subscriptions (list, add)
- Tags (add, rename, delete, ...)
- States (add, remove, list)
- Feed (list contents)
And it actually re-uses a few of the things we talked about:
GoogleSession
to send post requests, get feeds, ... ReaderCommand
, ReaderCommandFormatter
, ReaderUrl
to do all the URL related stuff GoogleParameter
to set POST
fields (fields we can find using Fiddler)
Putting It All Together
The console application was also updated with our ReaderService
:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("");
Console.Write(" Enter your Google username: ");
string username = Console.ReadLine();
Console.Write(" Enter your password: ");
string password = Console.ReadLine();
using (ReaderService rdr = new ReaderService
(username, password, "Sandworks.Google.App"))
{
Console.WriteLine("");
Console.WriteLine(" Last 5 articles from Sandrino's Blog: ");
foreach (ReaderItem item in rdr.GetFeedContent
("http://sandrinodimattia.net/blog/syndication.axd?format=rss", 5))
{
Console.WriteLine(" - " + item.Author + ": " + item.Title);
}
}
Console.ReadLine();
}
}
There you go, now you've got everything you need to get started with Google Reader in .NET. The following article will be about creating a basic WPF application to have a simple desktop version of Google Reader.
Enjoy...
History
- 12th July, 2010: Initial post