1 // OpenTween - Client of Twitter
2 // Copyright (c) 2016 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
5 // This file is part of OpenTween.
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General Public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
17 // You should have received a copy of the GNU General Public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
23 using System.Collections.Generic;
27 using System.Net.Cache;
28 using System.Net.Http;
29 using System.Runtime.Serialization;
31 using System.Threading;
32 using System.Threading.Tasks;
35 using OpenTween.Api.DataModel;
37 namespace OpenTween.Connection
39 public class TwitterApiConnection : IApiConnection, IDisposable
41 public static Uri RestApiBase { get; set; } = new Uri("https://api.twitter.com/1.1/");
43 // SettingCommon.xml の TwitterUrl との互換性のために用意
44 public static string RestApiHost
46 get => RestApiBase.Host;
47 set => RestApiBase = new Uri($"https://{value}/1.1/");
50 public bool IsDisposed { get; private set; } = false;
52 public string AccessToken { get; }
53 public string AccessSecret { get; }
55 internal HttpClient http;
56 internal HttpClient httpUpload;
57 internal HttpClient httpStreaming;
59 public TwitterApiConnection(string accessToken, string accessSecret)
61 this.AccessToken = accessToken;
62 this.AccessSecret = accessSecret;
64 this.InitializeHttpClients();
65 Networking.WebProxyChanged += this.Networking_WebProxyChanged;
68 private void InitializeHttpClients()
70 this.http = InitializeHttpClient(this.AccessToken, this.AccessSecret);
72 this.httpUpload = InitializeHttpClient(this.AccessToken, this.AccessSecret);
73 this.httpUpload.Timeout = Networking.UploadImageTimeout;
75 this.httpStreaming = InitializeHttpClient(this.AccessToken, this.AccessSecret, disableGzip: true);
76 this.httpStreaming.Timeout = Timeout.InfiniteTimeSpan;
79 public async Task<T> GetAsync<T>(Uri uri, IDictionary<string, string> param, string endpointName)
81 var requestUri = new Uri(RestApiBase, uri);
84 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
86 var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
90 using (var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
91 .ConfigureAwait(false))
93 await this.CheckStatusCode(response)
94 .ConfigureAwait(false);
96 if (endpointName != null)
97 MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName);
99 using (var content = response.Content)
101 var responseText = await content.ReadAsStringAsync()
102 .ConfigureAwait(false);
106 return MyCommon.CreateDataFromJson<T>(responseText);
108 catch (SerializationException ex)
110 throw TwitterApiException.CreateFromException(ex, responseText);
115 catch (HttpRequestException ex)
117 throw TwitterApiException.CreateFromException(ex);
119 catch (OperationCanceledException ex)
121 throw TwitterApiException.CreateFromException(ex);
125 public async Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string> param)
127 var requestUri = new Uri(RestApiBase, uri);
130 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
134 return await this.http.GetStreamAsync(requestUri)
135 .ConfigureAwait(false);
137 catch (HttpRequestException ex)
139 throw TwitterApiException.CreateFromException(ex);
141 catch (OperationCanceledException ex)
143 throw TwitterApiException.CreateFromException(ex);
147 public async Task<Stream> GetStreamingStreamAsync(Uri uri, IDictionary<string, string> param)
149 var requestUri = new Uri(RestApiBase, uri);
152 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
156 return await this.httpStreaming.GetStreamAsync(requestUri)
157 .ConfigureAwait(false);
159 catch (HttpRequestException ex)
161 throw TwitterApiException.CreateFromException(ex);
163 catch (OperationCanceledException ex)
165 throw TwitterApiException.CreateFromException(ex);
169 public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string> param)
171 var requestUri = new Uri(RestApiBase, uri);
172 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
174 using (var postContent = new FormUrlEncodedContent(param))
176 request.Content = postContent;
178 HttpResponseMessage response = null;
181 response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
182 .ConfigureAwait(false);
184 await this.CheckStatusCode(response)
185 .ConfigureAwait(false);
187 var result = new LazyJson<T>(response);
192 catch (HttpRequestException ex)
194 throw TwitterApiException.CreateFromException(ex);
196 catch (OperationCanceledException ex)
198 throw TwitterApiException.CreateFromException(ex);
207 public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string> param, IDictionary<string, IMediaItem> media)
209 var requestUri = new Uri(RestApiBase, uri);
210 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
212 using (var postContent = new MultipartFormDataContent())
216 foreach (var (key, value) in param)
217 postContent.Add(new StringContent(value), key);
221 foreach (var (key, value) in media)
222 postContent.Add(new StreamContent(value.OpenRead()), key, value.Name);
225 request.Content = postContent;
227 HttpResponseMessage response = null;
230 response = await this.httpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
231 .ConfigureAwait(false);
233 await this.CheckStatusCode(response)
234 .ConfigureAwait(false);
236 var result = new LazyJson<T>(response);
241 catch (HttpRequestException ex)
243 throw TwitterApiException.CreateFromException(ex);
245 catch (OperationCanceledException ex)
247 throw TwitterApiException.CreateFromException(ex);
256 public async Task PostJsonAsync(Uri uri, string json)
258 var requestUri = new Uri(RestApiBase, uri);
259 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
261 using (var postContent = new StringContent(json, Encoding.UTF8, "application/json"))
263 request.Content = postContent;
267 using (var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
268 .ConfigureAwait(false))
270 await this.CheckStatusCode(response)
271 .ConfigureAwait(false);
274 catch (HttpRequestException ex)
276 throw TwitterApiException.CreateFromException(ex);
278 catch (OperationCanceledException ex)
280 throw TwitterApiException.CreateFromException(ex);
285 protected async Task CheckStatusCode(HttpResponseMessage response)
287 var statusCode = response.StatusCode;
288 if (statusCode == HttpStatusCode.OK)
290 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
295 using (var content = response.Content)
297 responseText = await content.ReadAsStringAsync()
298 .ConfigureAwait(false);
301 if (string.IsNullOrWhiteSpace(responseText))
303 if (statusCode == HttpStatusCode.Unauthorized)
304 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
306 throw new TwitterApiException(statusCode, responseText);
311 var error = TwitterError.ParseJson(responseText);
313 if (error?.Errors == null || error.Errors.Length == 0)
314 throw new TwitterApiException(statusCode, responseText);
316 var errorCodes = error.Errors.Select(x => x.Code);
317 if (errorCodes.Any(x => x == TwitterErrorCode.InternalError || x == TwitterErrorCode.SuspendedAccount))
319 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
322 throw new TwitterApiException(error, responseText);
324 catch (SerializationException)
326 throw new TwitterApiException(statusCode, responseText);
330 public OAuthEchoHandler CreateOAuthEchoHandler(Uri authServiceProvider, Uri realm = null)
332 var uri = new Uri(RestApiBase, authServiceProvider);
334 return OAuthEchoHandler.CreateHandler(Networking.CreateHttpClientHandler(), uri,
335 ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret,
336 this.AccessToken, this.AccessSecret, realm);
339 public void Dispose()
342 GC.SuppressFinalize(this);
345 protected virtual void Dispose(bool disposing)
350 this.IsDisposed = true;
354 Networking.WebProxyChanged -= this.Networking_WebProxyChanged;
356 this.httpStreaming.Dispose();
360 ~TwitterApiConnection()
361 => this.Dispose(false);
363 private void Networking_WebProxyChanged(object sender, EventArgs e)
364 => this.InitializeHttpClients();
366 public static async Task<Tuple<string, string>> GetRequestTokenAsync()
368 var param = new Dictionary<string, string>
370 ["oauth_callback"] = "oob",
372 var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/request_token"), param, oauthToken: null)
373 .ConfigureAwait(false);
375 return Tuple.Create(response["oauth_token"], response["oauth_token_secret"]);
378 public static Uri GetAuthorizeUri(Tuple<string, string> requestToken, string screenName = null)
380 var param = new Dictionary<string, string>
382 ["oauth_token"] = requestToken.Item1,
385 if (screenName != null)
386 param["screen_name"] = screenName;
388 return new Uri("https://api.twitter.com/oauth/authorize?" + MyCommon.BuildQueryString(param));
391 public static async Task<IDictionary<string, string>> GetAccessTokenAsync(Tuple<string, string> requestToken, string verifier)
393 var param = new Dictionary<string, string>
395 ["oauth_verifier"] = verifier,
397 var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/access_token"), param, requestToken)
398 .ConfigureAwait(false);
403 private static async Task<IDictionary<string, string>> GetOAuthTokenAsync(Uri uri, IDictionary<string, string> param,
404 Tuple<string, string> oauthToken)
406 HttpClient authorizeClient;
407 if (oauthToken != null)
408 authorizeClient = InitializeHttpClient(oauthToken.Item1, oauthToken.Item2);
410 authorizeClient = InitializeHttpClient("", "");
412 var requestUri = new Uri(uri, "?" + MyCommon.BuildQueryString(param));
416 using (var request = new HttpRequestMessage(HttpMethod.Post, requestUri))
417 using (var response = await authorizeClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
418 .ConfigureAwait(false))
419 using (var content = response.Content)
421 var responseText = await content.ReadAsStringAsync()
422 .ConfigureAwait(false);
424 if (!response.IsSuccessStatusCode)
425 throw new TwitterApiException(response.StatusCode, responseText);
427 var responseParams = HttpUtility.ParseQueryString(responseText);
429 return responseParams.Cast<string>()
430 .ToDictionary(x => x, x => responseParams[x]);
433 catch (HttpRequestException ex)
435 throw TwitterApiException.CreateFromException(ex);
437 catch (OperationCanceledException ex)
439 throw TwitterApiException.CreateFromException(ex);
443 private static HttpClient InitializeHttpClient(string accessToken, string accessSecret, bool disableGzip = false)
445 var innerHandler = Networking.CreateHttpClientHandler();
446 innerHandler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
449 innerHandler.AutomaticDecompression = DecompressionMethods.None;
451 var handler = new OAuthHandler(innerHandler,
452 ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret,
453 accessToken, accessSecret);
455 return Networking.CreateHttpClient(handler);