Commits

Matt Hamilton committed 0f7a5b4

Refactored TwitterClient into two classes

  • Participants
  • Parent commits f7db8cc
  • Branches httpclient

Comments (0)

Files changed (20)

File Budgie.Playground/Program.cs

                 _accessSecret = args[1];
             }
 
-            var client = new TwitterClient(new ConsolePlatformAdapter(), _consumerKey, _consumerSecret);
+            var auth = new TwitterAuthenticator(new ConsolePlatformAdapter(), _consumerKey, _consumerSecret);
+            var client = auth.AuthenticateAsync(_accessToken, _accessSecret).Result.Result;
+            //var client = TestAuthentication(auth);
 
-            Authenticate(client);
+            //var client = auth.AuthenticateAsync(_accessSecret, _accessSecret).Result;
 
-            //var media = System.IO.File.ReadAllBytes(@"D:\Projects\Budgie\Images\budgie-256.png");
-            //var r = client.PostAsync("Testing image upload with Budgie and HttpClient!", new[] { media }).Result;
-            //var r = client.PostAsync("And testing a tweet with no media to make sure that's not broken. :|").Result;
-            TestHomeTimeline(client);
-            //Console.WriteLine(r.StatusCode + "\t" + r.ErrorMessage);
-            //TestAuthentication(client);
-            //TestHomeTimeline(client);
-            // TestFavorites(client);
-            // TestSavedSearches(client);
-            // TestLists(client);
-            // TestFollowerIds(client);
-            // TestFriendIds(client);
-
-            // FakeTwitterClient(client);
+            Console.WriteLine(client.User.ScreenName);
 
             Console.ReadKey();
         }
         static string _accessToken;
         static string _accessSecret;
 
-        private static void Authenticate(TwitterClient client)
-        {
-            client.Authenticate(_accessToken, _accessSecret);
-        }
-
-
         private static void FakeTwitterClient(TwitterClient client)
         {
-            Authenticate(client);
-
             client.DefaultPageSize = 10;
             long? since = null;
 
             }
         }
 
-        private static void TestAuthentication(TwitterClient client)
+        private static TwitterClient TestAuthentication(TwitterAuthenticator factory)
         {
             // Step 1 - acquire a "request token" from Twitter.
-            var requestTask = client.GetRequestTokenAsync();
-            var requestToken = requestTask.Result;
+            var requestToken = factory.GetRequestTokenAsync().Result;
 
-            if (requestToken == null) return; // something went wrong.
+            if (requestToken == null) return null; // something went wrong.
 
             // Step 2 - open a browser so the user can sign into Twitter and obtain a PIN.
             Process.Start(requestToken.AuthorizationUri.ToString());
 
             // Step 3 - ask the user for the PIN and pass it back to Twitter for an "access token".
+            Console.Write("PIN: ");
             var pin = Console.ReadLine();
 
-            var accessTask = client.AuthenticateAsync(requestToken, pin);
-            var accessToken = accessTask.Result;
-
-            Console.WriteLine(accessToken.Token);
-            Console.WriteLine(accessToken.Secret);
-            Console.WriteLine(accessToken.UserId);
-            Console.WriteLine(accessToken.ScreenName);
+            var client = factory.AuthenticateAsync(requestToken, pin).Result;
+            if (client.StatusCode != System.Net.HttpStatusCode.OK)
+            {
+                Console.WriteLine(client.StatusCode.ToString() + "\t" + client.ErrorMessage);
+                return null;
+            }
+            Console.WriteLine(client.Result.User.ScreenName);
+            return client.Result;
         }
