OSDN Git Service

ITwitterCredentialとアクセス手段ごとの具象クラスを追加
[opentween/open-tween.git] / OpenTween / Connection / TwitterApiConnection.cs
index 3431779..bfcc99b 100644 (file)
@@ -39,7 +39,7 @@ using OpenTween.Api.DataModel;
 
 namespace OpenTween.Connection
 {
-    public class TwitterApiConnection : IApiConnection, IDisposable
+    public class TwitterApiConnection : IApiConnection, IApiConnectionLegacy, IDisposable
     {
         public static Uri RestApiBase { get; set; } = new("https://api.twitter.com/1.1/");
 
@@ -52,35 +52,20 @@ namespace OpenTween.Connection
 
         public bool IsDisposed { get; private set; } = false;
 
-        public string AccessToken { get; }
-
-        public string AccessSecret { get; }
-
         internal HttpClient Http;
         internal HttpClient HttpUpload;
         internal HttpClient HttpStreaming;
 
-        private readonly TwitterAppToken appToken;
+        internal ITwitterCredential Credential { get; }
 
-        public TwitterApiConnection(ApiKey consumerKey, ApiKey consumerSecret, string accessToken, string accessSecret)
-            : this(
-                new()
-                {
-                    AuthType = APIAuthType.OAuth1,
-                    OAuth1CustomConsumerKey = consumerKey,
-                    OAuth1CustomConsumerSecret = consumerSecret,
-                },
-                accessToken,
-                accessSecret
-            )
+        public TwitterApiConnection()
+            : this(new TwitterCredentialNone())
         {
         }
 
-        public TwitterApiConnection(TwitterAppToken appToken, string accessToken, string accessSecret)
+        public TwitterApiConnection(ITwitterCredential credential)
         {
-            this.appToken = appToken;
-            this.AccessToken = accessToken;
-            this.AccessSecret = accessSecret;
+            this.Credential = credential;
 
             this.InitializeHttpClients();
             Networking.WebProxyChanged += this.Networking_WebProxyChanged;
@@ -89,51 +74,43 @@ namespace OpenTween.Connection
         [MemberNotNull(nameof(Http), nameof(HttpUpload), nameof(HttpStreaming))]
         private void InitializeHttpClients()
         {
-            this.Http = InitializeHttpClient(this.appToken, this.AccessToken, this.AccessSecret);
+            this.Http = InitializeHttpClient(this.Credential);
 
-            this.HttpUpload = InitializeHttpClient(this.appToken, this.AccessToken, this.AccessSecret);
+            this.HttpUpload = InitializeHttpClient(this.Credential);
             this.HttpUpload.Timeout = Networking.UploadImageTimeout;
 
-            this.HttpStreaming = InitializeHttpClient(this.appToken, this.AccessToken, this.AccessSecret, disableGzip: true);
+            this.HttpStreaming = InitializeHttpClient(this.Credential, disableGzip: true);
             this.HttpStreaming.Timeout = Timeout.InfiniteTimeSpan;
         }
 
-        public async Task<T> GetAsync<T>(Uri uri, IDictionary<string, string>? param, string? endpointName)
+        public async Task<ApiResponse> SendAsync(IHttpRequest request)
         {
+            var endpointName = request.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);
+            using var requestMessage = request.CreateMessage(RestApiBase);
 
+            HttpResponseMessage? responseMessage = null;
             try
             {
-                using var response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
-                    .ConfigureAwait(false);
+                responseMessage = await HandleTimeout(
+                    (token) => this.Http.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, token),
+                    Networking.DefaultTimeout
+                );
 
                 if (endpointName != null)
-                    MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName);
+                    MyCommon.TwitterApiInfo.UpdateFromHeader(responseMessage.Headers, endpointName);
 
-                await TwitterApiConnection.CheckStatusCode(response)
+                await TwitterApiConnection.CheckStatusCode(responseMessage)
                     .ConfigureAwait(false);
 
-                using var content = response.Content;
-                var responseText = await content.ReadAsStringAsync()
-                    .ConfigureAwait(false);
+                var response = new ApiResponse(responseMessage);
+                responseMessage = null; // responseMessage は ApiResponse で使用するため破棄されないようにする
 
-                try
-                {
-                    return MyCommon.CreateDataFromJson<T>(responseText);
-                }
-                catch (SerializationException ex)
-                {
-                    throw TwitterApiException.CreateFromException(ex, responseText);
-                }
+                return response;
             }
             catch (HttpRequestException ex)
             {
@@ -143,6 +120,26 @@ namespace OpenTween.Connection
             {
                 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>
@@ -167,8 +164,15 @@ namespace OpenTween.Connection
             }
         }
 
-        public async Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param)
+        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)
@@ -176,7 +180,16 @@ namespace OpenTween.Connection
 
             try
             {
-                return await this.Http.GetStreamAsync(requestUri)
+                var response = await this.Http.GetAsync(requestUri)
+                    .ConfigureAwait(false);
+
+                if (endpointName != null)
+                    MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName);
+
+                await TwitterApiConnection.CheckStatusCode(response)
+                    .ConfigureAwait(false);
+
+                return await response.Content.ReadAsStreamAsync()
                     .ConfigureAwait(false);
             }
             catch (HttpRequestException ex)
