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.
25 using System.Collections.Generic;
29 using System.Net.Cache;
30 using System.Net.Http;
31 using System.Runtime.Serialization;
33 using System.Threading;
34 using System.Threading.Tasks;
37 using OpenTween.Api.DataModel;
39 namespace OpenTween.Connection
41 public class TwitterApiConnection : IApiConnection, IDisposable
43 public static Uri RestApiBase { get; set; } = new Uri("https://api.twitter.com/1.1/");
45 // SettingCommon.xml の TwitterUrl との互換性のために用意
46 public static string RestApiHost
48 get => RestApiBase.Host;
49 set => RestApiBase = new Uri($"https://{value}/1.1/");
52 public bool IsDisposed { get; private set; } = false;
54 public string AccessToken { get; }
55 public string AccessSecret { get; }
57 internal HttpClient http = null!;
58 internal HttpClient httpUpload = null!;
59 internal HttpClient httpStreaming = null!;
61 public TwitterApiConnection(string accessToken, string accessSecret)
63 this.AccessToken = accessToken;
64 this.AccessSecret = accessSecret;
66 this.InitializeHttpClients();
67 Networking.WebProxyChanged += this.Networking_WebProxyChanged;
70 private void InitializeHttpClients()
72 this.http = InitializeHttpClient(this.AccessToken, this.AccessSecret);
74 this.httpUpload = InitializeHttpClient(this.AccessToken, this.AccessSecret);
75 this.httpUpload.Timeout = Networking.UploadImageTimeout;
77 this.httpStreaming = InitializeHttpClient(this.AccessToken, this.AccessSecret, disableGzip: true);
78 this.httpStreaming.Timeout = Timeout.InfiniteTimeSpan;
81 public async Task<T> GetAsync<T>(Uri uri, IDictionary<string, string>? param, string? endpointName)
83 // レートリミット規制中はAPIリクエストを送信せずに直ちにエラーを発生させる
84 if (endpointName != null)
85 this.ThrowIfRateLimitExceeded(endpointName);
87 var requestUri = new Uri(RestApiBase, uri);
90 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
92 var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
96 using var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
97 .ConfigureAwait(false);
99 if (endpointName != null)
100 MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName);
102 await this.CheckStatusCode(response)
103 .ConfigureAwait(false);
105 using var content = response.Content;
106 var responseText = await content.ReadAsStringAsync()
107 .ConfigureAwait(false);
111 return MyCommon.CreateDataFromJson<T>(responseText);
113 catch (SerializationException ex)
115 throw TwitterApiException.CreateFromException(ex, responseText);
118 catch (HttpRequestException ex)
120 throw TwitterApiException.CreateFromException(ex);
122 catch (OperationCanceledException ex)
124 throw TwitterApiException.CreateFromException(ex);
129 /// 指定されたエンドポイントがレートリミット規制中であれば例外を発生させる
131 private void ThrowIfRateLimitExceeded(string endpointName)
133 var limit = MyCommon.TwitterApiInfo.AccessLimit[endpointName];
137 if (limit.AccessLimitRemain == 0 && limit.AccessLimitResetDate > DateTimeUtc.Now)
139 var error = new TwitterError
143 new TwitterErrorItem { Code = TwitterErrorCode.RateLimit, Message = "" },
146 throw new TwitterApiException(0, error, "");
150 public async Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param)
152 var requestUri = new Uri(RestApiBase, uri);
155 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
159 return await this.http.GetStreamAsync(requestUri)
160 .ConfigureAwait(false);
162 catch (HttpRequestException ex)
164 throw TwitterApiException.CreateFromException(ex);
166 catch (OperationCanceledException ex)
168 throw TwitterApiException.CreateFromException(ex);
172 public async Task<Stream> GetStreamingStreamAsync(Uri uri, IDictionary<string, string>? param)
174 var requestUri = new Uri(RestApiBase, uri);
177 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
181 var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
182 var response = await this.httpStreaming.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
183 .ConfigureAwait(false);
185 await this.CheckStatusCode(response)
186 .ConfigureAwait(false);
188 return await response.Content.ReadAsStreamAsync()
189 .ConfigureAwait(false);
191 catch (HttpRequestException ex)
193 throw TwitterApiException.CreateFromException(ex);
195 catch (OperationCanceledException ex)
197 throw TwitterApiException.CreateFromException(ex);
201 public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string>? param)
203 var requestUri = new Uri(RestApiBase, uri);
204 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
206 using var postContent = new FormUrlEncodedContent(param);
207 request.Content = postContent;
209 HttpResponseMessage? response = null;
212 response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
213 .ConfigureAwait(false);
215 await this.CheckStatusCode(response)
216 .ConfigureAwait(false);
218 var result = new LazyJson<T>(response);
223 catch (HttpRequestException ex)
225 throw TwitterApiException.CreateFromException(ex);
227 catch (OperationCanceledException ex)
229 throw TwitterApiException.CreateFromException(ex);
237 public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string>? param, IDictionary<string, IMediaItem>? media)
239 var requestUri = new Uri(RestApiBase, uri);
240 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
242 using var postContent = new MultipartFormDataContent();
245 foreach (var (key, value) in param)
246 postContent.Add(new StringContent(value), key);
250 foreach (var (key, value) in media)
251 postContent.Add(new StreamContent(value.OpenRead()), key, value.Name);
254 request.Content = postContent;
256 HttpResponseMessage? response = null;
259 response = await this.httpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
260 .ConfigureAwait(false);
262 await this.CheckStatusCode(response)
263 .ConfigureAwait(false);
265 var result = new LazyJson<T>(response);
270 catch (HttpRequestException ex)
272 throw TwitterApiException.CreateFromException(ex);
274 catch (OperationCanceledException ex)
276 throw TwitterApiException.CreateFromException(ex);
284 public async Task PostAsync(Uri uri, IDictionary<string, string>? param, IDictionary<string, IMediaItem>? media)
286 var requestUri = new Uri(RestApiBase, uri);
287 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
289 using var postContent = new MultipartFormDataContent();
292 foreach (var (key, value) in param)
293 postContent.Add(new StringContent(value), key);
297 foreach (var (key, value) in media)
298 postContent.Add(new StreamContent(value.OpenRead()), key, value.Name);
301 request.Content = postContent;
305 using var response = await this.httpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
306 .ConfigureAwait(false);
308 await this.CheckStatusCode(response)
309 .ConfigureAwait(false);
311 catch (HttpRequestException ex)
313 throw TwitterApiException.CreateFromException(ex);
315 catch (OperationCanceledException ex)
317 throw TwitterApiException.CreateFromException(ex);
321 public async Task PostJsonAsync(Uri uri, string json)
322 => await this.PostJsonAsync<object>(uri, json)
324 .ConfigureAwait(false);
326 public async Task<LazyJson<T>> PostJsonAsync<T>(Uri uri, string json)
328 var requestUri = new Uri(RestApiBase, uri);
329 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
331 using var postContent = new StringContent(json, Encoding.UTF8, "application/json");
332 request.Content = postContent;
334 HttpResponseMessage? response = null;
337 response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
338 .ConfigureAwait(false);
340 await this.CheckStatusCode(response)
341 .ConfigureAwait(false);
343 var result = new LazyJson<T>(response);
348 catch (HttpRequestException ex)
350 throw TwitterApiException.CreateFromException(ex);
352 catch (OperationCanceledException ex)
354 throw TwitterApiException.CreateFromException(ex);
362 public async Task DeleteAsync(Uri uri)
364 var requestUri = new Uri(RestApiBase, uri);
365 using var request = new HttpRequestMessage(HttpMethod.Delete, requestUri);
369 using var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
370 .ConfigureAwait(false);
372 await this.CheckStatusCode(response)
373 .ConfigureAwait(false);
375 catch (HttpRequestException ex)
377 throw TwitterApiException.CreateFromException(ex);
379 catch (OperationCanceledException ex)
381 throw TwitterApiException.CreateFromException(ex);
385 protected async Task CheckStatusCode(HttpResponseMessage response)
387 var statusCode = response.StatusCode;
389 if ((int)statusCode >= 200 && (int)statusCode <= 299)
391 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
396 using (var content = response.Content)
398 responseText = await content.ReadAsStringAsync()
399 .ConfigureAwait(false);
402 if (string.IsNullOrWhiteSpace(responseText))
404 if (statusCode == HttpStatusCode.Unauthorized)
405 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
407 throw new TwitterApiException(statusCode, responseText);
412 var error = TwitterError.ParseJson(responseText);
414 if (error?.Errors == null || error.Errors.Length == 0)
415 throw new TwitterApiException(statusCode, responseText);
417 var errorCodes = error.Errors.Select(x => x.Code);
418 if (errorCodes.Any(x => x == TwitterErrorCode.InternalError || x == TwitterErrorCode.SuspendedAccount))
420 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
423 throw new TwitterApiException(statusCode, error, responseText);
425 catch (SerializationException)
427 throw new TwitterApiException(statusCode, responseText);
431 public OAuthEchoHandler CreateOAuthEchoHandler(Uri authServiceProvider, Uri? realm = null)
433 var uri = new Uri(RestApiBase, authServiceProvider);
435 return OAuthEchoHandler.CreateHandler(Networking.CreateHttpClientHandler(), uri,
436 ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret,
437 this.AccessToken, this.AccessSecret, realm);
440 public void Dispose()
443 GC.SuppressFinalize(this);
446 protected virtual void Dispose(bool disposing)
451 this.IsDisposed = true;
455 Networking.WebProxyChanged -= this.Networking_WebProxyChanged;
457 this.httpUpload.Dispose();
458 this.httpStreaming.Dispose();
462 ~TwitterApiConnection()
463 => this.Dispose(false);
465 private void Networking_WebProxyChanged(object sender, EventArgs e)
466 => this.InitializeHttpClients();
468 public static async Task<(string Token, string TokenSecret)> GetRequestTokenAsync()
470 var param = new Dictionary<string, string>
472 ["oauth_callback"] = "oob",
474 var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/request_token"), param, oauthToken: null)
475 .ConfigureAwait(false);
477 return (response["oauth_token"], response["oauth_token_secret"]);
480 public static Uri GetAuthorizeUri((string Token, string TokenSecret) requestToken, string? screenName = null)
482 var param = new Dictionary<string, string>
484 ["oauth_token"] = requestToken.Token,
487 if (screenName != null)
488 param["screen_name"] = screenName;
490 return new Uri("https://api.twitter.com/oauth/authorize?" + MyCommon.BuildQueryString(param));
493 public static async Task<IDictionary<string, string>> GetAccessTokenAsync((string Token, string TokenSecret) requestToken, string verifier)
495 var param = new Dictionary<string, string>
497 ["oauth_verifier"] = verifier,
499 var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/access_token"), param, requestToken)
500 .ConfigureAwait(false);
505 private static async Task<IDictionary<string, string>> GetOAuthTokenAsync(Uri uri, IDictionary<string, string> param,
506 (string Token, string TokenSecret)? oauthToken)
508 HttpClient authorizeClient;
509 if (oauthToken != null)
510 authorizeClient = InitializeHttpClient(oauthToken.Value.Token, oauthToken.Value.TokenSecret);
512 authorizeClient = InitializeHttpClient("", "");
514 var requestUri = new Uri(uri, "?" + MyCommon.BuildQueryString(param));
518 using var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
519 using var response = await authorizeClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
520 .ConfigureAwait(false);
522 using var content = response.Content;
523 var responseText = await content.ReadAsStringAsync()
524 .ConfigureAwait(false);
526 if (!response.IsSuccessStatusCode)
527 throw new TwitterApiException(response.StatusCode, responseText);
529 var responseParams = HttpUtility.ParseQueryString(responseText);
531 return responseParams.Cast<string>()
532 .ToDictionary(x => x, x => responseParams[x]);
534 catch (HttpRequestException ex)
536 throw TwitterApiException.CreateFromException(ex);
538 catch (OperationCanceledException ex)
540 throw TwitterApiException.CreateFromException(ex);
544 private static HttpClient InitializeHttpClient(string accessToken, string accessSecret, bool disableGzip = false)
546 var innerHandler = Networking.CreateHttpClientHandler();
547 innerHandler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
550 innerHandler.AutomaticDecompression = DecompressionMethods.None;
552 var handler = new OAuthHandler(innerHandler,
553 ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret,
554 accessToken, accessSecret);
556 return Networking.CreateHttpClient(handler);