-
+        
         private static void TestDirectMessages(TwitterClient client)
         {
-            Authenticate(client);
-
             var tweets = client.GetDirectMessagesSentAsync(since: 0).Result;
 
             Console.WriteLine(tweets.Result.Count() + " messages returned.");
             foreach (var tweet in tweets.Result)
             {
                 Console.WriteLine(tweet.ToUser.ScreenName + "\n\t" + tweet.Text + "\n");
-
             }
         }
 
         private static void TestHomeTimeline(TwitterClient client)
         {
-            Authenticate(client);
-
             // Get a task which is downloading the home timeline.
             var timelineTask = client.GetHomeTimelineAsync();
 
 
         private static void TestFavorites(TwitterClient client)
         {
-            Authenticate(client);
-
             var tweets = client.GetFavoritesAsync(count: 50).Result;
 
             Console.WriteLine(tweets.Result.Count() + " tweets returned.");
 
         private static void TestSavedSearches(TwitterClient client)
         {
-            Authenticate(client);
-
             var searches = client.GetSavedSearchesAsync().Result;
 
             Console.WriteLine(searches.RateLimit);
 
         private static void TestLists(TwitterClient client)
         {
-            Authenticate(client);
-
             var t = client.GetListMembershipsAsync();
             while (!t.IsCompleted)
             {
 
         private static void TestFollowerIds(TwitterClient client)
         {
-            Authenticate(client);
-
             var t = client.GetFollowerIdsAsync();
             while (!t.IsCompleted)
             {
 
         private static void TestFriendIds(TwitterClient client)
         {
-            Authenticate(client);
-
             var t = client.GetFriendIdsAsync();
             while (!t.IsCompleted)
             {

File Budgie/Budgie.csproj

     <DefineConstants>TRACE</DefineConstants>
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
+    <DocumentationFile>
+    </DocumentationFile>
   </PropertyGroup>
   <ItemGroup>
     <Reference Include="Newtonsoft.Json">
     <Compile Include="Json\JsonUrl.cs" />
     <Compile Include="Json\JsonUser.cs" />
     <Compile Include="Json\TwitterizerDateConverter.cs" />
+    <Compile Include="OAuthMessageHandler.cs" />
     <Compile Include="OAuthTokens.cs" />
     <Compile Include="TwitterClient.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="TwitterClient.Users.cs">
       <DependentUpon>TwitterClient.cs</DependentUpon>
     </Compile>
+    <Compile Include="TwitterAuthenticator.cs" />
     <Compile Include="TwitterFriendship.cs" />
     <Compile Include="TwitterList.cs" />
-    <Compile Include="TwitterClient.OAuth.cs">
-      <DependentUpon>TwitterClient.cs</DependentUpon>
-    </Compile>
     <Compile Include="TwitterClient.Timelines.cs">
       <DependentUpon>TwitterClient.cs</DependentUpon>
     </Compile>

File Budgie/Extensions/StringExceptions.cs

-using System;
+using Newtonsoft.Json;
+using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Text;
 
 namespace Budgie.Extensions
         /// <summary>
         /// Escapes a string according to the URI data string rules given in RFC 3986.
         /// </summary>
-        /// <param name="value">The value to escape.</param>
+        /// <param name="s">The value to escape.</param>
         /// <returns>The escaped value.</returns>
         /// <remarks>
         /// The <see cref="Uri.EscapeDataString"/> method is <i>supposed</i> to take on
             // Return the fully-RFC3986-escaped string.
             return escaped.ToString();
         }
+
+        internal static T Deserialize<T>(this string s)
+        {
+            return JsonConvert.DeserializeObject<T>(s, new Json.TwitterizerDateConverter());
+        }
     }
 }

File Budgie/Extensions/WebResponseExtensions.cs

-using System;
+using Budgie.Json;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
 using System.Linq;
-using System.IO;
-using System.Net;
+using System.Net.Http;
 using System.Threading.Tasks;
-using Budgie.Json;
-using Newtonsoft.Json;
-using System.Net.Http;
-using System.Collections.Generic;
 
 namespace Budgie.Extensions
 {
                 {
                     try
                     {
-                        var error = JsonConvert.DeserializeObject<JsonErrors>(content);
+                        var error = content.Deserialize<JsonErrors>();
                         if (error != null && error.errors.Any())
                         {
                             message += "\n\n" + error.errors.First().message;

File Budgie/OAuthMessageHandler.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Budgie
+{
+    internal class OAuthMessageHandler : DelegatingHandler
+    {
+        public OAuthMessageHandler(HttpMessageHandler innerHandler, string consumerKey, string consumerSecret, IPlatformAdaptor platformAdaptor)
+            : base(innerHandler)
+        {
+            _consumerKey = consumerKey;
+            _consumerSecret = consumerSecret;
+            _platformAdaptor = platformAdaptor;
+        }
+
+        IPlatformAdaptor _platformAdaptor;
+        string _consumerKey;
+        string _consumerSecret;
+
+        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
+        {
+            var tokens = request.Properties.ContainsKey("tokens") ? request.Properties["tokens"] as ITokenPair : null;
+            Uri callbackUri = request.Properties.ContainsKey("callbackUri") ? request.Properties["callbackUri"] as Uri : null;
+
+            var oauth_nonce = Convert.ToBase64String(UTF8Encoding.UTF8.GetBytes(DateTime.Now.Ticks.ToString()));
+            var timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
+            var oauth_timestamp = Convert.ToInt64(timeSpan.TotalSeconds).ToString();
+            var resource_url = request.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.Unescaped);
+
+            var dict = new Dictionary<string, string>
+            {
+                { "oauth_consumer_key",      _consumerKey },
+                { "oauth_nonce",             oauth_nonce },
+                { "oauth_signature_method",  "HMAC-SHA1" },
+                { "oauth_timestamp",         oauth_timestamp },
+                { "oauth_version",           "1.0" },
+            };
+
+            if (tokens != null)
+            {
+                dict.Add("oauth_token", tokens.Token);
+            }
+            if (callbackUri != null)
+            {
+                dict.Add("oauth_callback", Uri.EscapeDataString(callbackUri.ToString()));
+            }
+
+            if (!String.IsNullOrEmpty(request.RequestUri.Query))
+            {
+                foreach (var p in request.RequestUri.Query.Substring(1).Split('&'))
+                {
+                    var kv = p.Split('=');
+                    dict.Add(kv[0], kv[1]);
+                }
+            }
+
+            var body = request.Content as StringContent;
+            if (body != null)
+            {
+                var content = await body.ReadAsStringAsync();
+                foreach (var p in content.Split('&'))
+                {
+                    var kv = p.Split('=');
+                    dict.Add(kv[0], kv[1]);
+                }
+            }
+
+            var parms = dict.OrderBy((i) => i.Key);
+
+            var baseString = parms.First().Key + "=" + parms.First().Value;
+            foreach (var p in parms.Skip(1))
+            {
+                baseString += "&" + p.Key + "=" + p.Value;
+            }
+
+            baseString = request.Method + "&" + Uri.EscapeDataString(resource_url) + "&" + Uri.EscapeDataString(baseString);
+
+            var compositeKey = Uri.EscapeDataString(_consumerSecret) + "&";
+            if (tokens != null) compositeKey += Uri.EscapeDataString(tokens.Secret);
+
+            string oauth_signature;
+            oauth_signature = _platformAdaptor.ComputeSha1Hash(compositeKey, baseString);
+
+            string tokenParam = tokens == null ? "" : String.Format("oauth_token=\"{0}\", ", Uri.EscapeDataString(tokens.Token));
+            string callbackParam = callbackUri == null ? "" : String.Format("oauth_callback=\"{0}\", ", Uri.EscapeDataString(callbackUri.ToString()));
+
+            var authHeader = string.Format("{5}oauth_nonce=\"{0}\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"{1}\", oauth_consumer_key=\"{2}\", oauth_signature=\"{3}\", {4} oauth_version=\"1.0\"",
+                                    Uri.EscapeDataString(oauth_nonce),
+                                    Uri.EscapeDataString(oauth_timestamp),
+                                    Uri.EscapeDataString(_consumerKey),
+                                    Uri.EscapeDataString(oauth_signature),
+                                    tokenParam, callbackParam);
+            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("OAuth", authHeader);
+
+            return await base.SendAsync(request, cancellationToken);
+        }
+    }
+}

File Budgie/OAuthTokens.cs

         public string Token { get; internal set; }
         public string Secret { get; internal set; }
 
-        public string UserId { get; internal set; }
+        public long UserId { get; internal set; }
         public string ScreenName { get; internal set; }
     }
 }

File Budgie/TwitterAuthenticator.cs

+using Budgie.Extensions;
+using Budgie.Json;
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace Budgie
+{
+    // mad props to http://www.codeproject.com/Articles/247336/Twitter-OAuth-authentication-using-Net
+    /// <summary>
+    /// Provides methods to authenticate with Twitter, returning an authenticated <see cref="Budgie.TwitterClient"/> instance.
+    /// </summary>
+    public class TwitterAuthenticator
+    {
+        public TwitterAuthenticator(IPlatformAdaptor platformAdaptor, string consumerKey, string consumerSecret)
+        {
+            if (platformAdaptor == null) throw new ArgumentNullException("platformAdaptor");
+            if (String.IsNullOrWhiteSpace(consumerKey)) throw new ArgumentException("consumerKey");
+            if (String.IsNullOrWhiteSpace(consumerSecret)) throw new ArgumentException("consumerSecret");
+
+            DefaultPageSize = 20;
+
+            _client = new HttpClient(new OAuthMessageHandler(new HttpClientHandler(), consumerKey, consumerSecret, platformAdaptor))
+            {
+                BaseAddress = new Uri("https://api.twitter.com/1.1/"),
+                Timeout = TimeSpan.FromSeconds(100),
+            };
+        }
+
+        HttpClient _client;
+
+        protected Task<HttpResponseMessage> HttpGetAsync(string relativeUri, ITokenPair tokens)
+        {
+            var request = new HttpRequestMessage(HttpMethod.Get, relativeUri);
+            request.Properties["tokens"] = tokens;
+            return _client.SendAsync(request);
+        }
+
+        Task<HttpResponseMessage> HttpPostAsync(string relativeUri, Uri callbackUri)
+        {
+            var request = new HttpRequestMessage(HttpMethod.Post, relativeUri);
+            request.Properties["callbackUri"] = callbackUri;
+            return _client.SendAsync(request);
+        }
+
+        Task<HttpResponseMessage> HttpPostAsync(string relativeUri, string content, ITokenPair tokens)
+        {
+            var request = new HttpRequestMessage(HttpMethod.Post, relativeUri);
+            request.Properties["tokens"] = tokens;
+            request.Content = new StringContent(content, System.Text.Encoding.UTF8, "application/x-www-form-urlencoded");
+            return _client.SendAsync(request);
+        }
+
+        /// <summary>
+        /// The number of statuses returned by any method if the count parameter is omitted. Default is 20.
+        /// </summary>
+        public int DefaultPageSize { get; set; }
+
+        /// <summary>
+        /// The timeout for HTTP requests.
+        /// </summary>
+        public TimeSpan Timeout
+        {
+            get { return _client.Timeout; }
+            set
+            {
+                _client.Timeout = value;
+            }
+        }
+
+        /// <summary>
+        /// Gets an OAuth request token from Twitter.
+        /// </summary>
+        /// <returns>A valid <see cref="Budgie.TwitterRequestToken"/>.</returns>
+        public async Task<TwitterRequestToken> GetRequestTokenAsync(Uri callbackUri = null)
+        {
+            var response = await HttpPostAsync("/oauth/request_token", callbackUri);
+            if (!response.IsSuccessStatusCode) return null;
+
+            string responseString = await response.Content.ReadAsStringAsync();
+            if (String.IsNullOrWhiteSpace(responseString)) return null;
+
+            var result = new TwitterRequestToken(_client.BaseAddress);
+            foreach (var i in responseString.Split('&'))
+            {
+                var kv = i.Split('=');
+                if (kv.Length < 2) continue;
+
+                if (kv[0] == "oauth_callback_confirmed")
+                {
+                    if (kv[1] != "true") return null;
+                }
+                else if (kv[0] == "oauth_token")
+                {
+                    result.Token = kv[1];
+                }
+                else if (kv[0] == "oauth_token_secret")
+                {
+                    result.Secret = kv[1];
+                }
+            }
+
+            return result.IsValid ? result : null;
+        }
+
+        /// <summary>
+        /// Returns an authenticated <see cref="Budgie.TwitterClient"/> by requesting an OAuth access token and token_secret from Twitter.
+        /// </summary>
+        /// <param name="requestToken">An OAuth request token.</param>
+        /// <param name="verifier">A verification PIN obtained by the user from Twitter.</param>
+        /// <returns>An instance of <see cref="Budgie.TwitterClient"/> with the tokens returned from Twitter.</returns>
+        public async Task<ITwitterResponse<TwitterClient>> AuthenticateAsync(TwitterRequestToken requestToken, string verifier)
+        {
+            var result = new TwitterResponse<TwitterClient>();
+
+            var response = await HttpPostAsync("/oauth/access_token", "oauth_verifier=" + verifier, requestToken);
+            result.StatusCode = response.StatusCode;
+
+            if (!response.IsSuccessStatusCode)
+            {
+                result.ErrorMessage = response.ReasonPhrase;
+                return result;
+            }
+
+            string responseString = await response.Content.ReadAsStringAsync();
+            if (String.IsNullOrWhiteSpace(responseString)) 
+            {
+                result.ErrorMessage = "Unexpected response from access token request";
+                return result;
+            }
+
+            var token = new TwitterAccessToken();
+            foreach (var i in responseString.Split('&'))
+            {
+                var kv = i.Split('=');
+                if (kv.Length < 2) continue;
+
+                if (kv[0] == "oauth_token")
+                {
+                    token.Token = kv[1];
+                }
+                else if (kv[0] == "oauth_token_secret")
+                {
+                    token.Secret = kv[1];
+                }
+                else if (kv[0] == "user_id")
+                {
+                    long uid;
+                    if (long.TryParse(kv[1], out uid)) token.UserId = uid;
+                }
+                else if (kv[0] == "screen_name")
+                {
+                    token.ScreenName = kv[1];
+                }
+            }
+
+            if (!token.IsValid)
+            {
+                result.ErrorMessage = "Invalid access token";
+                return result;
+            }
+
+            result.Result = new TwitterClient(_client, token)
+            {
+                DefaultPageSize = this.DefaultPageSize,
+            };
+
+            return result;
+        }
+
+        public async Task<ITwitterResponse<TwitterClient>> AuthenticateAsync(string accessToken, string accessTokenSecret)
+        {
+            var tokens = new TwitterAccessToken
+            {
+                Token = accessToken,
+                Secret = accessTokenSecret,
+            };
+
+            var result = new TwitterResponse<TwitterClient>();
+
+            var response = await HttpGetAsync("account/verify_credentials.json?skip_status=true", tokens)
+                .RespondWith<TwitterUser>(content => content.Deserialize<JsonUser>());
+
+            result.StatusCode = response.StatusCode;
+
+            if (response.StatusCode != System.Net.HttpStatusCode.OK)
+            {
+                result.ErrorMessage = response.ErrorMessage;
+                return result;
+            }
+
+            result.Result = new TwitterClient(_client, tokens, response.Result)
+            {
+                DefaultPageSize = this.DefaultPageSize,
+            };
+            return result;
+        }
+    }
+}

File Budgie/TwitterClient.DirectMessages.cs

             relativeUri += (relativeUri.Contains("?") ? "&" : "?") + "include_entities=true&count=" + (count ?? DefaultPageSize);
 
             if (since.HasValue) relativeUri += "&since_id=" + since;
-
             return HttpGetAsync(relativeUri).RespondWith<IEnumerable<TwitterStatus>>(content =>
 
-                Deserialize<IEnumerable<JsonDirectMessage>>(content).Select(j => (TwitterStatus)j).ToList());
+                content.Deserialize<IEnumerable<JsonDirectMessage>>()
+                       .Select(j => (TwitterStatus)j)
+                       .ToList());
         }
 
         public Task<ITwitterResponse<IEnumerable<TwitterStatus>>> GetDirectMessagesAsync(int? count = null, long? since = null)
         public Task<ITwitterResponse<TwitterStatus>> GetDirectMessageAsync(long id)
         {
             var relativeUri = "direct_messages/show/" + id + ".json?include_entities=true";
-
             return HttpGetAsync(relativeUri).RespondWith<TwitterStatus>(content =>
 
-                Deserialize<JsonDirectMessage>(content));
+                content.Deserialize<JsonDirectMessage>());
         }
 
         public Task<ITwitterResponse<TwitterStatus>> SendDirectMessageAsync(string screenName, string text)
         {
             var relativeUri = "direct_messages/new.json";
             var parms = "screen_name=" + screenName.ToRfc3986Encoded() + "&text=" + text.ToRfc3986Encoded();
-
             return HttpPostAsync(relativeUri, parms).RespondWith<TwitterStatus>(content =>
 
-                Deserialize<JsonDirectMessage>(content));
+                content.Deserialize<JsonDirectMessage>());
         }
 
         public Task<ITwitterResponse<TwitterStatus>> DeleteDirectMessageAsync(long id)
         {
             var relativeUri = "direct_messages/destroy/" + id + ".json?include_entities=true";
-
             return HttpPostAsync(relativeUri).RespondWith<TwitterStatus>(content =>
 
-                 Deserialize<JsonStatus>(content));
+                 content.Deserialize<JsonStatus>());
         }
     }
 }

