// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
// Boston, MA 02110-1301, USA.
+#nullable enable
+
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using System.Web;
using OpenTween.Api;
namespace OpenTween.Connection
{
- public class TwitterApiConnection : IApiConnection, IDisposable
+ public class TwitterApiConnection : IApiConnection, IApiConnectionLegacy, IDisposable
{
- public static Uri RestApiBase { get; set; } = new Uri("https://api.twitter.com/1.1/");
+ public static Uri RestApiBase { get; set; } = new("https://api.twitter.com/1.1/");
// SettingCommon.xml の TwitterUrl との互換性のために用意
public static string RestApiHost
{
- get { return RestApiBase.Host; }
- set { RestApiBase = new Uri($"https://{value}/1.1/"); }
+ get => RestApiBase.Host;
+ set => RestApiBase = new Uri($"https://{value}/1.1/");
}
public bool IsDisposed { get; private set; } = false;
- public string AccessToken { get; }
- public string AccessSecret { get; }
+ internal HttpClient Http;
+ internal HttpClient HttpUpload;
+ internal HttpClient HttpStreaming;
+
+ internal ITwitterCredential Credential { get; }
- internal HttpClient http;
+ public TwitterApiConnection()
+ : this(new TwitterCredentialNone())
+ {
+ }
- public TwitterApiConnection(string accessToken, string accessSecret)
+ public TwitterApiConnection(ITwitterCredential credential)
{
- this.AccessToken = accessToken;
- this.AccessSecret = accessSecret;
+ this.Credential = credential;
- this.http = InitializeHttpClient(accessToken, accessSecret);
+ this.InitializeHttpClients();
Networking.WebProxyChanged += this.Networking_WebProxyChanged;
}
- public async Task<T> GetAsync<T>(Uri uri, IDictionary<string, string> param, string endpointName)
+ [MemberNotNull(nameof(Http), nameof(HttpUpload), nameof(HttpStreaming))]
+ private void InitializeHttpClients()
+ {
+ this.Http = InitializeHttpClient(this.Credential);
+
+ this.HttpUpload = InitializeHttpClient(this.Credential);
+ this.HttpUpload.Timeout = Networking.UploadImageTimeout;
+
+ this.HttpStreaming = InitializeHttpClient(this.Credential, disableGzip: true);
+ this.HttpStreaming.Timeout = Timeout.InfiniteTimeSpan;
+ }
+
+ public async Task<ApiResponse> SendAsync(IHttpRequest request)
+ {
+ var endpointName = request.EndpointName;
+
+ // レートリミット規制中はAPIリクエストを送信せずに直ちにエラーを発生させる
+ if (endpointName != null)
+ this.ThrowIfRateLimitExceeded(endpointName);
+
+ using var requestMessage = request.CreateMessage(RestApiBase);
+
+ HttpResponseMessage? responseMessage = null;
+ try
+ {
+ responseMessage = await HandleTimeout(
+ (token) => this.Http.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, token),
+ Networking.DefaultTimeout
+ );
+
+ if (endpointName != null)
+ MyCommon.TwitterApiInfo.UpdateFromHeader(responseMessage.Headers, endpointName);
+
+ await TwitterApiConnection.CheckStatusCode(responseMessage)
+ .ConfigureAwait(false);
+
+ var response = new ApiResponse(responseMessage);
+ responseMessage = null; // responseMessage は ApiResponse で使用するため破棄されないようにする
+
+ return response;
+ }
+ catch (HttpRequestException ex)
+ {
+ throw TwitterApiException.CreateFromException(ex);
+ }
+ catch (OperationCanceledException ex)
+ {
+ throw TwitterApiException.CreateFromException(ex);
+ }
+ finally
+ {
+ responseMessage?.Dispose();
+ }
+ }
+
+ public async Task<T> GetAsync<T>(Uri uri, IDictionary<string, string>? param, string? endpointName)
+ {
+ var request = new GetRequest
+ {
+ RequestUri = uri,
+ Query = param,
+ EndpointName = endpointName,
+ };
+
+ using var response = await this.SendAsync(request)
+ .ConfigureAwait(false);
+
+ return await response.ReadAsJson<T>()
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// 指定されたエンドポイントがレートリミット規制中であれば例外を発生させる
+ /// </summary>
+ private void ThrowIfRateLimitExceeded(string endpointName)
+ {
+ var limit = MyCommon.TwitterApiInfo.AccessLimit[endpointName];
+ if (limit == null)
+ return;
+
+ if (limit.AccessLimitRemain == 0 && limit.AccessLimitResetDate > DateTimeUtc.Now)
+ {
+ var error = new TwitterError
+ {
+ Errors = new[]
+ {
+ new TwitterErrorItem { Code = TwitterErrorCode.RateLimit, Message = "" },
+ },
+ };
+ throw new TwitterApiException(0, error, "");
+ }
+ }
+
+ public Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param)
+ => this.GetStreamAsync(uri, param, null);
+
+ public async Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param, string? endpointName)
{
+ // レートリミット規制中はAPIリクエストを送信せずに直ちにエラーを発生させる
+ if (endpointName != null)
+ this.ThrowIfRateLimitExceeded(endpointName);
+
var requestUri = new Uri(RestApiBase, uri);
if (param != null)
requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
- var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
-
try
{
- using (var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
- .ConfigureAwait(false))
- {
- await this.CheckStatusCode(response)
- .ConfigureAwait(false);
+ var response = await this.Http.GetAsync(requestUri)
+ .ConfigureAwait(false);
- if (endpointName != null)
- MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName);
+ if (endpointName != null)
+ MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName);
- using (var content = response.Content)
- {
- var responseText = await content.ReadAsStringAsync()
- .ConfigureAwait(false);
-
- try
- {
- return MyCommon.CreateDataFromJson<T>(responseText);
- }
- catch (SerializationException ex)
- {
- throw TwitterApiException.CreateFromException(ex, responseText);
- }
- }
- }
+ await TwitterApiConnection.CheckStatusCode(response)
+ .ConfigureAwait(false);
+
+ return await response.Content.ReadAsStreamAsync()
+ .ConfigureAwait(false);
}
catch (HttpRequestException ex)
{
}
}
- public async Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string> param)
+ public async Task<Stream> GetStreamingStreamAsync(Uri uri, IDictionary<string, string>? param)
{
var requestUri = new Uri(RestApiBase, uri);
try
{
- return await this.http.GetStreamAsync(requestUri)
+ var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
+ var response = await this.HttpStreaming.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
+ .ConfigureAwait(false);
+
+ await TwitterApiConnection.CheckStatusCode(response)
+ .ConfigureAwait(false);
+
+ return await response.Content.ReadAsStreamAsync()
.ConfigureAwait(false);
}
catch (HttpRequestException ex)
}
}
- public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string> param)
+ public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string>? param)
{
var requestUri = new Uri(RestApiBase, uri);
var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
- using (var postContent = new FormUrlEncodedContent(param))
+ using var postContent = new FormUrlEncodedContent(param);
+ request.Content = postContent;
+
+ HttpResponseMessage? response = null;
+ try
{
- request.Content = postContent;
+ response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
+ .ConfigureAwait(false);
- HttpResponseMessage response = null;
- try
- {
- response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
- .ConfigureAwait(false);
+ await TwitterApiConnection.CheckStatusCode(response)
+ .ConfigureAwait(false);
+
+ var result = new LazyJson<T>(response);
+ response = null;
- await this.CheckStatusCode(response)
- .ConfigureAwait(false);
+ return result;
+ }
+ catch (HttpRequestException ex)
+ {
+ throw TwitterApiException.CreateFromException(ex);
+ }
+ catch (OperationCanceledException ex)
+ {
+ throw TwitterApiException.CreateFromException(ex);
+ }
+ finally
+ {
+ response?.Dispose();
+ }
+ }
- var result = new LazyJson<T>(response);
- response = null;
+ public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string>? param, IDictionary<string, IMediaItem>? media)
+ {
+ var requestUri = new Uri(RestApiBase, uri);
+ var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
- return result;
- }
- catch (HttpRequestException ex)
- {
- throw TwitterApiException.CreateFromException(ex);
- }
- catch (OperationCanceledException ex)
- {
- throw TwitterApiException.CreateFromException(ex);
- }
- finally
- {
- response?.Dispose();
- }
+ using var postContent = new MultipartFormDataContent();
+ if (param != null)
+ {
+ foreach (var (key, value) in param)
+ postContent.Add(new StringContent(value), key);
+ }
+ if (media != null)
+ {
+ foreach (var (key, value) in media)
+ postContent.Add(new StreamContent(value.OpenRead()), key, value.Name);
+ }
+
+ request.Content = postContent;
+
+ HttpResponseMessage? response = null;
+ try
+ {
+ response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
+ .ConfigureAwait(false);
+
+ await TwitterApiConnection.CheckStatusCode(response)
+ .ConfigureAwait(false);
+
+ var result = new LazyJson<T>(response);
+ response = null;
+
+ return result;
+ }
+ catch (HttpRequestException ex)
+ {
+ throw TwitterApiException.CreateFromException(ex);
+ }
+ catch (OperationCanceledException ex)
+ {
+ throw TwitterApiException.CreateFromException(ex);
+ }
+ finally
+ {
+ response?.Dispose();
}
}
- public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string> param, IDictionary<string, IMediaItem> media)
+ public async Task PostAsync(Uri uri, IDictionary<string, string>? param, IDictionary<string, IMediaItem>? media)
{
var requestUri = new Uri(RestApiBase, uri);
var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
- using (var postContent = new MultipartFormDataContent())
+ using var postContent = new MultipartFormDataContent();
+ if (param != null)
+ {
+ foreach (var (key, value) in param)
+ postContent.Add(new StringContent(value), key);
+ }
+ if (media != null)
{
- foreach (var kv in param)
- postContent.Add(new StringContent(kv.Value), kv.Key);
+ foreach (var (key, value) in media)
+ postContent.Add(new StreamContent(value.OpenRead()), key, value.Name);
+ }
- foreach (var kv in media)
- postContent.Add(new StreamContent(kv.Value.OpenRead()), kv.Key, kv.Value.Name);
+ request.Content = postContent;
- request.Content = postContent;
+ try
+ {
+ using var response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
+ .ConfigureAwait(false);
- HttpResponseMessage response = null;
- try
- {
- response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
- .ConfigureAwait(false);
+ await TwitterApiConnection.CheckStatusCode(response)
+ .ConfigureAwait(false);
+ }
+ catch (HttpRequestException ex)
+ {
+ throw TwitterApiException.CreateFromException(ex);
+ }
+ catch (OperationCanceledException ex)
+ {
+ throw TwitterApiException.CreateFromException(ex);
+ }
+ }
- await this.CheckStatusCode(response)
- .ConfigureAwait(false);
+ public async Task<string> PostJsonAsync(Uri uri, string json)
+ {
+ var request = new PostJsonRequest
+ {
+ RequestUri = uri,
+ JsonString = json,
+ };
- var result = new LazyJson<T>(response);
- response = null;
+ using var response = await this.SendAsync(request)
+ .ConfigureAwait(false);
- return result;
- }
- catch (HttpRequestException ex)
- {
- throw TwitterApiException.CreateFromException(ex);
- }
- catch (OperationCanceledException ex)
- {
- throw TwitterApiException.CreateFromException(ex);
- }
- finally
+ return await response.ReadAsString()
+ .ConfigureAwait(false);
+ }
+
+ public async Task<LazyJson<T>> PostJsonAsync<T>(Uri uri, string json)
+ {
+ var request = new PostJsonRequest
+ {
+ RequestUri = uri,
+ JsonString = json,
+ };
+
+ using var response = await this.SendAsync(request)
+ .ConfigureAwait(false);
+
+ return response.ReadAsLazyJson<T>();
+ }
+
+ public async Task DeleteAsync(Uri uri)
+ {
+ var requestUri = new Uri(RestApiBase, uri);
+ using var request = new HttpRequestMessage(HttpMethod.Delete, requestUri);
+
+ try
+ {
+ using var response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
+ .ConfigureAwait(false);
+
+ await TwitterApiConnection.CheckStatusCode(response)
+ .ConfigureAwait(false);
+ }
+ catch (HttpRequestException ex)
+ {
+ throw TwitterApiException.CreateFromException(ex);
+ }
+ catch (OperationCanceledException ex)
+ {
+ throw TwitterApiException.CreateFromException(ex);
+ }
+ }
+
+ public static async Task<T> HandleTimeout<T>(Func<CancellationToken, Task<T>> func, TimeSpan timeout)
+ {
+ using var cts = new CancellationTokenSource();
+ var cancellactionToken = cts.Token;
+
+ var task = Task.Run(() => func(cancellactionToken), cancellactionToken);
+ var timeoutTask = Task.Delay(timeout);
+
+ if (await Task.WhenAny(task, timeoutTask) == timeoutTask)
+ {
+ // タイムアウト
+
+ // キャンセル後のタスクで発生した例外は無視する
+ static async Task IgnoreExceptions(Task task)
{
- response?.Dispose();
+ try
+ {
+ await task.ConfigureAwait(false);
+ }
+ catch
+ {
+ }
}
+ _ = IgnoreExceptions(task);
+ cts.Cancel();
+
+ throw new OperationCanceledException("Timeout", cancellactionToken);
}
+
+ return await task;
}
- protected async Task CheckStatusCode(HttpResponseMessage response)
+ protected static async Task CheckStatusCode(HttpResponseMessage response)
{
var statusCode = response.StatusCode;
- if (statusCode == HttpStatusCode.OK)
+
+ if ((int)statusCode >= 200 && (int)statusCode <= 299)
{
Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
return;
Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
}
- throw new TwitterApiException(error, responseText);
+ throw new TwitterApiException(statusCode, error, responseText);
}
catch (SerializationException)
{
}
}
- public OAuthEchoHandler CreateOAuthEchoHandler(Uri authServiceProvider, Uri realm = null)
+ public OAuthEchoHandler CreateOAuthEchoHandler(HttpMessageHandler innerHandler, Uri authServiceProvider, Uri? realm = null)
{
var uri = new Uri(RestApiBase, authServiceProvider);
- return OAuthEchoHandler.CreateHandler(Networking.CreateHttpClientHandler(), uri,
- ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret,
- this.AccessToken, this.AccessSecret, realm);
+ if (this.Credential is TwitterCredentialOAuth1 oauthCredential)
+ {
+ return OAuthEchoHandler.CreateHandler(
+ innerHandler,
+ uri,
+ oauthCredential.AppToken.OAuth1ConsumerKey,
+ oauthCredential.AppToken.OAuth1ConsumerSecret,
+ oauthCredential.Token,
+ oauthCredential.TokenSecret,
+ realm);
+ }
+ else
+ {
+ // MobipictureApi クラス向けの暫定対応
+ return OAuthEchoHandler.CreateHandler(
+ innerHandler,
+ uri,
+ ApiKey.Create(""),
+ ApiKey.Create(""),
+ "",
+ "",
+ realm);
+ }
}
public void Dispose()
if (disposing)
{
Networking.WebProxyChanged -= this.Networking_WebProxyChanged;
- this.http.Dispose();
+ this.Http.Dispose();
+ this.HttpUpload.Dispose();
+ this.HttpStreaming.Dispose();
}
}
~TwitterApiConnection()
- {
- this.Dispose(false);
- }
+ => this.Dispose(false);
private void Networking_WebProxyChanged(object sender, EventArgs e)
- {
- this.http = InitializeHttpClient(this.AccessToken, this.AccessSecret);
- }
+ => this.InitializeHttpClients();
- public static async Task<Tuple<string, string>> GetRequestTokenAsync()
+ public static async Task<TwitterCredentialOAuth1> GetRequestTokenAsync(TwitterAppToken appToken)
{
+ var emptyCredential = new TwitterCredentialOAuth1(appToken, "", "");
var param = new Dictionary<string, string>
{
["oauth_callback"] = "oob",
};
- var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/request_token"), param, oauthToken: null)
+ var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/request_token"), param, emptyCredential)
.ConfigureAwait(false);
- return Tuple.Create(response["oauth_token"], response["oauth_token_secret"]);
+ return new(appToken, response["oauth_token"], response["oauth_token_secret"]);
}
- public static Uri GetAuthorizeUri(Tuple<string, string> requestToken, string screenName = null)
+ public static Uri GetAuthorizeUri(TwitterCredentialOAuth1 requestToken, string? screenName = null)
{
var param = new Dictionary<string, string>
{
- ["oauth_token"] = requestToken.Item1,
+ ["oauth_token"] = requestToken.Token,
};
if (screenName != null)
return new Uri("https://api.twitter.com/oauth/authorize?" + MyCommon.BuildQueryString(param));
}
- public static async Task<IDictionary<string, string>> GetAccessTokenAsync(Tuple<string, string> requestToken, string verifier)
+ public static async Task<IDictionary<string, string>> GetAccessTokenAsync(TwitterCredentialOAuth1 credential, string verifier)
{
var param = new Dictionary<string, string>
{
["oauth_verifier"] = verifier,
};
- var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/access_token"), param, requestToken)
+ var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/access_token"), param, credential)
.ConfigureAwait(false);
return response;
}
- private static async Task<IDictionary<string, string>> GetOAuthTokenAsync(Uri uri, IDictionary<string, string> param,
- Tuple<string, string> oauthToken)
+ private static async Task<IDictionary<string, string>> GetOAuthTokenAsync(
+ Uri uri,
+ IDictionary<string, string> param,
+ TwitterCredentialOAuth1 credential
+ )
{
- HttpClient authorizeClient;
- if (oauthToken != null)
- authorizeClient = InitializeHttpClient(oauthToken.Item1, oauthToken.Item2);
- else
- authorizeClient = InitializeHttpClient("", "");
+ using var authorizeClient = InitializeHttpClient(credential);
var requestUri = new Uri(uri, "?" + MyCommon.BuildQueryString(param));
try
{
- using (var request = new HttpRequestMessage(HttpMethod.Post, requestUri))
- using (var response = await authorizeClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
- .ConfigureAwait(false))
- using (var content = response.Content)
- {
- var responseText = await content.ReadAsStringAsync()
- .ConfigureAwait(false);
+ using var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
+ using var response = await authorizeClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
+ .ConfigureAwait(false);
+
+ using var content = response.Content;
+ var responseText = await content.ReadAsStringAsync()
+ .ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- throw new TwitterApiException(response.StatusCode, responseText);
+ await TwitterApiConnection.CheckStatusCode(response)
+ .ConfigureAwait(false);
- var responseParams = HttpUtility.ParseQueryString(responseText);
+ var responseParams = HttpUtility.ParseQueryString(responseText);
- return responseParams.Cast<string>()
- .ToDictionary(x => x, x => responseParams[x]);
- }
+ return responseParams.Cast<string>()
+ .ToDictionary(x => x, x => responseParams[x]);
}
catch (HttpRequestException ex)
{
}
}
- private static HttpClient InitializeHttpClient(string accessToken, string accessSecret)
+ private static HttpClient InitializeHttpClient(ITwitterCredential credential, bool disableGzip = false)
{
- var innerHandler = Networking.CreateHttpClientHandler();
- innerHandler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
+ var builder = Networking.CreateHttpClientBuilder();
+
+ builder.SetupHttpClientHandler(x =>
+ {
+ x.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
+
+ if (disableGzip)
+ x.AutomaticDecompression = DecompressionMethods.None;
+ });
- var handler = new OAuthHandler(innerHandler,
- ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret,
- accessToken, accessSecret);
+ builder.AddHandler(x => credential.CreateHttpHandler(x));
- return Networking.CreateHttpClient(handler);
+ return builder.Build();
}
}
}