//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.ApplicationBlock { using System; using System.Collections.Generic; using System.Configuration; using System.Globalization; using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.Serialization.Json; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Xml; using System.Xml.Linq; using System.Xml.XPath; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.OAuth; using DotNetOpenAuth.OAuth.ChannelElements; using Newtonsoft.Json.Linq; /// /// A consumer capable of communicating with Twitter. /// public class TwitterConsumer : Consumer { /// /// The description of Twitter's OAuth protocol URIs for use with actually reading/writing /// a user's private Twitter data. /// public static readonly ServiceProviderDescription ServiceDescription = new ServiceProviderDescription( "https://api.twitter.com/oauth/request_token", "https://api.twitter.com/oauth/authorize", "https://api.twitter.com/oauth/access_token"); /// /// The description of Twitter's OAuth protocol URIs for use with their "Sign in with Twitter" feature. /// public static readonly ServiceProviderDescription SignInWithTwitterServiceDescription = new ServiceProviderDescription( "https://api.twitter.com/oauth/request_token", "https://api.twitter.com/oauth/authenticate", "https://api.twitter.com/oauth/access_token"); /// /// The URI to get a user's favorites. /// private static readonly Uri GetFavoritesEndpoint = new Uri("http://twitter.com/favorites.xml"); /// /// The URI to get the data on the user's home page. /// private static readonly Uri GetFriendTimelineStatusEndpoint = new Uri("https://api.twitter.com/1.1/statuses/home_timeline.json"); private static readonly Uri UpdateProfileBackgroundImageEndpoint = new Uri("http://twitter.com/account/update_profile_background_image.xml"); private static readonly Uri UpdateProfileImageEndpoint = new Uri("http://twitter.com/account/update_profile_image.xml"); private static readonly Uri VerifyCredentialsEndpoint = new Uri("http://api.twitter.com/1/account/verify_credentials.xml"); /// /// Initializes a new instance of the class. /// public TwitterConsumer() { this.ServiceProvider = ServiceDescription; this.ConsumerKey = ConfigurationManager.AppSettings["twitterConsumerKey"]; this.ConsumerSecret = ConfigurationManager.AppSettings["twitterConsumerSecret"]; this.TemporaryCredentialStorage = HttpContext.Current != null ? (ITemporaryCredentialStorage)new CookieTemporaryCredentialStorage() : new MemoryTemporaryCredentialStorage(); } /// /// Initializes a new instance of the class. /// /// The consumer key. /// The consumer secret. public TwitterConsumer(string consumerKey, string consumerSecret) : this() { this.ConsumerKey = consumerKey; this.ConsumerSecret = consumerSecret; } /// /// Gets a value indicating whether the Twitter consumer key and secret are set in the web.config file. /// public static bool IsTwitterConsumerConfigured { get { return !string.IsNullOrEmpty(ConfigurationManager.AppSettings["twitterConsumerKey"]) && !string.IsNullOrEmpty(ConfigurationManager.AppSettings["twitterConsumerSecret"]); } } public static Consumer CreateConsumer(bool forWeb = true) { string consumerKey = ConfigurationManager.AppSettings["twitterConsumerKey"]; string consumerSecret = ConfigurationManager.AppSettings["twitterConsumerSecret"]; if (IsTwitterConsumerConfigured) { ITemporaryCredentialStorage storage = forWeb ? (ITemporaryCredentialStorage)new CookieTemporaryCredentialStorage() : new MemoryTemporaryCredentialStorage(); return new Consumer(consumerKey, consumerSecret, ServiceDescription, storage) { HostFactories = new TwitterHostFactories(), }; } else { throw new InvalidOperationException("No Twitter OAuth consumer key and secret could be found in web.config AppSettings."); } } /// /// Prepares a redirect that will send the user to Twitter to sign in. /// /// if set to true the user will be required to re-enter their Twitter credentials even if already logged in to Twitter. /// The cancellation token. /// /// The redirect message. /// public static async Task StartSignInWithTwitterAsync(bool forceNewLogin = false, CancellationToken cancellationToken = default(CancellationToken)) { var redirectParameters = new Dictionary(); if (forceNewLogin) { redirectParameters["force_login"] = "true"; } Uri callback = MessagingUtilities.GetRequestUrlFromContext().StripQueryArgumentsWithPrefix("oauth_"); var consumer = CreateConsumer(); consumer.ServiceProvider = SignInWithTwitterServiceDescription; Uri redirectUrl = await consumer.RequestUserAuthorizationAsync(callback, cancellationToken: cancellationToken); return redirectUrl; } /// /// Checks the incoming web request to see if it carries a Twitter authentication response, /// and provides the user's Twitter screen name and unique id if available. /// /// The URL that came back from the service provider to complete the authorization. /// The cancellation token. /// /// A tuple with the screen name and Twitter unique user ID if successful; otherwise null. /// public static async Task> TryFinishSignInWithTwitterAsync(Uri completeUrl = null, CancellationToken cancellationToken = default(CancellationToken)) { var consumer = CreateConsumer(); consumer.ServiceProvider = SignInWithTwitterServiceDescription; var response = await consumer.ProcessUserAuthorizationAsync(completeUrl ?? HttpContext.Current.Request.Url, cancellationToken: cancellationToken); if (response == null) { return null; } string screenName = response.ExtraData["screen_name"]; int userId = int.Parse(response.ExtraData["user_id"]); return Tuple.Create(screenName, userId); } public async Task GetUpdatesAsync(AccessToken accessToken, CancellationToken cancellationToken = default(CancellationToken)) { if (string.IsNullOrEmpty(accessToken.Token)) { throw new ArgumentNullException("accessToken.Token"); } using (var httpClient = this.CreateHttpClient(accessToken)) { using (var response = await httpClient.GetAsync(GetFriendTimelineStatusEndpoint, cancellationToken)) { response.EnsureSuccessStatusCode(); string jsonString = await response.Content.ReadAsStringAsync(); var json = JArray.Parse(jsonString); return json; } } } public async Task GetFavorites(AccessToken accessToken, CancellationToken cancellationToken = default(CancellationToken)) { if (string.IsNullOrEmpty(accessToken.Token)) { throw new ArgumentNullException("accessToken.Token"); } using (var httpClient = this.CreateHttpClient(accessToken)) { using (HttpResponseMessage response = await httpClient.GetAsync(GetFavoritesEndpoint, cancellationToken)) { response.EnsureSuccessStatusCode(); return XDocument.Parse(await response.Content.ReadAsStringAsync()); } } } public async Task UpdateProfileBackgroundImageAsync(AccessToken accessToken, string image, bool tile, CancellationToken cancellationToken) { var imageAttachment = new StreamContent(File.OpenRead(image)); imageAttachment.Headers.ContentType = new MediaTypeHeaderValue("image/" + Path.GetExtension(image).Substring(1).ToLowerInvariant()); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, UpdateProfileBackgroundImageEndpoint); var content = new MultipartFormDataContent(); content.Add(imageAttachment, "image"); content.Add(new StringContent(tile.ToString().ToLowerInvariant()), "tile"); request.Content = content; request.Headers.ExpectContinue = false; using (var httpClient = this.CreateHttpClient(accessToken)) { using (HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken)) { response.EnsureSuccessStatusCode(); string responseString = await response.Content.ReadAsStringAsync(); return XDocument.Parse(responseString); } } } public Task UpdateProfileImageAsync(AccessToken accessToken, string pathToImage, CancellationToken cancellationToken = default(CancellationToken)) { string contentType = "image/" + Path.GetExtension(pathToImage).Substring(1).ToLowerInvariant(); return this.UpdateProfileImageAsync(accessToken, File.OpenRead(pathToImage), contentType, cancellationToken); } public async Task UpdateProfileImageAsync(AccessToken accessToken, Stream image, string contentType, CancellationToken cancellationToken = default(CancellationToken)) { var imageAttachment = new StreamContent(image); imageAttachment.Headers.ContentType = new MediaTypeHeaderValue(contentType); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, UpdateProfileImageEndpoint); var content = new MultipartFormDataContent(); content.Add(imageAttachment, "image", "twitterPhoto"); request.Content = content; using (var httpClient = this.CreateHttpClient(accessToken)) { using (HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken)) { response.EnsureSuccessStatusCode(); string responseString = await response.Content.ReadAsStringAsync(); return XDocument.Parse(responseString); } } } public async Task VerifyCredentialsAsync(AccessToken accessToken, CancellationToken cancellationToken = default(CancellationToken)) { using (var httpClient = this.CreateHttpClient(accessToken)) { using (var response = await httpClient.GetAsync(VerifyCredentialsEndpoint, cancellationToken)) { response.EnsureSuccessStatusCode(); using (var stream = await response.Content.ReadAsStreamAsync()) { return XDocument.Load(XmlReader.Create(stream)); } } } } public async Task GetUsername(AccessToken accessToken, CancellationToken cancellationToken = default(CancellationToken)) { XDocument xml = await this.VerifyCredentialsAsync(accessToken, cancellationToken); XPathNavigator nav = xml.CreateNavigator(); return nav.SelectSingleNode("/user/screen_name").Value; } private class TwitterHostFactories : IHostFactories { private static readonly IHostFactories underlyingFactories = new DefaultOAuthHostFactories(); public HttpMessageHandler CreateHttpMessageHandler() { return new WebRequestHandler(); } public HttpClient CreateHttpClient(HttpMessageHandler handler = null) { var client = underlyingFactories.CreateHttpClient(handler); // Twitter can't handle the Expect 100 Continue HTTP header. client.DefaultRequestHeaders.ExpectContinue = false; return client; } } } }