File Budgie/TwitterClient.Favorites.cs

         public Task<ITwitterResponse<TwitterStatus>> FavouriteAsync(long id)
         {
             return HttpPostAsync("favorites/create.json", "id=" + id)
-                .RespondWith<TwitterStatus>(content => 
-                    Deserialize<JsonStatus>(content));
+                .RespondWith<TwitterStatus>(content =>
+                    content.Deserialize<JsonStatus>());
         }
 
         public Task<ITwitterResponse<TwitterStatus>> UnfavouriteAsync(long id)
         {
             return HttpPostAsync("favorites/destroy.json", "id=" + id)
-                .RespondWith<TwitterStatus>(content => 
-                    Deserialize<JsonStatus>(content));
+                .RespondWith<TwitterStatus>(content =>
+                    content.Deserialize<JsonStatus>());
         }
     }
 }

File Budgie/TwitterClient.Friendships.cs

             var response = await t.ToTwitterResponse<IEnumerable<long>>(ids);
             if (response.StatusCode != HttpStatusCode.OK) return response;
 
-            var idCursor = Deserialize<JsonIdCursor>(response.RawContent);
+            var idCursor = response.RawContent.Deserialize<JsonIdCursor>();
 
             ids.AddRange(idCursor.ids);
 
         public Task<ITwitterResponse<TwitterUser>> FollowAsync(string screenName)
         {
             return HttpPostAsync("friendships/create.json", "screen_name=" + screenName.ToRfc3986Encoded())
-                .RespondWith<TwitterUser>(content => 
-                    Deserialize<JsonUser>(content));
+                .RespondWith<TwitterUser>(content =>
+                    content.Deserialize<JsonUser>());
         }
 
         public Task<ITwitterResponse<TwitterUser>> UnfollowAsync(string screenName)
         {
             return HttpPostAsync("friendships/destroy.json", "screen_name=" + screenName.ToRfc3986Encoded())
-                .RespondWith<TwitterUser>(content => 
-                    Deserialize<JsonUser>(content));
+                .RespondWith<TwitterUser>(content =>
+                    content.Deserialize<JsonUser>());
         }
 
         public Task<ITwitterResponse<TwitterFriendship>> GetFriendshipAsync(string sourceScreenName, string targetScreenName)
         {
             return HttpGetAsync("friendships/show.json?source_screen_name=" + sourceScreenName.ToRfc3986Encoded() + "&target_screen_name=" + targetScreenName.ToRfc3986Encoded())
                 .RespondWith<TwitterFriendship>(content =>
-                    Deserialize<JsonFriendship>(content));
+                    content.Deserialize<JsonFriendship>());
         }
     }
 }

