Scott Hanselman

The Weekly Source Code 22 - C# and VB .NET Libraries to Digg, Flickr, Facebook, YouTube, Twitter, Live Services, Google and other Web 2.0 APIs

March 27, 2008 Comment on this post [31] Posted in ASP.NET | Programming | Source Code | Web Services | XML
Sponsored By

Someone emailed me recently saying that they couldn’t find enough examples in .NET for talking to the recent proliferation of “Web 2.0 APIs” so I thought I’d put together a list and look at some source. I think that a nice API wrapper is usually a useful thing, but since these APIs are so transparent and basic, there's not really a huge need given LINQ to XML but I understand the knee-jerk reaction to hunt for a wrapper when faced with the word "API."

One thing to point out is that 99.9% of these APIs are calling

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);

under the covers the doing something with the resulting string. Some hide the URL creation, some use XmlDocuments, others use XmlSerialization. When you use a random API you find on the net you're usually getting what you pay for. You're getting a one person's free view on how they perceived a certain API should be called. Some will like be more performant than others. Some might be better thought out than others.

I'll try to juxtapose a few differences between them, but I just want you to remember that we're talking about pushing Angle Brackets around, and little else. You can always do it yourself.

And so, Dear Reader, I present to you twenty-first in a infinite number of posts of "The Weekly Source Code."

Digg

Digg is a community-voted controlled explosion of news stories. Their API is "REST" and speaks XML or JSON on the wire.

DiggApiNET is a .NET Wrapper for the Digg API. It has no releases, so you'll have to get the source code. It was last updated in May of 2007. There's also another at CodeProject called, creatively, digg API.NET.

Let's talk philosophy of design and look a the first library. Here's some snippets pulled from all over the code. This API builds the URL and loads the results of the call into an XmlDocument, holds it for a second and SelectNodes the values into Digg-specific objects. These objects know about the existence of System.Xml.

private const string get_popular = "http://services.digg.com/stories/popular/comments/{0}";

public DiggComments GetPopular()
{
return GetPopular(new Hashtable());
}
public DiggComments GetPopular(Hashtable args)
{
string uri = String.Format(get_popular, HttpBuildUrl(args));
return new DiggComments(Request(uri));
}
public DiggComments(XmlDocument xml_doc) : base(xml_doc, "events")
{
_comments = new List();
if (xml_doc.SelectSingleNode("events") == null
|| xml_doc.SelectSingleNode("events").SelectNodes("comment") == null) {
throw new DiggApiException("XML response appears to be malformed, or contains unexpected data.");
}
foreach (XmlNode node in xml_doc.SelectSingleNode("events").SelectNodes("comment")) {
_comments.Add(new DiggComment(node));
}
}

This is a pretty straight-forward if not totally "clean" way to do it. SelectSingleNode and SelectNodes aren't too fast, but we're looking at tiny chunks of data, probably under 100k. I'd probably do it with either XmlReader or XmlSerializer, or more likely, LINQ to XML. I'd make a service that handle the wire protocol, and make the objects know less.

Facebook

Facebook has a very sophisticated and deep API and there's lots of support for it on .NET that is well explained by Nikhil. You can develop for Facebook using the free Express Visual Studio editions.

There's quite a few available:

Nikhil's Facebook client APIs feel well factored, with separate services for each major Facebook service and a FacebookSession object proving contextual state. Requests are pulled out into FacebookRequest and include asynchronous options, which is thoughtful.

Here's an edited (for brevity) example of a WinForm that allows you to set your Facebook status. I like the IsPermissionGranted call, which I think is clean and clever, given that there is a large enum of permissions.

    public partial class StatusForm : Form {

private const string _apiKey = "[Your API Key]";
private const string _secret = "[Your Secret]";

private FacebookService _fbService;
private bool _loggingIn;

private void LoadStatus() {
_nameLabel.Text = "Loading...";

User user = _fbService.Users.GetUser(null, "name,status");
if (user != null) {
_nameLabel.Text = user.Name;

_statusTextBox.Text = user.Status.Message;
_dateLabel.Text = user.Status.UpdateDate.ToLocalTime().ToString("g");
}

bool canSetStatus = _fbService.Permissions.IsPermissionGranted(Permission.SetStatus);
_permissionsLink.Visible = !canSetStatus;
_updateButton.Enabled = canSetStatus;
_statusTextBox.ReadOnly = !canSetStatus;
}

protected override void OnActivated(EventArgs e) {
base.OnActivated(e);

if ((_fbService == null) && (_loggingIn == false)) {
_loggingIn = true;

try {
FacebookClientSession fbSession = new FacebookClientSession(_apiKey, _secret);
if (fbSession.Initialize(this)) {
_fbService = new FacebookService(fbSession);
LoadStatus();
}
}
finally {
_loggingIn = false;
}
}
}

private void OnUpdateButtonClick(object sender, EventArgs e) {
string text = _statusTextBox.Text.Trim();

_fbService.Users.SetStatus(text, /* includesVerb */ true);
LoadStatus();
}
}
}

