OSDN Git Service

ITwitterCredentialとアクセス手段ごとの具象クラスを追加
[opentween/open-tween.git] / OpenTween / Connection / TwitterApiConnection.cs
index f42d93d..bfcc99b 100644 (file)
@@ -23,6 +23,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
 using System.Net;
@@ -38,9 +39,9 @@ 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 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
@@ -51,74 +52,65 @@ 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;
 
-        internal HttpClient http = null!;
-        internal HttpClient httpUpload = null!;
-        internal HttpClient httpStreaming = null!;
+        internal ITwitterCredential Credential { get; }
 
-        private readonly ApiKey consumerKey;
-        private readonly ApiKey consumerSecret;
+        public TwitterApiConnection()
+            : this(new TwitterCredentialNone())
+        {
+        }
 
-        public TwitterApiConnection(ApiKey consumerKey, ApiKey consumerSecret, string accessToken, string accessSecret)
+        public TwitterApiConnection(ITwitterCredential credential)
         {
-            this.consumerKey = consumerKey;
-            this.consumerSecret = consumerSecret;
-            this.AccessToken = accessToken;
-            this.AccessSecret = accessSecret;
+            this.Credential = credential;
 
             this.InitializeHttpClients();
             Networking.WebProxyChanged += this.Networking_WebProxyChanged;
         }
 
+        [MemberNotNull(nameof(Http), nameof(HttpUpload), nameof(HttpStreaming))]
         private void InitializeHttpClients()
         {
-            this.http = InitializeHttpClient(this.consumerKey, this.consumerSecret, this.AccessToken, this.AccessSecret);
+            this.Http = InitializeHttpClient(this.Credential);
 
-            this.httpUpload = InitializeHttpClient(this.consumerKey, this.consumerSecret, this.AccessToken, this.AccessSecret);
-            this.httpUpload.Timeout = Networking.UploadImageTimeout;
+            this.HttpUpload = InitializeHttpClient(this.Credential);
+            this.HttpUpload.Timeout = Networking.UploadImageTimeout;
 
-            this.httpStreaming = InitializeHttpClient(this.consumerKey, this.consumerSecret, this.AccessToken, this.AccessSecret, disableGzip: true);
-            this.httpStreaming.Timeout = Timeout.InfiniteTimeSpan;
+            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 this.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)
             {
@@ -128,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>
@@ -152,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)
@@ -161,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)
@@ -184,10 +212,10 @@ namespace OpenTween.Connection
             try
             {
                 var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
-                var response = await this.httpStreaming.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
+                var response = await this.HttpStreaming.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
                     .ConfigureAwait(false);
 
-                await this.CheckStatusCode(response)
+                await TwitterApiConnection.CheckStatusCode(response)
                     .ConfigureAwait(false);
 
                 return await response.Content.ReadAsStreamAsync()
@@ -214,10 +242,10 @@ namespace OpenTween.Connection
             HttpResponseMessage? response = null;
             try
             {
-                response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
+                response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
                     .ConfigureAwait(false);
 
-                await this.CheckStatusCode(response)
+                await TwitterApiConnection.CheckStatusCode(response)
                     .ConfigureAwait(false);
 
                 var result = new LazyJson<T>(response);
@@ -261,10 +289,10 @@ namespace OpenTween.Connection
             HttpResponseMessage? response = null;
             try
             {
-                response = await this.httpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
+                response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
                     .ConfigureAwait(false);
 
-                await this.CheckStatusCode(response)
+                await TwitterApiConnection.CheckStatusCode(response)
                     .ConfigureAwait(false);
 
                 var result = new LazyJson<T>(response);
@@ -307,10 +335,10 @@ namespace OpenTween.Connection
 
             try
             {
-                using var response = await this.httpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
+                using var response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
                     .ConfigureAwait(false);
 
-                await this.CheckStatusCode(response)
+                await TwitterApiConnection.CheckStatusCode(response)
                     .ConfigureAwait(false);
             }
             catch (HttpRequestException ex)
@@ -323,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 this.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)
@@ -371,10 +387,10 @@ namespace OpenTween.Connection
 
             try
             {
-                using var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
+                using var response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
                     .ConfigureAwait(false);
 
-                await this.CheckStatusCode(response)
+                await TwitterApiConnection.CheckStatusCode(response)
                     .ConfigureAwait(false);
             }
             catch (HttpRequestException ex)
@@ -387,7 +403,39 @@ namespace OpenTween.Connection
             }
         }
 