File Budgie/TwitterClient.Lists.cs

             var response = await t.ToTwitterResponse<IEnumerable<TwitterList>>(lists);
             if (response.StatusCode != HttpStatusCode.OK) return response;
 
-            var userCursor = Deserialize<JsonListCursor>(response.RawContent);
+            var userCursor = response.RawContent.Deserialize<JsonListCursor>();
 
             foreach (var item in userCursor.lists)
                 lists.Add(item);
 
             return HttpGetAsync(relativeUri)
                 .RespondWith<IEnumerable<TwitterList>>(content =>
-                    Deserialize<IEnumerable<JsonList>>(content)
-                        .Select(j => (TwitterList)j)
-                        .ToList());
+                    content.Deserialize<IEnumerable<JsonList>>()
+                           .Select(j => (TwitterList)j)
+                           .ToList());
         }
 
         public Task<ITwitterResponse<IEnumerable<TwitterList>>> GetListMembershipsAsync()

File Budgie/TwitterClient.OAuth.cs

-using Budgie.Extensions;
-using Budgie.Json;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Budgie
-{
-    partial class TwitterClient
-    {
-        // mad props to http://www.codeproject.com/Articles/247336/Twitter-OAuth-authentication-using-Net
-
-        /// <summary>
-        /// Gets an AOuth request token from Twitter.
-        /// </summary>
-        /// <returns>A valid <see cref="Budgie.TwitterClient.TwitterRequestToken"/>.</returns>
-        public async Task<TwitterRequestToken> GetRequestTokenAsync(Uri callbackUri = null)
-        {
-            var response = await HttpPostAsync("/oauth/request_token", callbackUri);
-            if (response.StatusCode != HttpStatusCode.OK) return null;
-
-            string responseString = await response.Content.ReadAsStringAsync();
-            if (String.IsNullOrWhiteSpace(responseString)) return null;
-
-            var result = new TwitterRequestToken(_client.BaseAddress);
-            foreach (var i in responseString.Split('&'))
-            {
-                var kv = i.Split('=');
-                if (kv.Length < 2) continue;
-
-                if (kv[0] == "oauth_callback_confirmed")
-                {
-                    if (kv[1] != "true") return null;
-                }
-                else if (kv[0] == "oauth_token")
-                {
-                    result.Token = kv[1];
-                }
-                else if (kv[0] == "oauth_token_secret")
-                {
-                    result.Secret = kv[1];
-                }
-            }
-
-            return result.IsValid ? result : null;
-        }
-
-        /// <summary>
-        /// Authenticates this <see cref="Budgie.TwitterClient"/> with an already-known OAuth access token and token_secret.
-        /// </summary>
-        /// <param name="accessToken">An OAuth access token.</param>
-        /// <param name="accessTokenSecret">An OAuth access token_secret.</param>
-        /// <returns>An instance of <see cref="Budgie.TwitterClient.TwitterAccessToken"/> with the supplied values.</returns>
-        public TwitterAccessToken Authenticate(string accessToken, string accessTokenSecret)
-        {
-            _accessToken = new TwitterAccessToken
-            {
-                Token = accessToken,
-                Secret = accessTokenSecret,
-            };
-            return _accessToken;
-        }
-
-        public Task<ITwitterResponse<TwitterUser>> VerifyCredentialsAsync()
-        {
-            return HttpGetAsync("account/verify_credentials.json?skip_status=true")
-                .RespondWith<TwitterUser>(content =>
-                    Deserialize<JsonUser>(content));
-        }
-
-        /// <summary>
-        /// Authenticates this <see cref="Budgie.TwitterClient"/> by requesting an OAuth access token and token_secret from Twitter.
-        /// </summary>
-        /// <param name="requestToken">An OAuth request token.</param>
-        /// <param name="verifier">A verification PIN obtained by the user from Twitter.</param>
-        /// <returns>An instance of <see cref="Budgie.TwitterClient.TwitterAccessToken"/> with the tokens returned from Twitter.</returns>
-        public async Task<TwitterAccessToken> AuthenticateAsync(TwitterRequestToken requestToken, string verifier)
-        {
-            var response = await HttpPostAsync("/oauth/access_token", "oauth_verifier=" + verifier, requestToken);
-            if (response.StatusCode != HttpStatusCode.OK) return null;
-
-            string responseString = await response.Content.ReadAsStringAsync();
-            if (String.IsNullOrWhiteSpace(responseString)) return null;
-
-            var result = new TwitterAccessToken();
-            foreach (var i in responseString.Split('&'))
-            {
-                var kv = i.Split('=');
-                if (kv.Length < 2) continue;
-
-                if (kv[0] == "oauth_token")
-                {
-                    result.Token = kv[1];
-                }
-                else if (kv[0] == "oauth_token_secret")
-                {
-                    result.Secret = kv[1];
-                }
-                else if (kv[0] == "user_id")
-                {
-                    result.UserId = kv[1];
-                }
-                else if (kv[0] == "screen_name")
-                {
-                    result.ScreenName = kv[1];
-                }
-            }
-
-            if (!result.IsValid) return null;
-
-            return (_accessToken = result);
-        }
-    }
-
-    internal class OAuthMessageHandler : DelegatingHandler
-    {
-        public OAuthMessageHandler(HttpMessageHandler innerHandler, string consumerKey, string consumerSecret, IPlatformAdaptor platformAdaptor)
-            : base(innerHandler)
-        {
-            _consumerKey = consumerKey;
-            _consumerSecret = consumerSecret;
-            _platformAdaptor = platformAdaptor;
-        }
-
-        IPlatformAdaptor _platformAdaptor;
-        string _consumerKey;
-        string _consumerSecret;
-
-        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
-        {
-            var tokens = request.Properties.ContainsKey("tokens") ? request.Properties["tokens"] as ITokenPair : null;
-            Uri callbackUri = request.Properties.ContainsKey("callbackUri") ? request.Properties["callbackUri"] as Uri : null;
-
-            var oauth_nonce = Convert.ToBase64String(UTF8Encoding.UTF8.GetBytes(DateTime.Now.Ticks.ToString()));
-            var timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
-            var oauth_timestamp = Convert.ToInt64(timeSpan.TotalSeconds).ToString();
-            var resource_url = request.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.Unescaped);
-
-            var dict = new Dictionary<string, string>
-            {
-                { "oauth_consumer_key",      _consumerKey },
-                { "oauth_nonce",             oauth_nonce },
-                { "oauth_signature_method",  "HMAC-SHA1" },
-                { "oauth_timestamp",         oauth_timestamp },
-                { "oauth_version",           "1.0" },
-            };
-
-            if (tokens != null)
-            {
-                dict.Add("oauth_token", tokens.Token);
-            }
-            if (callbackUri != null)
-            {
-                dict.Add("oauth_callback", Uri.EscapeDataString(callbackUri.ToString()));
-            }
-
-            if (!String.IsNullOrEmpty(request.RequestUri.Query))
-            {
-                foreach (var p in request.RequestUri.Query.Substring(1).Split('&'))
-                {
-                    var kv = p.Split('=');
-                    dict.Add(kv[0], kv[1]);
-                }
-            }
-
-            var body = request.Content as StringContent;
-            if (body != null)
-            {
-                var content = await body.ReadAsStringAsync();
-                foreach (var p in content.Split('&'))
-                {
-                    var kv = p.Split('=');
-                    dict.Add(kv[0], kv[1]);
-                }
-            }
-
-            var parms = dict.OrderBy((i) => i.Key);
-
-            var baseString = parms.First().Key + "=" + parms.First().Value;
-            foreach (var p in parms.Skip(1))
-            {
-                baseString += "&" + p.Key + "=" + p.Value;
-            }
-
-            baseString = request.Method + "&" + Uri.EscapeDataString(resource_url) + "&" + Uri.EscapeDataString(baseString);
-
-            var compositeKey = Uri.EscapeDataString(_consumerSecret) + "&";
-            if (tokens != null) compositeKey += Uri.EscapeDataString(tokens.Secret);
-
-            string oauth_signature;
-            oauth_signature = _platformAdaptor.ComputeSha1Hash(compositeKey, baseString);
-
-            string tokenParam = tokens == null ? "" : String.Format("oauth_token=\"{0}\", ", Uri.EscapeDataString(tokens.Token));
-            string callbackParam = callbackUri == null ? "" : String.Format("oauth_callback=\"{0}\", ", Uri.EscapeDataString(callbackUri.ToString()));
-
-            var authHeader = string.Format("{5}oauth_nonce=\"{0}\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"{1}\", oauth_consumer_key=\"{2}\", oauth_signature=\"{3}\", {4} oauth_version=\"1.0\"",
-                                    Uri.EscapeDataString(oauth_nonce),
-                                    Uri.EscapeDataString(oauth_timestamp),
-                                    Uri.EscapeDataString(_consumerKey),
-                                    Uri.EscapeDataString(oauth_signature),
-                                    tokenParam, callbackParam);
-            request.Headers.Authorization = new  System.Net.Http.Headers.AuthenticationHeaderValue("OAuth", authHeader);
-
-            return await base.SendAsync(request, cancellationToken);
-        }
-    }
-}