Interestingly, the Facebook API also includes it's own JsonReader and JsonWriter, rather than using the new JsonSerializer, presumably because the lib was written a year ago.

Windows Live Services

There's a bunch of info on http://dev.live.com/ and a bunch of complete sample apps with source as well as a Live SDK interactive site. The Live Contacts API, for example . Unfortunately with the Contact's API there's no .NET samples I can find that includes wrappers around the angle brackets, so you'll be parsing in whatever way you prefer.

The objects that are provided in the Alpha SDK are really focused initially on security and permissions. For example, before I was able to access my contacts programmatically, I had to explicitly allow access and chose a length of time to allow it. I allowed it for a day to be extra secure.

Once you've retrieved some data, it's very simple so a request like https://cumulus.services.live.com/wlddemo@hotmail.com/LiveContacts would give you:

<LiveContacts> 
   <Owner>
       <FirstName/>
       <LastName/>
       <WindowsLiveID/>                 
   </Owner>                       
   <Contacts>         
<Contact>            
<ID>{ContactID}</ID>            
<WindowsLiveID>{Passport Member Name}</WindowsLiveID>
       <Comment>comment here</Comment>            
<Profiles/>            
<Emails/>            
<Phones/>           
<Locations/>        
</Contact>     
</Contacts> </LiveContacts>

The Live Search API speaks SOAP and has samples in six languages including C#, VB, Ruby, PHP, Python, and Java.

YouTube

YouTube has two different versions of their API, but the original/old version is officially deprecated. Now that they are Google, the YouTube APIs are all GData style, replacing their REST/XML-RPC APIs.

There is a .NET Library that speaks the GData XML format and querying YouTube with C# is fairly simple from there. You can even upload videos programmatically to YouTube like this gentleman.

This fellow eschews GData's uber libraries and uses a StringBuilder to build the GData payload and that's OK. :)

private string GetHeader(string title, string description, Catagory catagory,
                         string keywords, string videoFileName)
{
    StringBuilder xml = new StringBuilder();
    xml.Append(boundary + lineTerm + "Content-Type: application/atom+xml; charset=UTF-8" + lineTerm + lineTerm);
    xml.Append("<?xml version=\"1.0\"?><entry xmlns=\"http://www.w3.org/2005/Atom\" ");
    xml.Append("xmlns:media=\"http://search.yahoo.com/mrss/\" xmlns:yt=\"http://gdata.youtube.com/schemas/2007\">");
    xml.AppendFormat("<media:group><media:title type=\"plain\">{0}</media:title>", title);
    xml.AppendFormat("<media:description type=\"plain\">{0}</media:description>", description);
    xml.AppendFormat("<media:category scheme=\"http://gdata.youtube.com/schemas/2007/categories.cat\">{0}</media:category>", catagory);
    xml.AppendFormat("<media:keywords>{0}</media:keywords>", keywords);
    xml.Append("</media:group></entry>" + lineTerm);
    xml.Append(boundary + lineTerm + "Content-Type: video/*" + lineTerm + "Content-Transfer-Encoding: binary" + lineTerm + lineTerm);
    return xml.ToString();
}

GData

GData is Google's standard protocol for moving data around via XML and HTTP. There are GData endpoints for Blogger, Google Calendar, Notebook, Spreadsheets, Documents, Picassa, etc. From their site:

NET Developer Guides exist for specific Data APIs. They can be found under the page for each Data API