@@ -338,45 +351,33 @@ namespace OpenTween.Connection
             }
         }
 
-        public async Task PostJsonAsync(Uri uri, string json)
-            => await this.PostJsonAsync<object>(uri, json)
-                         .IgnoreResponse()
-                         .ConfigureAwait(false);
-
-        public async Task<LazyJson<T>> PostJsonAsync<T>(Uri uri, string json)
+        public async Task<string> PostJsonAsync(Uri uri, string json)
         {
-            var requestUri = new Uri(RestApiBase, uri);
-            var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
-
-            using var postContent = new StringContent(json, Encoding.UTF8, "application/json");
-            request.Content = postContent;
-
-            HttpResponseMessage? response = null;
-            try
+            var request = new PostJsonRequest
             {
-                response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
-                    .ConfigureAwait(false);
+                RequestUri = uri,
+                JsonString = json,
+            };
 
-                await TwitterApiConnection.CheckStatusCode(response)
-                    .ConfigureAwait(false);
+            using var response = await this.SendAsync(request)
+                .ConfigureAwait(false);
 
-                var result = new LazyJson<T>(response);
-                response = null;
+            return await response.ReadAsString()
+                .ConfigureAwait(false);
+        }
 
-                return result;
-            }
-            catch (HttpRequestException ex)
-            {
-                throw TwitterApiException.CreateFromException(ex);
-            }
-            catch (OperationCanceledException ex)
-            {
-                throw TwitterApiException.CreateFromException(ex);
-            }
-            finally
+        public async Task<LazyJson<T>> PostJsonAsync<T>(Uri uri, string json)
+        {
+            var request = new PostJsonRequest
             {
-                response?.Dispose();
-            }
+                RequestUri = uri,
+                JsonString = json,
+            };
+
+            using var response = await this.SendAsync(request)
+                .ConfigureAwait(false);
+
+            return response.ReadAsLazyJson<T>();
         }
 
         public async Task DeleteAsync(Uri uri)
@@ -402,6 +403,38 @@ namespace OpenTween.Connection
             }
         }
 
+        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)
+                {
+                    try
+                    {
+                        await task.ConfigureAwait(false);
+                    }
+                    catch
+                    {
+                    }
+                }
+                _ = IgnoreExceptions(task);
+                cts.Cancel();
+
+                throw new OperationCanceledException("Timeout", cancellactionToken);
+            }
+
+            return await task;
+        }
+
         protected static async Task CheckStatusCode(HttpResponseMessage response)
         {
             var statusCode = response.StatusCode;
@@ -448,18 +481,33 @@ namespace OpenTween.Connection
             }
         }
 
-        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,
-                this.appToken.OAuth1ConsumerKey,
-                this.appToken.OAuth1ConsumerSecret,
-                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()
@@ -490,19 +538,20 @@ namespace OpenTween.Connection
         private void Networking_WebProxyChanged(object sender, EventArgs e)
             => this.InitializeHttpClients();
 