File Budgie/TwitterClient.SavedSearches.cs

 
 namespace Budgie
 {
-	partial class TwitterClient
+    using System.Net;
+    using SearchResponse = ITwitterResponse<IEnumerable<TwitterStatus>>;
+    
+    partial class TwitterClient
 	{
+        /// <summary>
+        /// Searches Twitter using the given search query and returns matching statuses.
+        /// </summary>
+        /// <param name="query">The query to issue to Twitter.</param>
+        /// <param name="count">The maximum number of tweets to return. If omitted, <seealso cref="DefaultPageSize" /> will be used.</param>
+        /// <param name="since">If specified, only tweets created after the tweet with this Id will be returned.</param>
+        /// <returns></returns>
+        public async Task<SearchResponse> SearchAsync(string query, int? count = null, long? since = null)
+        {
+            var relativeUri = "search/tweets.json?q=" + query.ToRfc3986Encoded() + "&include_entities=true" + "&count=" + (count ?? DefaultPageSize);
+
+            if (since.HasValue)
+            {
+                relativeUri += "&since_id=" + since;
+            }
+
+            var t = await HttpGetAsync(relativeUri);
+
+            var result = await t.ToTwitterResponse<IEnumerable<TwitterStatus>>();
+            if (result.StatusCode != HttpStatusCode.OK) return result;
+
+            result.Result = ((TwitterSearchResults)result.RawContent.Deserialize<JsonSearchResults>()).Results;
+            return result;
+        }
+
         public Task<ITwitterResponse<IEnumerable<TwitterSavedSearch>>> GetSavedSearchesAsync()
         {
             return HttpGetAsync("saved_searches/list.json")
-                .RespondWith<IEnumerable<TwitterSavedSearch>>(content =>
-                    Deserialize<IEnumerable<JsonSavedSearch>>(content)
-                        .Select(j => (TwitterSavedSearch)j)
-                        .ToList());
+                .RespondWith<IEnumerable<TwitterSavedSearch>>(content => 
+                    content.Deserialize<IEnumerable<JsonSavedSearch>>()
+                           .Select(j => (TwitterSavedSearch)j)
+                           .ToList());
         }
 
         public Task<ITwitterResponse<TwitterSavedSearch>> CreateSavedSearchAsync(string query)
         {
             return HttpPostAsync("saved_searches/create.json", "query=" + query.ToRfc3986Encoded())
-                .RespondWith<TwitterSavedSearch>(content =>
-                    Deserialize<JsonSavedSearch>(content));
+                .RespondWith<TwitterSavedSearch>(content => content.Deserialize<JsonSavedSearch>());
         }
 
         public Task<ITwitterResponse<TwitterSavedSearch>> DeleteSavedSearchAsync(long id)
         {
             return HttpPostAsync("saved_searches/destroy/" + id + ".json")
-                .RespondWith<TwitterSavedSearch>(content =>
-                    Deserialize<JsonSavedSearch>(content));
+                .RespondWith<TwitterSavedSearch>(content => content.Deserialize<JsonSavedSearch>());
         }
     }
 }