The GData C# client is written by Google, so I was really interested to read their code as their interview process is legendary and I assume everyone is a 17 year old PhD. The code is exceedingly object oriented with more than 165 files over 10 folders (not counting unit tests and project stuff). It's also VERY well commented, but interestingly, not always commented using the standard XML comments most MSFT Programmers use, but rather a different format I'm not familiar with.

All the APIs are fairly similar. Here's a GData sample that Queries the Calendar for events within a date range.

static void DateRangeQuery(CalendarService service, DateTime startTime, DateTime endTime)
{
EventQuery myQuery = new EventQuery(feedUri);
myQuery.StartTime = startTime;
myQuery.EndTime = endTime;

EventFeed myResultsFeed = service.Query(myQuery) as EventFeed;

Console.WriteLine("Matching events from {0} to {1}:",
startTime.ToShortDateString(),
endTime.ToShortDateString());
Console.WriteLine();
for (int i = 0; i < myResultsFeed.Entries.Count; i++)
{
Console.WriteLine(myResultsFeed.Entries[i].Title.Text);
}
Console.WriteLine();
}

Here's an example that downloads all the pictures from a specific username in Picassa using C#. Everything in GData is an "AtomEntry" and many have extensions. You can handle the GData types or use specific sub-classes like PhotoQuery, or whatever, to make thing easier.

private static void DownAlbum(string UserN, string AlbumN)
{
string fileName;
Uri uriPath;
WebClient HttpClient = new WebClient();
// Three important elements of PicasaWeb API are
// PhotoQuery, PicasaService and PicasaFeed
PhotoQuery query = new PhotoQuery();
query.Uri = new Uri(PhotoQuery.CreatePicasaUri(UserN, AlbumN));
PicasaService service = new PicasaService("Sams PicasaWeb Explorer");
PicasaFeed feed = (PicasaFeed)service.Query(query);

Directory.SetCurrentDirectory("c:\\");
foreach (AtomEntry aentry in feed.Entries)
{
uriPath = new Uri(aentry.Content.Src.ToString());
fileName = uriPic.LocalPath.Substring(uriPath.LocalPath.LastIndexOf('/')+1);
try {
Console.WriteLine("Downloading: " + fileName);
HttpClient.DownloadFile(aentry.Content.Src.ToString(), fileName);
Console.WriteLine("Download Complete");
}
catch (WebException we)
{ Console.WriteLine(we.Message); }
}
}

You can also certainly use any standard System.Xml APIs if you like.

GData is an extension of the Atom Pub protocol. Atom Pub is used by Astoria (ADO.NET Data Extensions) which can be accessed basically via "LINQ to REST."

Flickr

Flickr has a nice API and WackyLabs has a CodePlex project for their FlickrNET API Library written in C#. It's also confirmed to work on Compact Framework and Mono as well as .NET 1.1 and up. There's a fine Coding4Fun article on this library.

This API couldn't be much easier to use. For example, this searches for photos tagged blue and sky and makes sure it returns the DateTaken and OriginalFormat.

PhotosSearchOptions options = new PhotosSearchOptions();
options.Tags = "blue,sky";
options.Extras |= PhotoSearchExtras.DateTaken | PhotoSearchExtras.OriginalFormat;
Photos photos = flickr.PhotosSearch(options);

The PhotosSearch() method includes dozens of overloads taking date ranges, paging and other options. All the real work happens in GetResponse() via GetResponseCache(). The URL is built all in one method, the response is retrieved and deserialized via XmlSerializer. This API is the closest to the way I'd do it. It's pragmatic, uses as much of the underlying libraries as possible. It's not really extensible or overly OO, but it gets the job done cleanly.

Since Flickr is a data intensive thing, this library also includes a thread safe PersisitentCache for storing all that data. I'd probably just have used System.Web.Cache because it can live in any application, even ones outside ASP.NET. However, theirs is a Persistent one, saving huge chunks of data to a configurable location. It's actually an interesting enough class that it could be used outside of this lib, methinks. It stores everything in a super "poor man's database," basically a serialized Hashtable of blobs, ala (gasp) OLE Structured Storage.

WordPress and XML-RPC based Blogs

Most blogs use either the Blogger or MetaWeblog APIs and they are easy to call with .NET.  That includes MSN Spaces, DasBlog, SubText, etc. There's samples deep on MSDN on how to call XML-RPC with C# or VB.

