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, 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 public string AccessToken { get; }
57 public string AccessSecret { get; }
59 internal HttpClient Http;
60 internal HttpClient HttpUpload;
61 internal HttpClient HttpStreaming;
63 private readonly TwitterAppToken appToken;
65 public TwitterApiConnection(ApiKey consumerKey, ApiKey consumerSecret, string accessToken, string accessSecret)
69 AuthType = APIAuthType.OAuth1,
70 OAuth1CustomConsumerKey = consumerKey,
71 OAuth1CustomConsumerSecret = consumerSecret,
79 public TwitterApiConnection(TwitterAppToken appToken, string accessToken, string accessSecret)
81 this.appToken = appToken;
82 this.AccessToken = accessToken;
83 this.AccessSecret = accessSecret;
85 this.InitializeHttpClients();
86 Networking.WebProxyChanged += this.Networking_WebProxyChanged;
89 [MemberNotNull(nameof(Http), nameof(HttpUpload), nameof(HttpStreaming))]
90 private void InitializeHttpClients()
92 this.Http = InitializeHttpClient(this.appToken, this.AccessToken, this.AccessSecret);
94 this.HttpUpload = InitializeHttpClient(this.appToken, this.AccessToken, this.AccessSecret);
95 this.HttpUpload.Timeout = Networking.UploadImageTimeout;
97 this.HttpStreaming = InitializeHttpClient(this.appToken, this.AccessToken, this.AccessSecret, disableGzip: true);
98 this.HttpStreaming.Timeout = Timeout.InfiniteTimeSpan;
101 public async Task<T> GetAsync<T>(Uri uri, IDictionary<string, string>? param, string? endpointName)
103 // レートリミット規制中はAPIリクエストを送信せずに直ちにエラーを発生させる
104 if (endpointName != null)
105 this.ThrowIfRateLimitExceeded(endpointName);
107 var requestUri = new Uri(RestApiBase, uri);
110 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
112 var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
116 using var response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
117 .ConfigureAwait(false);
119 if (endpointName != null)
120 MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName);
122 await TwitterApiConnection.CheckStatusCode(response)
123 .ConfigureAwait(false);
125 using var content = response.Content;
126 var responseText = await content.ReadAsStringAsync()
127 .ConfigureAwait(false);
131 return MyCommon.CreateDataFromJson<T>(responseText);
133 catch (SerializationException ex)
135 throw TwitterApiException.CreateFromException(ex, responseText);
138 catch (HttpRequestException ex)
140 throw TwitterApiException.CreateFromException(ex);
142 catch (OperationCanceledException ex)
144 throw TwitterApiException.CreateFromException(ex);
149 /// 指定されたエンドポイントがレートリミット規制中であれば例外を発生させる
151 private void ThrowIfRateLimitExceeded(string endpointName)
153 var limit = MyCommon.TwitterApiInfo.AccessLimit[endpointName];
157 if (limit.AccessLimitRemain == 0 && limit.AccessLimitResetDate > DateTimeUtc.Now)
159 var error = new TwitterError
163 new TwitterErrorItem { Code = TwitterErrorCode.RateLimit, Message = "" },
166 throw new TwitterApiException(0, error, "");
170 public Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param)
171 => this.GetStreamAsync(uri, param, null);
173 public async Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param, string? endpointName)
175 // レートリミット規制中はAPIリクエストを送信せずに直ちにエラーを発生させる
176 if (endpointName != null)
177 this.ThrowIfRateLimitExceeded(endpointName);
179 var requestUri = new Uri(RestApiBase, uri);
182 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
186 var response = await this.Http.GetAsync(requestUri)
187 .ConfigureAwait(false);
189 if (endpointName != null)
190 MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName);
192 await TwitterApiConnection.CheckStatusCode(response)
193 .ConfigureAwait(false);
195 return await response.Content.ReadAsStreamAsync()
196 .ConfigureAwait(false);
198 catch (HttpRequestException ex)
200 throw TwitterApiException.CreateFromException(ex);
202 catch (OperationCanceledException ex)
204 throw TwitterApiException.CreateFromException(ex);
208 public async Task<Stream> GetStreamingStreamAsync(Uri uri, IDictionary<string, string>? param)
210 var requestUri = new Uri(RestApiBase, uri);
213 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
217 var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
218 var response = await this.HttpStreaming.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
219 .ConfigureAwait(false);
221 await TwitterApiConnection.CheckStatusCode(response)
222 .ConfigureAwait(false);
224 return await response.Content.ReadAsStreamAsync()
225 .ConfigureAwait(false);
227 catch (HttpRequestException ex)
229 throw TwitterApiException.CreateFromException(ex);
231 catch (OperationCanceledException ex)
233 throw TwitterApiException.CreateFromException(ex);
237 public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string>? param)
239 var requestUri = new Uri(RestApiBase, uri);
240 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
242 using var postContent = new FormUrlEncodedContent(param);
243 request.Content = postContent;
245 HttpResponseMessage? response = null;
248 response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
249 .ConfigureAwait(false);
251 await TwitterApiConnection.CheckStatusCode(response)
252 .ConfigureAwait(false);
254 var result = new LazyJson<T>(response);
259 catch (HttpRequestException ex)
261 throw TwitterApiException.CreateFromException(ex);
263 catch (OperationCanceledException ex)
265 throw TwitterApiException.CreateFromException(ex);
273 public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string>? param, IDictionary<string, IMediaItem>? media)
275 var requestUri = new Uri(RestApiBase, uri);
276 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
278 using var postContent = new MultipartFormDataContent();
281 foreach (var (key, value) in param)
282 postContent.Add(new StringContent(value), key);
286 foreach (var (key, value) in media)
287 postContent.Add(new StreamContent(value.OpenRead()), key, value.Name);
290 request.Content = postContent;
292 HttpResponseMessage? response = null;
295 response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
296 .ConfigureAwait(false);
298 await TwitterApiConnection.CheckStatusCode(response)
299 .ConfigureAwait(false);
301 var result = new LazyJson<T>(response);
306 catch (HttpRequestException ex)
308 throw TwitterApiException.CreateFromException(ex);
310 catch (OperationCanceledException ex)
312 throw TwitterApiException.CreateFromException(ex);
320 public async Task PostAsync(Uri uri, IDictionary<string, string>? param, IDictionary<string, IMediaItem>? media)
322 var requestUri = new Uri(RestApiBase, uri);
323 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
325 using var postContent = new MultipartFormDataContent();
328 foreach (var (key, value) in param)
329 postContent.Add(new StringContent(value), key);
333 foreach (var (key, value) in media)
334 postContent.Add(new StreamContent(value.OpenRead()), key, value.Name);
337 request.Content = postContent;
341 using var response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
342 .ConfigureAwait(false);
344 await TwitterApiConnection.CheckStatusCode(response)
345 .ConfigureAwait(false);
347 catch (HttpRequestException ex)
349 throw TwitterApiException.CreateFromException(ex);
351 catch (OperationCanceledException ex)
353 throw TwitterApiException.CreateFromException(ex);
357 public async Task<string> PostJsonAsync(Uri uri, string json)
359 var requestUri = new Uri(RestApiBase, uri);
360 using var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
362 using var postContent = new StringContent(json, Encoding.UTF8, "application/json");
363 request.Content = postContent;
367 using var response = await this.Http.SendAsync(request)
368 .ConfigureAwait(false);
370 await TwitterApiConnection.CheckStatusCode(response)
371 .ConfigureAwait(false);
373 return await response.Content.ReadAsStringAsync()
374 .ConfigureAwait(false);
376 catch (HttpRequestException ex)
378 throw TwitterApiException.CreateFromException(ex);
380 catch (OperationCanceledException ex)
382 throw TwitterApiException.CreateFromException(ex);
386 public async Task<LazyJson<T>> PostJsonAsync<T>(Uri uri, string json)
388 var requestUri = new Uri(RestApiBase, uri);
389 var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
391 using var postContent = new StringContent(json, Encoding.UTF8, "application/json");
392 request.Content = postContent;
394 HttpResponseMessage? response = null;
397 response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
398 .ConfigureAwait(false);
400 await TwitterApiConnection.CheckStatusCode(response)
401 .ConfigureAwait(false);
403 var result = new LazyJson<T>(response);
408 catch (HttpRequestException ex)
410 throw TwitterApiException.CreateFromException(ex);
412 catch (OperationCanceledException ex)
414 throw TwitterApiException.CreateFromException(ex);
422 public async Task DeleteAsync(Uri uri)
424 var requestUri = new Uri(RestApiBase, uri);
425 using var request = new HttpRequestMessage(HttpMethod.Delete, requestUri);
429 using var response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
430 .ConfigureAwait(false);
432 await TwitterApiConnection.CheckStatusCode(response)
433 .ConfigureAwait(false);
435 catch (HttpRequestException ex)
437 throw TwitterApiException.CreateFromException(ex);
439 catch (OperationCanceledException ex)
441 throw TwitterApiException.CreateFromException(ex);
445 protected static async Task CheckStatusCode(HttpResponseMessage response)
447 var statusCode = response.StatusCode;
449 if ((int)statusCode >= 200 && (int)statusCode <= 299)
451 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
456 using (var content = response.Content)
458 responseText = await content.ReadAsStringAsync()
459 .ConfigureAwait(false);
462 if (string.IsNullOrWhiteSpace(responseText))
464 if (statusCode == HttpStatusCode.Unauthorized)
465 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
467 throw new TwitterApiException(statusCode, responseText);
472 var error = TwitterError.ParseJson(responseText);
474 if (error?.Errors == null || error.Errors.Length == 0)
475 throw new TwitterApiException(statusCode, responseText);
477 var errorCodes = error.Errors.Select(x => x.Code);
478 if (errorCodes.Any(x => x == TwitterErrorCode.InternalError || x == TwitterErrorCode.SuspendedAccount))
480 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
483 throw new TwitterApiException(statusCode, error, responseText);
485 catch (SerializationException)
487 throw new TwitterApiException(statusCode, responseText);
491 public OAuthEchoHandler CreateOAuthEchoHandler(HttpMessageHandler innerHandler, Uri authServiceProvider, Uri? realm = null)
493 var uri = new Uri(RestApiBase, authServiceProvider);
495 return OAuthEchoHandler.CreateHandler(
498 this.appToken.OAuth1ConsumerKey,
499 this.appToken.OAuth1ConsumerSecret,
505 public void Dispose()
508 GC.SuppressFinalize(this);
511 protected virtual void Dispose(bool disposing)
516 this.IsDisposed = true;
520 Networking.WebProxyChanged -= this.Networking_WebProxyChanged;
522 this.HttpUpload.Dispose();
523 this.HttpStreaming.Dispose();
527 ~TwitterApiConnection()
528 => this.Dispose(false);
530 private void Networking_WebProxyChanged(object sender, EventArgs e)
531 => this.InitializeHttpClients();
533 public static async Task<(string Token, string TokenSecret)> GetRequestTokenAsync(TwitterAppToken appToken)
535 var param = new Dictionary<string, string>
537 ["oauth_callback"] = "oob",
539 var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/request_token"), param, appToken, oauthToken: null)
540 .ConfigureAwait(false);
542 return (response["oauth_token"], response["oauth_token_secret"]);
545 public static Uri GetAuthorizeUri((string Token, string TokenSecret) requestToken, string? screenName = null)
547 var param = new Dictionary<string, string>
549 ["oauth_token"] = requestToken.Token,
552 if (screenName != null)
553 param["screen_name"] = screenName;
555 return new Uri("https://api.twitter.com/oauth/authorize?" + MyCommon.BuildQueryString(param));
558 public static async Task<IDictionary<string, string>> GetAccessTokenAsync(TwitterAppToken appToken, (string Token, string TokenSecret) requestToken, string verifier)
560 var param = new Dictionary<string, string>
562 ["oauth_verifier"] = verifier,
564 var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/access_token"), param, appToken, requestToken)
565 .ConfigureAwait(false);
570 private static async Task<IDictionary<string, string>> GetOAuthTokenAsync(
572 IDictionary<string, string> param,
573 TwitterAppToken appToken,
574 (string Token, string TokenSecret)? oauthToken)
576 HttpClient authorizeClient;
577 if (oauthToken != null)
578 authorizeClient = InitializeHttpClient(appToken.OAuth1ConsumerKey, appToken.OAuth1ConsumerSecret, oauthToken.Value.Token, oauthToken.Value.TokenSecret);
580 authorizeClient = InitializeHttpClient(appToken.OAuth1ConsumerKey, appToken.OAuth1ConsumerSecret, "", "");
582 var requestUri = new Uri(uri, "?" + MyCommon.BuildQueryString(param));
586 using var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
587 using var response = await authorizeClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
588 .ConfigureAwait(false);
590 using var content = response.Content;
591 var responseText = await content.ReadAsStringAsync()
592 .ConfigureAwait(false);
594 await TwitterApiConnection.CheckStatusCode(response)
595 .ConfigureAwait(false);
597 var responseParams = HttpUtility.ParseQueryString(responseText);
599 return responseParams.Cast<string>()
600 .ToDictionary(x => x, x => responseParams[x]);
602 catch (HttpRequestException ex)
604 throw TwitterApiException.CreateFromException(ex);
606 catch (OperationCanceledException ex)
608 throw TwitterApiException.CreateFromException(ex);
612 private static HttpClient InitializeHttpClient(ApiKey consumerKey, ApiKey consumerSecret, string accessToken, string accessSecret, bool disableGzip = false)
614 var builder = Networking.CreateHttpClientBuilder();
616 builder.SetupHttpClientHandler(x =>
618 x.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
621 x.AutomaticDecompression = DecompressionMethods.None;
624 builder.AddHandler(x => new OAuthHandler(x, consumerKey, consumerSecret, accessToken, accessSecret));
626 return builder.Build();
629 private static HttpClient InitializeHttpClient(TwitterAppToken appToken, string accessToken, string accessSecret, bool disableGzip = false)
631 var builder = Networking.CreateHttpClientBuilder();
633 builder.SetupHttpClientHandler(x =>
635 x.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
638 x.AutomaticDecompression = DecompressionMethods.None;
641 builder.AddHandler(x => appToken.AuthType switch
644 => new OAuthHandler(x, appToken.OAuth1ConsumerKey, appToken.OAuth1ConsumerSecret, accessToken, accessSecret),
645 APIAuthType.TwitterComCookie
646 => new TwitterComCookieHandler(x, appToken.TwitterComCookie),
647 _ => throw new NotImplementedException(),
650 return builder.Build();