File Budgie/TwitterClient.Timelines.cs

 
             return HttpGetAsync(relativeUri)
                 .RespondWith<IEnumerable<TwitterStatus>>(content =>
-                    Deserialize<IEnumerable<JsonStatus>>(content).Select(j => (TwitterStatus)j).ToList());
+                    content.Deserialize<IEnumerable<JsonStatus>>()
+                           .Select(j => (TwitterStatus)j)
+                           .ToList());
         }
 
         public Task<TimelineResponse> GetUserTimelineAsync(string screenName, bool includeRetweets = false, int? count = null, long? since = null, long? max = null)

File Budgie/TwitterClient.Tweets.cs

             var relativeUri = "statuses/show/" + statusId + ".json?include_entities=true";
 
             return HttpGetAsync(relativeUri)
-                .RespondWith<TwitterStatus>(content => 
-                    Deserialize<JsonStatus>(content));
+                .RespondWith<TwitterStatus>(content =>
+                    content.Deserialize<JsonStatus>());
         }
 
         public Task<ITwitterResponse<IEnumerable<TwitterUser>>> GetRetweetersAsync(long statusId, int? count = null)
         {
-            var relativeUri = "statuses/" + statusId + "/retweeted_by.json";
-            if (count.HasValue) relativeUri += "?count=" + count;
-
+            var relativeUri = "statuses/" + statusId + "/retweeted_by.json" + "?count=" + (count ?? DefaultPageSize);
             return HttpGetAsync(relativeUri)
-                .RespondWith<IEnumerable<TwitterUser>>(content => 
-                    Deserialize<IEnumerable<JsonUser>>(content)
-                        .Select(j => (TwitterUser)j)
-                        .ToList());
+                .RespondWith<IEnumerable<TwitterUser>>(content =>
+                    content.Deserialize<IEnumerable<JsonUser>>()
+                           .Select(j => (TwitterUser)j)
+                           .ToList());
         }
 
         public Task<StatusResponse> DeleteStatusAsync(long id)
             var relativeUri = "statuses/destroy/" + id + ".json?include_entities=true";
 
             return HttpPostAsync(relativeUri)