Windows Live Writer and BlogJet use these APIs to talk to blogs when you're authoring a post, so I'm using .NET and XML-RPC right now. ;)

A very simple example in VB.NET using the very awesome XML-RPC.NET library is here. Here's a more complete example and here's a mini blogging client.

DasBlog uses this library to be an XML-RPC Server.

In this sample, the type "IWP" derives from XmlRpcProxy and uses the category structure. The library handles all the mappings an deserializaiton such that calling XML-RPC feels ;like using any Web Service, even though XML-RPC is a precursor to SOAP and not the SOAP you're used it.

Dim proxy As IWP = XmlRpcProxyGen.Create(Of IWP)()
Dim args() As String = {“http://myblog.blogstogo.com”, _
“username”, “password”}
Dim categories() As category
categories = proxy.getCategories(args)

You can also use WCF to talk XML-RPC

Twitter

I've talked about Twitter before and they have a Twitter API that is at least an order of magnitude more important than their site. There is a pile of source out there to talk to Twitter.

Last year Alan Le blogged about his adventures in creating a library around Twitter's API and Witty is a actively developed WPF C# application that fronts Twitter. You can browse their source and see their simple TwitterLib.

TwitterNet.cs is the meat of it and just builds up objects using XmlDocuments and does what I called "left hand/right hand" code. That's where you've got an object on the left and some other object/bag/pileOdata on the right and you spend a lot of lines just going "left side, right side, left side, right side.

For (trimmed) example:

 public UserCollection GetFriends(int userId)
{
UserCollection users = new UserCollection();

// Twitter expects http://twitter.com/statuses/friends/12345.xml
string requestURL = FriendsUrl + "/" + userId + Format;

int friendsCount = 0;

// Since the API docs state "Returns up to 100 of the authenticating user's friends", we need
// to use the page param and to fetch ALL of the users friends. We can find out how many pages
// we need by dividing the # of friends by 100 and rounding any remainder up.
// merging the responses from each request may be tricky.
if (currentLoggedInUser != null && currentLoggedInUser.Id == userId)
{
friendsCount = CurrentlyLoggedInUser.FollowingCount;
}
else
{
// need to make an extra call to twitter
User user = GetUser(userId);
friendsCount = user.FollowingCount;
}

int numberOfPagesToFetch = (friendsCount / 100) + 1;

string pageRequestUrl = requestURL;

for (int count = 1; count <= numberOfPagesToFetch; count++)
{
pageRequestUrl = requestURL + "?page=" + count;
HttpWebRequest request = WebRequest.Create(pageRequestUrl) as HttpWebRequest;
request.Credentials = new NetworkCredential(username, password);

try
{
using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
StreamReader reader = new StreamReader(response.GetResponseStream());
XmlDocument doc = new XmlDocument();
doc.Load(reader);
XmlNodeList nodes = doc.SelectNodes("/users/user");

foreach (XmlNode node in nodes)
{
User user = new User();
user.Id = int.Parse(node.SelectSingleNode("id").InnerText);
user.Name = node.SelectSingleNode("name").InnerText;
user.ScreenName = node.SelectSingleNode("screen_name").InnerText;
user.ImageUrl = node.SelectSingleNode("profile_image_url").InnerText;
user.SiteUrl = node.SelectSingleNode("url").InnerText;
user.Location = node.SelectSingleNode("location").InnerText;
user.Description = node.SelectSingleNode("description").InnerText;

users.Add(user);
}

}
}
catch (WebException webExcp)
{
// SNIPPED BY SCOTT
}
}
return users;
}

So far, there's a .NET lib for every Web 2.0 application I've wanted to use. I even banged a .NET Client out for Wesabe last year then did it again in IronRuby.

Enjoy. Which (of the hundreds) did I miss?

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service
March 27, 2008 4:10
Scott,

I've been reading your site for a long time, but never thought I'd end up on it. In fact, I've been using your plans for creating an in-home network as the basis for how I design the next place I live, and your more recent article equating VB.NET developers to Sox fans was a great hit with my fellow fans in MA. Thanks a lot for your recognition, and please keep the site up!

