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;
58 internal ITwitterCredential Credential { get; }
60 public TwitterApiConnection()
61 : this(new TwitterCredentialNone())
65 public TwitterApiConnection(ITwitterCredential credential)
67 this.Credential = credential;
69 this.InitializeHttpClients();
70 Networking.WebProxyChanged += this.Networking_WebProxyChanged;
73 [MemberNotNull(nameof(Http), nameof(HttpUpload))]
74 private void InitializeHttpClients()
76 this.Http = InitializeHttpClient(this.Credential);
78 // タイムアウト設定は IHttpRequest.Timeout でリクエスト毎に制御する
79 this.Http.Timeout = Timeout.InfiniteTimeSpan;
81 this.HttpUpload = InitializeHttpClient(this.Credential);
82 this.HttpUpload.Timeout = Networking.UploadImageTimeout;
85 public async Task<ApiResponse> SendAsync(IHttpRequest request)
87 var endpointName = request.EndpointName;
89 // レートリミット規制中はAPIリクエストを送信せずに直ちにエラーを発生させる
90 if (endpointName != null)
91 this.ThrowIfRateLimitExceeded(endpointName);
93 using var requestMessage = request.CreateMessage(RestApiBase);
95 HttpResponseMessage? responseMessage = null;
98 responseMessage = await HandleTimeout(
99 (token) => this.Http.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, token),
103 if (endpointName != null)
104 MyCommon.TwitterApiInfo.UpdateFromHeader(responseMessage.Headers, endpointName);
106 await TwitterApiConnection.CheckStatusCode(responseMessage)
107 .ConfigureAwait(false);
109 var response = new ApiResponse(responseMessage);
110 responseMessage = null; // responseMessage は ApiResponse で使用するため破棄されないようにする
114 catch (HttpRequestException ex)
116 throw TwitterApiException.CreateFromException(ex);
118 catch (OperationCanceledException ex)
120 throw TwitterApiException.CreateFromException(ex);
124 responseMessage?.Dispose();
128 public async Task<T> GetAsync<T>(Uri uri, IDictionary<string, string>? param, string? endpointName)
130 var request = new GetRequest
134 EndpointName = endpointName,
137 using var response = await this.SendAsync(request)
138 .ConfigureAwait(false);
140 return await response.ReadAsJson<T>()
141 .ConfigureAwait(false);
145 /// 指定されたエンドポイントがレートリミット規制中であれば例外を発生させる
147 private void ThrowIfRateLimitExceeded(string endpointName)
149 var limit = MyCommon.TwitterApiInfo.AccessLimit[endpointName];
153 if (limit.AccessLimitRemain == 0 && limit.AccessLimitResetDate > DateTimeUtc.Now)
155 var error = new TwitterError
159 new TwitterErrorItem { Code = TwitterErrorCode.RateLimit, Message = "" },
162 throw new TwitterApiException(0, error, "");
166 public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string>? param)
168 var request = new PostRequest
174 using var response = await this.SendAsync(request)
175 .ConfigureAwait(false);
177 return response.ReadAsLazyJson<T>();
180 public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string>? param, IDictionary<string, IMediaItem>? media)
182 var requestUri = new Uri(RestApiBase, uri);
183 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
185 using var postContent = new MultipartFormDataContent();
188 foreach (var (key, value) in param)
189 postContent.Add(new StringContent(value), key);
193 foreach (var (key, value) in media)
194 postContent.Add(new StreamContent(value.OpenRead()), key, value.Name);
197 request.Content = postContent;
199 HttpResponseMessage? response = null;
202 response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
203 .ConfigureAwait(false);
205 await TwitterApiConnection.CheckStatusCode(response)
206 .ConfigureAwait(false);
208 var result = new LazyJson<T>(response);
213 catch (HttpRequestException ex)
215 throw TwitterApiException.CreateFromException(ex);
217 catch (OperationCanceledException ex)
219 throw TwitterApiException.CreateFromException(ex);
227 public async Task PostAsync(Uri uri, IDictionary<string, string>? param, IDictionary<string, IMediaItem>? media)
229 var requestUri = new Uri(RestApiBase, uri);
230 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
232 using var postContent = new MultipartFormDataContent();
235 foreach (var (key, value) in param)
236 postContent.Add(new StringContent(value), key);
240 foreach (var (key, value) in media)
241 postContent.Add(new StreamContent(value.OpenRead()), key, value.Name);
244 request.Content = postContent;
248 using var response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
249 .ConfigureAwait(false);
251 await TwitterApiConnection.CheckStatusCode(response)
252 .ConfigureAwait(false);
254 catch (HttpRequestException ex)
256 throw TwitterApiException.CreateFromException(ex);
258 catch (OperationCanceledException ex)
260 throw TwitterApiException.CreateFromException(ex);
264 public static async Task<T> HandleTimeout<T>(Func<CancellationToken, Task<T>> func, TimeSpan timeout)
266 using var cts = new CancellationTokenSource();
267 var cancellactionToken = cts.Token;
269 var task = Task.Run(() => func(cancellactionToken), cancellactionToken);
270 var timeoutTask = Task.Delay(timeout);
272 if (await Task.WhenAny(task, timeoutTask) == timeoutTask)
276 // キャンセル後のタスクで発生した例外は無視する
277 static async Task IgnoreExceptions(Task task)
281 await task.ConfigureAwait(false);
287 _ = IgnoreExceptions(task);
290 throw new OperationCanceledException("Timeout", cancellactionToken);
296 protected static async Task CheckStatusCode(HttpResponseMessage response)
298 var statusCode = response.StatusCode;
300 if ((int)statusCode >= 200 && (int)statusCode <= 299)
302 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
307 using (var content = response.Content)
309 responseText = await content.ReadAsStringAsync()
310 .ConfigureAwait(false);
313 if (string.IsNullOrWhiteSpace(responseText))
315 if (statusCode == HttpStatusCode.Unauthorized)
316 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
318 throw new TwitterApiException(statusCode, responseText);
323 var error = TwitterError.ParseJson(responseText);
325 if (error?.Errors == null || error.Errors.Length == 0)
326 throw new TwitterApiException(statusCode, responseText);
328 var errorCodes = error.Errors.Select(x => x.Code);
329 if (errorCodes.Any(x => x == TwitterErrorCode.InternalError || x == TwitterErrorCode.SuspendedAccount))
331 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
334 throw new TwitterApiException(statusCode, error, responseText);
336 catch (SerializationException)
338 throw new TwitterApiException(statusCode, responseText);
342 public OAuthEchoHandler CreateOAuthEchoHandler(HttpMessageHandler innerHandler, Uri authServiceProvider, Uri? realm = null)
344 var uri = new Uri(RestApiBase, authServiceProvider);
346 if (this.Credential is TwitterCredentialOAuth1 oauthCredential)
348 return OAuthEchoHandler.CreateHandler(
351 oauthCredential.AppToken.OAuth1ConsumerKey,
352 oauthCredential.AppToken.OAuth1ConsumerSecret,
353 oauthCredential.Token,
354 oauthCredential.TokenSecret,
359 // MobipictureApi クラス向けの暫定対応
360 return OAuthEchoHandler.CreateHandler(
371 public void Dispose()
374 GC.SuppressFinalize(this);
377 protected virtual void Dispose(bool disposing)
382 this.IsDisposed = true;
386 Networking.WebProxyChanged -= this.Networking_WebProxyChanged;
388 this.HttpUpload.Dispose();
392 ~TwitterApiConnection()
393 => this.Dispose(false);
395 private void Networking_WebProxyChanged(object sender, EventArgs e)
396 => this.InitializeHttpClients();
398 public static async Task<TwitterCredentialOAuth1> GetRequestTokenAsync(TwitterAppToken appToken)
400 var emptyCredential = new TwitterCredentialOAuth1(appToken, "", "");
401 var param = new Dictionary<string, string>
403 ["oauth_callback"] = "oob",
405 var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/request_token"), param, emptyCredential)
406 .ConfigureAwait(false);
408 return new(appToken, response["oauth_token"], response["oauth_token_secret"]);
411 public static Uri GetAuthorizeUri(TwitterCredentialOAuth1 requestToken, string? screenName = null)
413 var param = new Dictionary<string, string>
415 ["oauth_token"] = requestToken.Token,
418 if (screenName != null)
419 param["screen_name"] = screenName;
421 return new Uri("https://api.twitter.com/oauth/authorize?" + MyCommon.BuildQueryString(param));
424 public static async Task<IDictionary<string, string>> GetAccessTokenAsync(TwitterCredentialOAuth1 credential, string verifier)
426 var param = new Dictionary<string, string>
428 ["oauth_verifier"] = verifier,
430 var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/access_token"), param, credential)
431 .ConfigureAwait(false);
436 private static async Task<IDictionary<string, string>> GetOAuthTokenAsync(
438 IDictionary<string, string> param,
439 TwitterCredentialOAuth1 credential
442 using var authorizeClient = InitializeHttpClient(credential);
444 var requestUri = new Uri(uri, "?" + MyCommon.BuildQueryString(param));
448 using var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
449 using var response = await authorizeClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
450 .ConfigureAwait(false);
452 using var content = response.Content;
453 var responseText = await content.ReadAsStringAsync()
454 .ConfigureAwait(false);
456 await TwitterApiConnection.CheckStatusCode(response)
457 .ConfigureAwait(false);
459 var responseParams = HttpUtility.ParseQueryString(responseText);
461 return responseParams.Cast<string>()
462 .ToDictionary(x => x, x => responseParams[x]);
464 catch (HttpRequestException ex)
466 throw TwitterApiException.CreateFromException(ex);
468 catch (OperationCanceledException ex)
470 throw TwitterApiException.CreateFromException(ex);
474 private static HttpClient InitializeHttpClient(ITwitterCredential credential)
476 var builder = Networking.CreateHttpClientBuilder();
478 builder.SetupHttpClientHandler(
479 x => x.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache)
482 builder.AddHandler(x => credential.CreateHttpHandler(x));
484 return builder.Build();