-                .RespondWith<TwitterStatus>(content => 
-                    Deserialize<JsonStatus>(content));
+                .RespondWith<TwitterStatus>(content =>
+                    content.Deserialize<JsonStatus>());
         }
 
         public Task<StatusResponse> RetweetAsync(long id)
             var relativeUri = "statuses/retweet/" + id + ".json?include_entities=true";
 
             return HttpPostAsync(relativeUri)
-                .RespondWith<TwitterStatus>(content => 
-                    Deserialize<JsonStatus>(content));
+                .RespondWith<TwitterStatus>(content =>
+                    content.Deserialize<JsonStatus>());
         }
 
         public Task<StatusResponse> PostAsync(string text)
             var parms = "include_entities=true&status=" + text.ToRfc3986Encoded();
 
             return HttpPostAsync(relativeUri, parms)
-                .RespondWith<TwitterStatus>(content => 
-                    Deserialize<JsonStatus>(content));
+                .RespondWith<TwitterStatus>(content =>
+                    content.Deserialize<JsonStatus>());
         }
 
         public Task<StatusResponse> ReplyToAsync(long id, string text)
             var parms = "include_entities=true&in_reply_to_status_id=" + id + "&status=" + text.ToRfc3986Encoded();
 
             return HttpPostAsync(relativeUri, parms)
-                .RespondWith<TwitterStatus>(content => 
-                    Deserialize<JsonStatus>(content));
+                .RespondWith<TwitterStatus>(content =>
+                    content.Deserialize<JsonStatus>());
         }
 
         public Task<StatusResponse> PostAsync(string text, params byte[][] images)
 
             return _client.SendAsync(request)
                 .RespondWith<TwitterStatus>(content =>
-                    Deserialize<JsonStatus>(content));
+                    content.Deserialize<JsonStatus>());
         }
     }
 }

File Budgie/TwitterClient.Users.cs

             var response = await t.ToTwitterResponse<IEnumerable<TwitterUser>>(users);
             if (response.StatusCode != HttpStatusCode.OK) return response;
 
-            var userCursor = Deserialize<JsonUserCursor>(response.RawContent);
+            var userCursor = response.RawContent.Deserialize<JsonUserCursor>();
 
             foreach (var user in userCursor.users)
                 users.Add(user);
             var response = await t.ToTwitterResponse<IEnumerable<TwitterUser>>(users);
             if (response.StatusCode != HttpStatusCode.OK) return response;
 
-            foreach (var u in Deserialize<IEnumerable<JsonUser>>(response.RawContent))
+            foreach (var u in response.RawContent.Deserialize<IEnumerable<JsonUser>>())
             {
                 users.Add(u);
             }
 
             return HttpGetAsync("users/show.json?screen_name=" + screenName.ToRfc3986Encoded())
                 .RespondWith<TwitterUser>(content =>
-                    Deserialize<JsonUser>(content));
+                    content.Deserialize<JsonUser>());
         }
 
         public Task<UsersResponse> FindUsersAsync(string query, int? count = null)
         {
             return HttpGetAsync("users/search.json?per_page=" + (count ?? DefaultPageSize))
                 .RespondWith<IEnumerable<TwitterUser>>(content =>
-                    Deserialize<IEnumerable<JsonUser>>(content)
-                        .Select(u => (TwitterUser)u)
-                        .ToList());
+                    content.Deserialize<IEnumerable<JsonUser>>()
+                           .Select(u => (TwitterUser)u)
+                           .ToList());
         }
 
         public Task<ITwitterResponse<TwitterUser>> ReportSpammerAsync(string screenName)
         {
             return HttpPostAsync("report_spam.json", "screen_name=" + screenName.ToRfc3986Encoded())
                 .RespondWith<TwitterUser>(content =>
-                    Deserialize<JsonUser>(content));
+                    content.Deserialize<JsonUser>());
         }
     }
 }