--J
March 27, 2008 4:54
Great post Scott! Thanks
March 27, 2008 5:49
Hi Scott,

I don't know if you didn't notice or if this is one of those problems that seems simple for the user but trick to fix, but anyway, I like reading everything right from google reader, and your code samples becomes too messy there, I took a screenshot:

http://img87.imageshack.us/img87/9523/rssbr8.jpg

So, as I'm lazy as hell, I don't read because I have to click on the link :)

Anyway, if you could find some way to fix at least the spacing and line breaks, I'll be grateful.

Thanx.
March 27, 2008 6:48
Come on, the StringBuilder is NOT ok for building XML. An XmlWriter is just as quick and dirty; there is no reason not to use it.
What happens when the title of your video is "Rock & Roll"?
March 27, 2008 7:38
StringBuilders are always at least one level of abstraction too low for the job except (surprise!) building strings.
March 27, 2008 8:09
For me, the pay off in using a community API over rolling my own is in how well the API brings the service to .Net - i.e. does it enable LINQ on returned objects? support for design time with a GridView? Can it serialize with the standard methods?

A second factor is does the API enhance the service and solve common problems? Flickr.Net is a perfect example with the cache option (even though I may have thoughts on the implementation) - using it on a website you'll need to cache the calls and this saves me the effort.

A note on XML-RCP; it still saddens me this was not baked into WCF somehow or provided by another area of the framework. There is a good deal of services using XML-RCP (including most blogs), or offering in as an API option (there is also a sad lack of WS-SOAP offered, and Flickr offers SOAP without a WSDL?!). Luckily the community project XML-RPC.Net is awesome.
March 27, 2008 8:10
There's also my Minima 3.5 Blog Engine (a minimalistic training blog engine) which includes the MetaWeblog API and XML-RPC.NET. Then, there's also my Bible web service REST wrapper for .NET 3.5.
March 27, 2008 9:36
@Fábio, I see the same problem with Bloglines (sample screenshot).

I'm not sure why both of these readers are adding the extra spaces. The code is enclosed in [pre] and uses [br] at each line end - maybe that's the problem.

In any case, for any posts like this where Scott includes code samples, it's easier to just jump over to the live site to read it with better formatting.
March 27, 2008 9:44
FeedDemon desktop client doesn't like your code snippets either, darn computers.
March 27, 2008 10:31
Great post Scott! Nice to finally see a "heads up" on all of these API's on one place!
Thanks!
Rob
March 27, 2008 11:21
cool post.

@Scott:
just a little nag, I fear that the picasa sample has a spelly, fileName = uriPicuriPath.LocalPath

@StringBuilderBashers:
I, for one, prefer building xml with a templating engine (like velocity / AspView / whatever). should that not be available, I'd rather StringBuilder it that XmlXyz it, as I prefer to see the output structure in front of my face, rather than the api calls. You guys calling out "It's wrong" without giving a solid reason is, well, bashing.
March 27, 2008 17:10
I'd love to see samples of using the WCF 3.5 Syndication APIs against Google GData, using ElementExtensions and the like with SyndicationItems and SyndicationFeeds to stamp in the custom Atom elements, be it the custom Yahoo Media Elements or Open Search elements, as ElementExtensions. It would be doubly good, as this is the new direction of Astoria access using AtomPub, as you mention.
March 27, 2008 17:15
I've been using a lot of Web 2.0 APIs without C# or VB.NET. I use pure JavaScript to consume their JSON feed. If they do not provide a JSON feed then I use Yahoo! Pipes to convert the XML feed into a JSON feed (so essentially everything is available in JSON format). I currently have 15 examples on my web site and since it is all JavaScript you can easily view the code in the page source. I'll be working on a Yahoo! Maps with Twitter friend locations mashup today.
March 27, 2008 17:41
@Ken, I did give a reason why "its wrong", although I guess it was too subtle.

If you pass in a title of "Rock & Roll" to the YouTube method Scott shows above, the result will be invalid XML, and likely unparseable by the receiver. That is because the ampersand needs to be encoded as &amp;. Now, you could either do all of the encoding yourself with the StringBuilder, checking for all of the scenarios that require it and doing a string replace - or you could just use an XmlWriter (wrapping a StringBuilder if you wish) which will handle all of that for you.
March 27, 2008 18:38
I recently wrote an API for work in C# to be able to publish content and run searches against a Google Search Appliance. Scott, you just inspired me to post it on CodePlex.
March 27, 2008 20:05
Scott, a good API wrapper library is much more than a wrapper for WebRequest.Create.
March 27, 2008 21:15
how about del.icio.us?

