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;
26 using System.Diagnostics.CodeAnalysis;
30 using System.Net.Cache;
31 using System.Net.Http;
32 using System.Runtime.Serialization;
34 using System.Threading;
35 using System.Threading.Tasks;
38 using OpenTween.Api.DataModel;
40 namespace OpenTween.Connection
42 public class TwitterApiConnection : IApiConnection, IApiConnectionLegacy, IDisposable
44 public static Uri RestApiBase { get; set; } = new("https://api.twitter.com/1.1/");
46 // SettingCommon.xml の TwitterUrl との互換性のために用意
47 public static string RestApiHost
49 get => RestApiBase.Host;
50 set => RestApiBase = new Uri($"https://{value}/1.1/");
53 public bool IsDisposed { get; private set; } = false;
55 internal HttpClient Http;
56 internal HttpClient HttpUpload;
57 internal HttpClient HttpStreaming;
59 internal ITwitterCredential Credential { get; }
61 public TwitterApiConnection()
62 : this(new TwitterCredentialNone())
66 public TwitterApiConnection(ITwitterCredential credential)
68 this.Credential = credential;
70 this.InitializeHttpClients();
71 Networking.WebProxyChanged += this.Networking_WebProxyChanged;
74 [MemberNotNull(nameof(Http), nameof(HttpUpload), nameof(HttpStreaming))]
75 private void InitializeHttpClients()
77 this.Http = InitializeHttpClient(this.Credential);
79 this.HttpUpload = InitializeHttpClient(this.Credential);
80 this.HttpUpload.Timeout = Networking.UploadImageTimeout;
82 this.HttpStreaming = InitializeHttpClient(this.Credential, disableGzip: true);
83 this.HttpStreaming.Timeout = Timeout.InfiniteTimeSpan;
86 public async Task<ApiResponse> SendAsync(IHttpRequest request)
88 var endpointName = request.EndpointName;
90 // レートリミット規制中はAPIリクエストを送信せずに直ちにエラーを発生させる
91 if (endpointName != null)
92 this.ThrowIfRateLimitExceeded(endpointName);
94 using var requestMessage = request.CreateMessage(RestApiBase);
96 HttpResponseMessage? responseMessage = null;
99 responseMessage = await HandleTimeout(
100 (token) => this.Http.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, token),
101 Networking.DefaultTimeout
104 if (endpointName != null)
105 MyCommon.TwitterApiInfo.UpdateFromHeader(responseMessage.Headers, endpointName);
107 await TwitterApiConnection.CheckStatusCode(responseMessage)
108 .ConfigureAwait(false);
110 var response = new ApiResponse(responseMessage);
111 responseMessage = null; // responseMessage は ApiResponse で使用するため破棄されないようにする
115 catch (HttpRequestException ex)
117 throw TwitterApiException.CreateFromException(ex);
119 catch (OperationCanceledException ex)
121 throw TwitterApiException.CreateFromException(ex);
125 responseMessage?.Dispose();
129 public async Task<T> GetAsync<T>(Uri uri, IDictionary<string, string>? param, string? endpointName)
131 var request = new GetRequest
135 EndpointName = endpointName,
138 using var response = await this.SendAsync(request)
139 .ConfigureAwait(false);
141 return await response.ReadAsJson<T>()
142 .ConfigureAwait(false);
146 /// 指定されたエンドポイントがレートリミット規制中であれば例外を発生させる
148 private void ThrowIfRateLimitExceeded(string endpointName)
150 var limit = MyCommon.TwitterApiInfo.AccessLimit[endpointName];
154 if (limit.AccessLimitRemain == 0 && limit.AccessLimitResetDate > DateTimeUtc.Now)
156 var error = new TwitterError
160 new TwitterErrorItem { Code = TwitterErrorCode.RateLimit, Message = "" },
163 throw new TwitterApiException(0, error, "");
167 public Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param)
168 => this.GetStreamAsync(uri, param, null);
170 public async Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param, string? endpointName)
172 // レートリミット規制中はAPIリクエストを送信せずに直ちにエラーを発生させる
173 if (endpointName != null)
174 this.ThrowIfRateLimitExceeded(endpointName);
176 var requestUri = new Uri(RestApiBase, uri);
179 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
183 var response = await this.Http.GetAsync(requestUri)
184 .ConfigureAwait(false);
186 if (endpointName != null)
187 MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName);
189 await TwitterApiConnection.CheckStatusCode(response)
190 .ConfigureAwait(false);
192 return await response.Content.ReadAsStreamAsync()
193 .ConfigureAwait(false);
195 catch (HttpRequestException ex)
197 throw TwitterApiException.CreateFromException(ex);
199 catch (OperationCanceledException ex)
201 throw TwitterApiException.CreateFromException(ex);
205 public async Task<Stream> GetStreamingStreamAsync(Uri uri, IDictionary<string, string>? param)
207 var requestUri = new Uri(RestApiBase, uri);
210 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
214 var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
215 var response = await this.HttpStreaming.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
216 .ConfigureAwait(false);
218 await TwitterApiConnection.CheckStatusCode(response)
219 .ConfigureAwait(false);
221 return await response.Content.ReadAsStreamAsync()
222 .ConfigureAwait(false);
224 catch (HttpRequestException ex)
226 throw TwitterApiException.CreateFromException(ex);
228 catch (OperationCanceledException ex)
230 throw TwitterApiException.CreateFromException(ex);
234 public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string>? param)
236 var requestUri = new Uri(RestApiBase, uri);
237 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
239 using var postContent = new FormUrlEncodedContent(param);
240 request.Content = postContent;
242 HttpResponseMessage? response = null;
245 response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
246 .ConfigureAwait(false);
248 await TwitterApiConnection.CheckStatusCode(response)
249 .ConfigureAwait(false);
251 var result = new LazyJson<T>(response);
256 catch (HttpRequestException ex)
258 throw TwitterApiException.CreateFromException(ex);
260 catch (OperationCanceledException ex)
262 throw TwitterApiException.CreateFromException(ex);
270 public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string>? param, IDictionary<string, IMediaItem>? media)
272 var requestUri = new Uri(RestApiBase, uri);
273 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
275 using var postContent = new MultipartFormDataContent();
278 foreach (var (key, value) in param)
279 postContent.Add(new StringContent(value), key);
283 foreach (var (key, value) in media)
284 postContent.Add(new StreamContent(value.OpenRead()), key, value.Name);
287 request.Content = postContent;
289 HttpResponseMessage? response = null;
292 response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
293 .ConfigureAwait(false);
295 await TwitterApiConnection.CheckStatusCode(response)
296 .ConfigureAwait(false);
298 var result = new LazyJson<T>(response);
303 catch (HttpRequestException ex)
305 throw TwitterApiException.CreateFromException(ex);
307 catch (OperationCanceledException ex)
309 throw TwitterApiException.CreateFromException(ex);
317 public async Task PostAsync(Uri uri, IDictionary<string, string>? param, IDictionary<string, IMediaItem>? media)
319 var requestUri = new Uri(RestApiBase, uri);
320 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
322 using var postContent = new MultipartFormDataContent();
325 foreach (var (key, value) in param)
326 postContent.Add(new StringContent(value), key);
330 foreach (var (key, value) in media)
331 postContent.Add(new StreamContent(value.OpenRead()), key, value.Name);
334 request.Content = postContent;
338 using var response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
339 .ConfigureAwait(false);
341 await TwitterApiConnection.CheckStatusCode(response)
342 .ConfigureAwait(false);
344 catch (HttpRequestException ex)
346 throw TwitterApiException.CreateFromException(ex);
348 catch (OperationCanceledException ex)
350 throw TwitterApiException.CreateFromException(ex);
354 public async Task DeleteAsync(Uri uri)
356 var requestUri = new Uri(RestApiBase, uri);
357 using var request = new HttpRequestMessage(HttpMethod.Delete, requestUri);
361 using var response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
362 .ConfigureAwait(false);
364 await TwitterApiConnection.CheckStatusCode(response)
365 .ConfigureAwait(false);
367 catch (HttpRequestException ex)
369 throw TwitterApiException.CreateFromException(ex);
371 catch (OperationCanceledException ex)
373 throw TwitterApiException.CreateFromException(ex);
377 public static async Task<T> HandleTimeout<T>(Func<CancellationToken, Task<T>> func, TimeSpan timeout)
379 using var cts = new CancellationTokenSource();
380 var cancellactionToken = cts.Token;
382 var task = Task.Run(() => func(cancellactionToken), cancellactionToken);
383 var timeoutTask = Task.Delay(timeout);
385 if (await Task.WhenAny(task, timeoutTask) == timeoutTask)
389 // キャンセル後のタスクで発生した例外は無視する
390 static async Task IgnoreExceptions(Task task)
394 await task.ConfigureAwait(false);
400 _ = IgnoreExceptions(task);
403 throw new OperationCanceledException("Timeout", cancellactionToken);
409 protected static async Task CheckStatusCode(HttpResponseMessage response)
411 var statusCode = response.StatusCode;
413 if ((int)statusCode >= 200 && (int)statusCode <= 299)
415 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
420 using (var content = response.Content)
422 responseText = await content.ReadAsStringAsync()
423 .ConfigureAwait(false);
426 if (string.IsNullOrWhiteSpace(responseText))
428 if (statusCode == HttpStatusCode.Unauthorized)
429 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
431 throw new TwitterApiException(statusCode, responseText);
436 var error = TwitterError.ParseJson(responseText);
438 if (error?.Errors == null || error.Errors.Length == 0)
439 throw new TwitterApiException(statusCode, responseText);
441 var errorCodes = error.Errors.Select(x => x.Code);
442 if (errorCodes.Any(x => x == TwitterErrorCode.InternalError || x == TwitterErrorCode.SuspendedAccount))
444 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
447 throw new TwitterApiException(statusCode, error, responseText);
449 catch (SerializationException)
451 throw new TwitterApiException(statusCode, responseText);
455 public OAuthEchoHandler CreateOAuthEchoHandler(HttpMessageHandler innerHandler, Uri authServiceProvider, Uri? realm = null)
457 var uri = new Uri(RestApiBase, authServiceProvider);
459 if (this.Credential is TwitterCredentialOAuth1 oauthCredential)
461 return OAuthEchoHandler.CreateHandler(
464 oauthCredential.AppToken.OAuth1ConsumerKey,
465 oauthCredential.AppToken.OAuth1ConsumerSecret,
466 oauthCredential.Token,
467 oauthCredential.TokenSecret,
472 // MobipictureApi クラス向けの暫定対応
473 return OAuthEchoHandler.CreateHandler(
484 public void Dispose()
487 GC.SuppressFinalize(this);
490 protected virtual void Dispose(bool disposing)
495 this.IsDisposed = true;
499 Networking.WebProxyChanged -= this.Networking_WebProxyChanged;
501 this.HttpUpload.Dispose();
502 this.HttpStreaming.Dispose();
506 ~TwitterApiConnection()
507 => this.Dispose(false);
509 private void Networking_WebProxyChanged(object sender, EventArgs e)
510 => this.InitializeHttpClients();
512 public static async Task<TwitterCredentialOAuth1> GetRequestTokenAsync(TwitterAppToken appToken)
514 var emptyCredential = new TwitterCredentialOAuth1(appToken, "", "");
515 var param = new Dictionary<string, string>
517 ["oauth_callback"] = "oob",
519 var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/request_token"), param, emptyCredential)
520 .ConfigureAwait(false);
522 return new(appToken, response["oauth_token"], response["oauth_token_secret"]);
525 public static Uri GetAuthorizeUri(TwitterCredentialOAuth1 requestToken, string? screenName = null)
527 var param = new Dictionary<string, string>
529 ["oauth_token"] = requestToken.Token,
532 if (screenName != null)
533 param["screen_name"] = screenName;
535 return new Uri("https://api.twitter.com/oauth/authorize?" + MyCommon.BuildQueryString(param));
538 public static async Task<IDictionary<string, string>> GetAccessTokenAsync(TwitterCredentialOAuth1 credential, string verifier)
540 var param = new Dictionary<string, string>
542 ["oauth_verifier"] = verifier,
544 var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/access_token"), param, credential)
545 .ConfigureAwait(false);
550 private static async Task<IDictionary<string, string>> GetOAuthTokenAsync(
552 IDictionary<string, string> param,
553 TwitterCredentialOAuth1 credential
556 using var authorizeClient = InitializeHttpClient(credential);
558 var requestUri = new Uri(uri, "?" + MyCommon.BuildQueryString(param));
562 using var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
563 using var response = await authorizeClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
564 .ConfigureAwait(false);
566 using var content = response.Content;
567 var responseText = await content.ReadAsStringAsync()
568 .ConfigureAwait(false);
570 await TwitterApiConnection.CheckStatusCode(response)
571 .ConfigureAwait(false);
573 var responseParams = HttpUtility.ParseQueryString(responseText);
575 return responseParams.Cast<string>()
576 .ToDictionary(x => x, x => responseParams[x]);
578 catch (HttpRequestException ex)
580 throw TwitterApiException.CreateFromException(ex);
582 catch (OperationCanceledException ex)
584 throw TwitterApiException.CreateFromException(ex);
588 private static HttpClient InitializeHttpClient(ITwitterCredential credential, bool disableGzip = false)
590 var builder = Networking.CreateHttpClientBuilder();
592 builder.SetupHttpClientHandler(x =>
594 x.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
597 x.AutomaticDecompression = DecompressionMethods.None;
600 builder.AddHandler(x => credential.CreateHttpHandler(x));
602 return builder.Build();