File Budgie/TwitterClient.cs

 using Budgie.Extensions;
 using Budgie.Json;
-using Newtonsoft.Json;
 using System;
-using System.Collections.Generic;
 using System.Net;
 using System.Net.Http;
 using System.Threading.Tasks;
 
 namespace Budgie
 {
-    using SearchResponse = ITwitterResponse<IEnumerable<TwitterStatus>>;
-
     public partial class TwitterClient 
     {
-        public TwitterClient(IPlatformAdaptor platformAdaptor, string consumerKey, string consumerSecret)
+        internal TwitterClient(HttpClient httpClient, TwitterAccessToken accessToken, TwitterUser user = null)
         {
-            if (platformAdaptor == null) throw new ArgumentNullException("platformAdaptor");
-            if (String.IsNullOrWhiteSpace(consumerKey)) throw new ArgumentException("consumerKey");
-            if (String.IsNullOrWhiteSpace(consumerSecret)) throw new ArgumentException("consumerSecret");
+            _client = httpClient;
+            _accessToken = accessToken;
 
-            DefaultPageSize = 20;
-
-            _client = new HttpClient(new OAuthMessageHandler(new HttpClientHandler(), consumerKey, consumerSecret, platformAdaptor))
+            User = user ?? new TwitterUser
             {
-                BaseAddress = new Uri("https://api.twitter.com/1.1/"),
-                Timeout = TimeSpan.FromSeconds(100),
+                Id = accessToken.UserId,
+                ScreenName = accessToken.ScreenName,
             };
         }
 
         TwitterAccessToken _accessToken;
         HttpClient _client;
 
-        T Deserialize<T>(string value)
-        {
-            return JsonConvert.DeserializeObject<T>(value, new TwitterizerDateConverter());
-        }
-
         protected Task<HttpResponseMessage> HttpGetAsync(string relativeUri)
         {
             var request = new HttpRequestMessage(HttpMethod.Get, relativeUri);
             return _client.SendAsync(request);
         }
 
-        Task<HttpResponseMessage> HttpPostAsync(string relativeUri, string content = null, ITokenPair tokens = null)
+        Task<HttpResponseMessage> HttpPostAsync(string relativeUri, string content = null)
         {
             var request = new HttpRequestMessage(HttpMethod.Post, relativeUri);
-            request.Properties["tokens"] = tokens ?? _accessToken;
+            request.Properties["tokens"] = _accessToken;
             request.Content = new StringContent(content, System.Text.Encoding.UTF8, "application/x-www-form-urlencoded");
             return _client.SendAsync(request);
         }
 
-        Task<HttpResponseMessage> HttpPostAsync(string relativeUri, Uri callbackUri)
-        {
-            var request = new HttpRequestMessage(HttpMethod.Post, relativeUri);
-            request.Properties["callbackUri"] = callbackUri;
-            return _client.SendAsync(request);
-        }
-
         /// <summary>
         /// The number of statuses returned by any method if the count parameter is omitted. Default is 20.
         /// </summary>
         public int DefaultPageSize { get; set; }
 
+        public TwitterUser User { get; private set; }
+
         /// <summary>
         /// The timeout for HTTP requests.
         /// </summary>
         public TimeSpan Timeout
         {
             get { return _client.Timeout; }
-            set
-            {
-                _client.Timeout = value;
-            }
         }
 
         /// <summary>
-        /// Searches Twitter using the given search query and returns matching statuses.
+        /// Verifies this <see cref="Budgie.TwitterClient"/>'s access tokens and, if successful, populates its <seealso cref="User"/> property.
         /// </summary>
-        /// <param name="query">The query to issue to Twitter.</param>
-        /// <param name="count">The maximum number of tweets to return. If omitted, <see cref="DefaultPageSize" /> will be used.</param>
-        /// <param name="since">If specified, only tweets created after the tweet with this Id will be returned.</param>
         /// <returns></returns>
-        public async Task<SearchResponse> SearchAsync(string query, int? count = null, long? since = null)
+        public async Task<ITwitterResponse<TwitterUser>> VerifyCredentialsAsync()
         {
-            var relativeUri = "search/tweets.json?q=" + query.ToRfc3986Encoded() + "&include_entities=true";
+            var result = await HttpGetAsync("account/verify_credentials.json?skip_status=true")
+                .RespondWith<TwitterUser>(content =>
+                    content.Deserialize<JsonUser>());
 
-            if (count.HasValue)
+            if (result.StatusCode == HttpStatusCode.OK)
             {
-                relativeUri += "&count=" + count;
+                User = result.Result;
             }
 
-            if (since.HasValue)
-            {
-                relativeUri += "&since_id=" + since;
-            }
-
-            var t = await HttpGetAsync(relativeUri);
-            
-            var result = await t.ToTwitterResponse<IEnumerable<TwitterStatus>>();
-            if (result.StatusCode != HttpStatusCode.OK) return result;
-
-            result.Result = ((TwitterSearchResults)Deserialize<JsonSearchResults>(result.RawContent)).Results;
             return result;
         }
     }

File Budgie/TwitterFriendship.cs

-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
+
 namespace Budgie
 {
     public class TwitterFriendship

File Budgie/TwitterRateLimit.cs

 
 namespace Budgie
 {
+    /// <summary>
+    /// Represents the current limit and remaining requests for the time period in which the request was made.
+    /// </summary>
     public class TwitterRateLimit
     {
         internal
         {
         }
 
+        /// <summary>
+        /// The maximum number of requests allowed by Twitter in the allotted time.
+        /// </summary>
         public int Limit { get; internal set; }
+
+        /// <summary>
+        /// The number of allowed requests remaining in the allotted time.
+        /// </summary>
         public int Remaining { get; internal set; }
 
+        /// <summary>
+        /// Returns this instance in string form.
+        /// </summary>
+        /// <returns>A string in the format "Limits/Remaining".</returns>
         public override string ToString()
         {
             return Remaining + " / " + Limit;

File Budgie/TwitterStatus.cs

-using System;
-using Budgie.Entities;
+using Budgie.Entities;
+using System;
 
 namespace Budgie
 {