http://sourceforge.net/project/showfiles.php?group_id=174552

or

http://netlicious.sourceforge.net/

or someday LINQ to del.icio.us, anyone?
March 27, 2008 21:28
Crap! I forgot delicious! Thanks David, that's a good find.

Jeffrey - Can you expand?
March 27, 2008 23:34
Is there a book site that provides an API? I've been using LibraryThing almost since the beginning, but still waiting for an API.
March 28, 2008 1:31
You forgot http://code.google.com/p/twitterbots, which provides a client API (IBotClient) as well as a minimalist bot framework to create your own twitter-based bots.

The nice thing about it is that it's prepared for mocking so you can easily unit test your bots.
We're planning to refactor it into a generic bot framework, and a twitter# lib, SMS lib, FrontLine SMS lib, etc., that all plug on top of it.

The library is nicely factored so that you don't have a single huge class with every REST endpoint exposed by twitter, but rather have an intuitive programming model like:

botClient.Messages.Send(....);
botClient.Friendship.Follow("kzu");
botClient.Status.Update("commenting @ scott's blog");

:)
March 28, 2008 13:10
Nice post Scott , I'm currently probing into some of those API's (Google's Gdata and WebBlogAPI ) for a project I'm working on with some friends. I must say that you guys have "balls" to be writing your own API's (unfortunately i'm not at the skill level yet). keep up the good work. Also can i get a reference to some sort of library for dynamically formatting text with HTML for desktop apps ( WYSIWIG HTML editor sort of).

PS: Any comments on Jeff Atwood leaving Vertigo ?
March 28, 2008 14:10
Scott,

Just a note that there is a .Net wrapper for Windows Live services. My friend and I have been building it for the last few months. Details of it are at http://www.codeplex.com/LiveNet

Scøtt
March 28, 2008 21:30
Hey Scott,
Thanks for the link to my post regarding Facebook development!
March 28, 2008 21:44
Hey Scott,

Thanks for the great post! It's kinda sad that the Windows Live Services section is this small. You'd think that in their transformation efforts, MS would be building a very robust and complete set of APIs and and .NET wrappers for *every* Live Service. For example, SkyDrive is still without an API although if it had one, 3rd party tools would probably drive its adoption much higher so I just can't understand why this is not a top area of focus for MS.

Adham
March 29, 2008 1:38
It may not be Web 2.0 but I would love to have a good wrapper for the API to AIM (AOL Instant Messenger) to build a client. Their .NET code samples are not well done and it's talking to com objects so it's an area that is tricky. Plus they are in c# and I want to use VB. A nice wrapper would save me a lot of time and frustration.
March 29, 2008 1:42
I've been looking for this information, very timely, thank you.
March 29, 2008 18:41
Yet, another instance of one smart developer (Scott) amongst the hordes of lazy .NET developers. I, being a .NET developer, watch helplessly at forums, news groups and in the workplace as people flounder around with "I just want it to bind" as they move up and up the layers of abstraction.
Scott is right, these services are stupid simple to use. Have at them. Most likely, most people have forgotten how the plumbing even works.
March 30, 2008 3:52

This will help me a lot in my current/recent projects.

Josh Coswell
http://riverasp.net
March 31, 2008 18:11
See some of the API working live on Http://oryore.com with fragrance of AJAX on it.
April 01, 2008 20:50
Great post, Scott. One question -- most Web API's have "one call per second" limit. What is the best way to do a site-wide proxy / queue to throttle your reqeuests?
jb
April 08, 2008 16:50
Hi Scott,

I am the main maintainer of the Google GData .NET libraries. The code is grown over 3 years, so i do not doubt that there are some inconsistencies around somewhere. The documentation was initially build with NDocs, and when that project died, i switched to Sandcastle. I am curious to learn what kind of commenting inconsistencies you have found, so that i can put that on my plate to work on someday :)

Thanks for taking a look


Frank Mantek
Google

Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.