-        protected async Task CheckStatusCode(HttpResponseMessage response)
+        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;
 
@@ -433,12 +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.consumerKey, this.consumerSecret, 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()
@@ -457,9 +526,9 @@ namespace OpenTween.Connection
             if (disposing)
             {
                 Networking.WebProxyChanged -= this.Networking_WebProxyChanged;
-                this.http.Dispose();
-                this.httpUpload.Dispose();
-                this.httpStreaming.Dispose();
+                this.Http.Dispose();
+                this.HttpUpload.Dispose();
+                this.HttpStreaming.Dispose();
             }
         }
 
@@ -469,22 +538,20 @@ namespace OpenTween.Connection
         private void Networking_WebProxyChanged(object sender, EventArgs e)
             => this.InitializeHttpClients();
 
-        public static Task<(string Token, string TokenSecret)> GetRequestTokenAsync()
-            => GetRequestTokenAsync(ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret);
-
-        public static async Task<(string Token, string TokenSecret)> GetRequestTokenAsync(ApiKey consumerKey, ApiKey consumerSecret)
+        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, consumerKey, consumerSecret, 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>
             {
@@ -497,29 +564,25 @@ namespace OpenTween.Connection
             return new Uri("https://api.twitter.com/oauth/authorize?" + MyCommon.BuildQueryString(param));
         }
 
-        public static Task<IDictionary<string, string>> GetAccessTokenAsync((string Token, string TokenSecret) requestToken, string verifier)
-            => GetAccessTokenAsync(ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret, requestToken, verifier);
-
-        public static async Task<IDictionary<string, string>> GetAccessTokenAsync(ApiKey consumerKey, ApiKey consumerSecret, (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, consumerKey, consumerSecret, 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,
-            ApiKey consumerKey, ApiKey consumerSecret, (string Token, string TokenSecret)? oauthToken)
+        private static async Task<IDictionary<string, string>> GetOAuthTokenAsync(
+            Uri uri,
+            IDictionary<string, string> param,
+            TwitterCredentialOAuth1 credential
+        )
         {
-            HttpClient authorizeClient;
-            if (oauthToken != null)
-                authorizeClient = InitializeHttpClient(consumerKey, consumerSecret, oauthToken.Value.Token, oauthToken.Value.TokenSecret);
-            else
-                authorizeClient = InitializeHttpClient(consumerKey, consumerSecret, "", "");
+            using var authorizeClient = InitializeHttpClient(credential);
 
             var requestUri = new Uri(uri, "?" + MyCommon.BuildQueryString(param));
 
@@ -533,8 +596,8 @@ namespace OpenTween.Connection
                 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);
 
@@ -551,17 +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);
+            var builder = Networking.CreateHttpClientBuilder();
+
+            builder.SetupHttpClientHandler(x =>
+            {
+                x.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
 
-            if (disableGzip)
-                innerHandler.AutomaticDecompression = DecompressionMethods.None;
+                if (disableGzip)
+                    x.AutomaticDecompression = DecompressionMethods.None;
+            });
 
-            var handler = new OAuthHandler(innerHandler, consumerKey, consumerSecret, accessToken, accessSecret);
+            builder.AddHandler(x => credential.CreateHttpHandler(x));
 
-            return Networking.CreateHttpClient(handler);
+            return builder.Build();
         }
     }
 }