using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net;
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
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)
{
{
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)
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()
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);
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);
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)
}
}
- 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)
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)
}
}
- 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;
}
}
- 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()
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();
}
}
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>
{
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));
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);
}
}
- 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();
}
}
}