-        public static async Task<(string Token, string TokenSecret)> GetRequestTokenAsync(TwitterAppToken appToken)
+        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, appToken, oauthToken: null)
+            var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/request_token"), param, emptyCredential)
                 .ConfigureAwait(false);
 
-            return (response["oauth_token"], response["oauth_token_secret"]);
+            return new(appToken, response["oauth_token"], response["oauth_token_secret"]);
         }
 
-        public static Uri GetAuthorizeUri((string Token, string TokenSecret) requestToken, string? screenName = null)
+        public static Uri GetAuthorizeUri(TwitterCredentialOAuth1 requestToken, string? screenName = null)
         {
             var param = new Dictionary<string, string>
             {
@@ -515,13 +564,13 @@ namespace OpenTween.Connection
             return new Uri("https://api.twitter.com/oauth/authorize?" + MyCommon.BuildQueryString(param));
         }
 
-        public static async Task<IDictionary<string, string>> GetAccessTokenAsync(TwitterAppToken appToken, (string Token, string TokenSecret) 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, appToken, requestToken)
+            var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/access_token"), param, credential)
                 .ConfigureAwait(false);
 
             return response;
@@ -530,14 +579,10 @@ namespace OpenTween.Connection
         private static async Task<IDictionary<string, string>> GetOAuthTokenAsync(
             Uri uri,
             IDictionary<string, string> param,
-            TwitterAppToken appToken,
-            (string Token, string TokenSecret)? oauthToken)
+            TwitterCredentialOAuth1 credential
+        )
         {
-            HttpClient authorizeClient;
-            if (oauthToken != null)
-                authorizeClient = InitializeHttpClient(appToken.OAuth1ConsumerKey, appToken.OAuth1ConsumerSecret, oauthToken.Value.Token, oauthToken.Value.TokenSecret);
-            else
-                authorizeClient = InitializeHttpClient(appToken.OAuth1ConsumerKey, appToken.OAuth1ConsumerSecret, "", "");
+            using var authorizeClient = InitializeHttpClient(credential);
 
             var requestUri = new Uri(uri, "?" + MyCommon.BuildQueryString(param));
 
@@ -569,37 +614,21 @@ namespace OpenTween.Connection
             }
         }
 
-        private static HttpClient InitializeHttpClient(ApiKey consumerKey, ApiKey consumerSecret, string accessToken, string accessSecret, bool disableGzip = false)
+        private static HttpClient InitializeHttpClient(ITwitterCredential credential, bool disableGzip = false)
         {
-            var innerHandler = Networking.CreateHttpClientHandler();
-            innerHandler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
-
-            if (disableGzip)
-                innerHandler.AutomaticDecompression = DecompressionMethods.None;
+            var builder = Networking.CreateHttpClientBuilder();
 
-            var handler = new OAuthHandler(innerHandler, consumerKey, consumerSecret, accessToken, accessSecret);
-
-            return Networking.CreateHttpClient(handler);
-        }
-
-        private static HttpClient InitializeHttpClient(TwitterAppToken appToken, string accessToken, string accessSecret, bool disableGzip = false)
-        {
-            var innerHandler = Networking.CreateHttpClientHandler();
-            innerHandler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
+            builder.SetupHttpClientHandler(x =>
+            {
+                x.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
 
-            if (disableGzip)
-                innerHandler.AutomaticDecompression = DecompressionMethods.None;
+                if (disableGzip)
+                    x.AutomaticDecompression = DecompressionMethods.None;
+            });
 
-            HttpMessageHandler handler = appToken.AuthType switch
-            {
-                APIAuthType.OAuth1
-                    => new OAuthHandler(innerHandler, appToken.OAuth1ConsumerKey, appToken.OAuth1ConsumerSecret, accessToken, accessSecret),
-                APIAuthType.TwitterComCookie
-                    => new TwitterComCookieHandler(innerHandler, appToken.TwitterComCookie),
-                _ => throw new NotImplementedException(),
-            };
+            builder.AddHandler(x => credential.CreateHttpHandler(x));
 
-            return Networking.CreateHttpClient(handler);
+            return builder.Build();
         }
     }
 }