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/");
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;
[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)
{
{
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>
}
}
- 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)
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)
}
}
- 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)
}
}
+ 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;
}
}
- 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()
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>
{
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;
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));
}
}
- 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();
}
}
}