diff --git a/OpenTween.Tests/Api/TwitterApiTest.cs b/OpenTween.Tests/Api/TwitterApiTest.cs index 1fde2e592..c660337a3 100644 --- a/OpenTween.Tests/Api/TwitterApiTest.cs +++ b/OpenTween.Tests/Api/TwitterApiTest.cs @@ -23,6 +23,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Net.Http; using System.Reflection; using System.Runtime.InteropServices; @@ -674,8 +675,9 @@ public async Task DirectMessagesEventsList_Test() [Fact] public async Task DirectMessagesEventsNew_Test() { + using var responseMessage = new HttpResponseMessage(HttpStatusCode.OK); var mock = new Mock(); - var responseText = """ + var requestJson = """ { "event": { "type": "message_create", @@ -697,11 +699,14 @@ public async Task DirectMessagesEventsNew_Test() } """; mock.Setup(x => - x.PostJsonAsync( - new Uri("direct_messages/events/new.json", UriKind.Relative), - responseText) + x.SendAsync( + It.Is(r => + r.RequestUri == new Uri("direct_messages/events/new.json", UriKind.Relative) && + r.JsonString == requestJson + ) + ) ) - .ReturnsAsync(LazyJson.Create(new TwitterMessageEventSingle())); + .ReturnsAsync(new ApiResponse(responseMessage)); using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; @@ -716,10 +721,15 @@ public async Task DirectMessagesEventsDestroy_Test() { var mock = new Mock(); mock.Setup(x => - x.DeleteAsync( - new Uri("direct_messages/events/destroy.json?id=100", UriKind.Relative)) - ) - .Returns(Task.CompletedTask); + x.SendAsync( + It.Is(r => + r.RequestUri == new Uri("direct_messages/events/destroy.json", UriKind.Relative) && + r.Query != null && + r.Query.Count == 1 && + r.Query["id"] == "100" + ) + ) + ); using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; @@ -1304,11 +1314,13 @@ public async Task MediaMetadataCreate_Test() { var mock = new Mock(); mock.Setup(x => - x.PostJsonAsync( - new Uri("https://upload.twitter.com/1.1/media/metadata/create.json", UriKind.Absolute), - """{"media_id": "12345", "alt_text": {"text": "hogehoge"}}""") - ) - .ReturnsAsync(""); + x.SendAsync( + It.Is(r => + r.RequestUri == new Uri("https://upload.twitter.com/1.1/media/metadata/create.json") && + r.JsonString == """{"media_id": "12345", "alt_text": {"text": "hogehoge"}}""" + ) + ) + ); using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; diff --git a/OpenTween.Tests/Connection/DeleteRequestTest.cs b/OpenTween.Tests/Connection/DeleteRequestTest.cs new file mode 100644 index 000000000..3b72c5b0d --- /dev/null +++ b/OpenTween.Tests/Connection/DeleteRequestTest.cs @@ -0,0 +1,50 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2023 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using Xunit; + +namespace OpenTween.Connection +{ + public class DeleteRequestTest + { + [Fact] + public void CreateMessage_Test() + { + var request = new DeleteRequest + { + RequestUri = new("hoge/aaa.json", UriKind.Relative), + Query = new Dictionary + { + ["id"] = "12345", + }, + }; + + var baseUri = new Uri("https://example.com/v1/"); + using var requestMessage = request.CreateMessage(baseUri); + + Assert.Equal(HttpMethod.Delete, requestMessage.Method); + Assert.Equal(new("https://example.com/v1/hoge/aaa.json?id=12345"), requestMessage.RequestUri); + } + } +} diff --git a/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs b/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs index c646f07bb..cc8935625 100644 --- a/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs +++ b/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs @@ -280,52 +280,6 @@ public async Task SendAsync_ErrorJsonTest() Assert.Equal(0, mockHandler.QueueCount); } - [Fact] - public async Task GetStreamAsync_Test() - { - using var mockHandler = new HttpMessageHandlerMock(); - using var http = new HttpClient(mockHandler); - using var apiConnection = new TwitterApiConnection(); - using var image = TestUtils.CreateDummyImage(); - apiConnection.Http = http; - - mockHandler.Enqueue(x => - { - Assert.Equal(HttpMethod.Get, x.Method); - Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json", - x.RequestUri.GetLeftPart(UriPartial.Path)); - - var query = HttpUtility.ParseQueryString(x.RequestUri.Query); - - Assert.Equal("1111", query["aaaa"]); - Assert.Equal("2222", query["bbbb"]); - - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new ByteArrayContent(image.Stream.ToArray()), - }; - }); - - var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); - var param = new Dictionary - { - ["aaaa"] = "1111", - ["bbbb"] = "2222", - }; - - var stream = await apiConnection.GetStreamAsync(endpoint, param); - - using (var memoryStream = new MemoryStream()) - { - // 内容の比較のために MemoryStream にコピー - await stream.CopyToAsync(memoryStream); - - Assert.Equal(image.Stream.ToArray(), memoryStream.ToArray()); - } - - Assert.Equal(0, mockHandler.QueueCount); - } - [Fact] public async Task PostLazyAsync_Test() { @@ -481,99 +435,6 @@ public async Task PostLazyAsync_Multipart_NullTest() Assert.Equal(0, mockHandler.QueueCount); } - [Fact] - public async Task PostJsonAsync_Test() - { - using var mockHandler = new HttpMessageHandlerMock(); - using var http = new HttpClient(mockHandler); - using var apiConnection = new TwitterApiConnection(); - apiConnection.Http = http; - - mockHandler.Enqueue(async x => - { - Assert.Equal(HttpMethod.Post, x.Method); - Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json", - x.RequestUri.AbsoluteUri); - - Assert.Equal("application/json; charset=utf-8", x.Content.Headers.ContentType.ToString()); - - var body = await x.Content.ReadAsStringAsync(); - - Assert.Equal("""{"aaaa": 1111}""", body); - - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(@"{""ok"":true}"), - }; - }); - - var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); - - var response = await apiConnection.PostJsonAsync(endpoint, """{"aaaa": 1111}"""); - - Assert.Equal(@"{""ok"":true}", response); - Assert.Equal(0, mockHandler.QueueCount); - } - - [Fact] - public async Task PostJsonAsync_T_Test() - { - using var mockHandler = new HttpMessageHandlerMock(); - using var http = new HttpClient(mockHandler); - using var apiConnection = new TwitterApiConnection(); - apiConnection.Http = http; - - mockHandler.Enqueue(async x => - { - Assert.Equal(HttpMethod.Post, x.Method); - Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json", - x.RequestUri.AbsoluteUri); - - Assert.Equal("application/json; charset=utf-8", x.Content.Headers.ContentType.ToString()); - - var body = await x.Content.ReadAsStringAsync(); - - Assert.Equal("""{"aaaa": 1111}""", body); - - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("\"hogehoge\""), - }; - }); - - var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); - var response = await apiConnection.PostJsonAsync(endpoint, """{"aaaa": 1111}"""); - var result = await response.LoadJsonAsync(); - - Assert.Equal("hogehoge", result); - - Assert.Equal(0, mockHandler.QueueCount); - } - - [Fact] - public async Task DeleteAsync_Test() - { - using var mockHandler = new HttpMessageHandlerMock(); - using var http = new HttpClient(mockHandler); - using var apiConnection = new TwitterApiConnection(); - apiConnection.Http = http; - - mockHandler.Enqueue(x => - { - Assert.Equal(HttpMethod.Delete, x.Method); - Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json", - x.RequestUri.AbsoluteUri); - - return new HttpResponseMessage(HttpStatusCode.NoContent); - }); - - var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); - - await apiConnection.DeleteAsync(endpoint); - - Assert.Equal(0, mockHandler.QueueCount); - } - [Fact] public async Task HandleTimeout_SuccessTest() { diff --git a/OpenTween/Api/TwitterApi.cs b/OpenTween/Api/TwitterApi.cs index 6614555e7..342122287 100644 --- a/OpenTween/Api/TwitterApi.cs +++ b/OpenTween/Api/TwitterApi.cs @@ -425,10 +425,8 @@ public Task DirectMessagesEventsList(int? count = null, return this.Connection.GetAsync(endpoint, param, "/direct_messages/events/list"); } - public Task> DirectMessagesEventsNew(long recipientId, string text, long? mediaId = null) + public async Task> DirectMessagesEventsNew(long recipientId, string text, long? mediaId = null) { - var endpoint = new Uri("direct_messages/events/new.json", UriKind.Relative); - var attachment = ""; if (mediaId != null) { @@ -458,21 +456,32 @@ public Task> DirectMessagesEventsNew(long re } """; - return this.Connection.PostJsonAsync(endpoint, json); + var request = new PostJsonRequest + { + RequestUri = new("direct_messages/events/new.json", UriKind.Relative), + JsonString = json, + }; + + var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task DirectMessagesEventsDestroy(TwitterDirectMessageId eventId) + public async Task DirectMessagesEventsDestroy(TwitterDirectMessageId eventId) { - var endpoint = new Uri("direct_messages/events/destroy.json", UriKind.Relative); - var param = new Dictionary + var request = new DeleteRequest { - ["id"] = eventId.Id, + RequestUri = new("direct_messages/events/destroy.json", UriKind.Relative), + Query = new Dictionary + { + ["id"] = eventId.Id, + }, }; - // なぜか application/x-www-form-urlencoded でパラメーターを送ると Bad Request になる謎仕様 - endpoint = new Uri(endpoint.OriginalString + "?" + MyCommon.BuildQueryString(param), UriKind.Relative); - - return this.Connection.DeleteAsync(endpoint); + await this.Connection.SendAsync(request) + .IgnoreResponse() + .ConfigureAwait(false); } public Task UsersShow(string screenName) @@ -792,14 +801,18 @@ public Task MediaUploadStatus(long mediaId) return this.Connection.GetAsync(endpoint, param, endpointName: null); } - public Task MediaMetadataCreate(long mediaId, string altText) + public async Task MediaMetadataCreate(long mediaId, string altText) { - var endpoint = new Uri("https://upload.twitter.com/1.1/media/metadata/create.json"); - var escapedAltText = JsonUtils.EscapeJsonString(altText); - var json = $$$"""{"media_id": "{{{mediaId}}}", "alt_text": {"text": "{{{escapedAltText}}}"}}"""; + var request = new PostJsonRequest + { + RequestUri = new("https://upload.twitter.com/1.1/media/metadata/create.json"), + JsonString = $$$"""{"media_id": "{{{mediaId}}}", "alt_text": {"text": "{{{escapedAltText}}}"}}""", + }; - return this.Connection.PostJsonAsync(endpoint, json); + await this.Connection.SendAsync(request) + .IgnoreResponse() + .ConfigureAwait(false); } public OAuthEchoHandler CreateOAuthEchoHandler(HttpMessageHandler innerHandler, Uri authServiceProvider, Uri? realm = null) diff --git a/OpenTween/Connection/ApiResponse.cs b/OpenTween/Connection/ApiResponse.cs index ee1560058..da03230e9 100644 --- a/OpenTween/Connection/ApiResponse.cs +++ b/OpenTween/Connection/ApiResponse.cs @@ -114,4 +114,13 @@ public async Task ReadAsString() .ConfigureAwait(false); } } + + public static class ApiResponseTaskExtension + { + public static async Task IgnoreResponse(this Task task) + { + using var response = await task.ConfigureAwait(false); + // レスポンスボディを読み込まず破棄する + } + } } diff --git a/OpenTween/Connection/DeleteRequest.cs b/OpenTween/Connection/DeleteRequest.cs new file mode 100644 index 000000000..f15502814 --- /dev/null +++ b/OpenTween/Connection/DeleteRequest.cs @@ -0,0 +1,45 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2023 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// 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.Net.Http; + +namespace OpenTween.Connection +{ + public class DeleteRequest : IHttpRequest + { + public required Uri RequestUri { get; set; } + + public IDictionary? Query { get; set; } + + public string? EndpointName { get; set; } + + public HttpRequestMessage CreateMessage(Uri baseUri) + => new() + { + Method = HttpMethod.Delete, + RequestUri = GetRequest.BuildUriWithQuery(new(baseUri, this.RequestUri), this.Query), + }; + } +} diff --git a/OpenTween/Connection/IApiConnectionLegacy.cs b/OpenTween/Connection/IApiConnectionLegacy.cs index 4175e589d..839ad462e 100644 --- a/OpenTween/Connection/IApiConnectionLegacy.cs +++ b/OpenTween/Connection/IApiConnectionLegacy.cs @@ -34,22 +34,10 @@ public interface IApiConnectionLegacy : IApiConnection, IDisposable { Task GetAsync(Uri uri, IDictionary? param, string? endpointName); - Task GetStreamAsync(Uri uri, IDictionary? param); - - Task GetStreamAsync(Uri uri, IDictionary? param, string? endpointName); - - Task GetStreamingStreamAsync(Uri uri, IDictionary? param); - Task> PostLazyAsync(Uri uri, IDictionary? param); Task> PostLazyAsync(Uri uri, IDictionary? param, IDictionary? media); Task PostAsync(Uri uri, IDictionary? param, IDictionary? media); - - Task PostJsonAsync(Uri uri, string json); - - Task> PostJsonAsync(Uri uri, string json); - - Task DeleteAsync(Uri uri); } } diff --git a/OpenTween/Connection/TwitterApiConnection.cs b/OpenTween/Connection/TwitterApiConnection.cs index bfcc99b0d..0e860607c 100644 --- a/OpenTween/Connection/TwitterApiConnection.cs +++ b/OpenTween/Connection/TwitterApiConnection.cs @@ -54,7 +54,6 @@ public static string RestApiHost internal HttpClient Http; internal HttpClient HttpUpload; - internal HttpClient HttpStreaming; internal ITwitterCredential Credential { get; } @@ -71,16 +70,13 @@ public TwitterApiConnection(ITwitterCredential credential) Networking.WebProxyChanged += this.Networking_WebProxyChanged; } - [MemberNotNull(nameof(Http), nameof(HttpUpload), nameof(HttpStreaming))] + [MemberNotNull(nameof(Http), nameof(HttpUpload))] 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 SendAsync(IHttpRequest request) @@ -164,73 +160,6 @@ private void ThrowIfRateLimitExceeded(string endpointName) } } - public Task GetStreamAsync(Uri uri, IDictionary? param) - => this.GetStreamAsync(uri, param, null); - - public async Task GetStreamAsync(Uri uri, IDictionary? 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)); - - try - { - 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) - { - throw TwitterApiException.CreateFromException(ex); - } - catch (OperationCanceledException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - } - - public async Task GetStreamingStreamAsync(Uri uri, IDictionary? param) - { - var requestUri = new Uri(RestApiBase, uri); - - if (param != null) - requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param)); - - try - { - 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) - { - throw TwitterApiException.CreateFromException(ex); - } - catch (OperationCanceledException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - } - public async Task> PostLazyAsync(Uri uri, IDictionary? param) { var requestUri = new Uri(RestApiBase, uri); @@ -351,58 +280,6 @@ await TwitterApiConnection.CheckStatusCode(response) } } - public async Task PostJsonAsync(Uri uri, string json) - { - var request = new PostJsonRequest - { - RequestUri = uri, - JsonString = json, - }; - - using var response = await this.SendAsync(request) - .ConfigureAwait(false); - - return await response.ReadAsString() - .ConfigureAwait(false); - } - - public async Task> PostJsonAsync(Uri uri, string json) - { - var request = new PostJsonRequest - { - RequestUri = uri, - JsonString = json, - }; - - using var response = await this.SendAsync(request) - .ConfigureAwait(false); - - return response.ReadAsLazyJson(); - } - - 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 HandleTimeout(Func> func, TimeSpan timeout) { using var cts = new CancellationTokenSource(); @@ -528,7 +405,6 @@ protected virtual void Dispose(bool disposing) Networking.WebProxyChanged -= this.Networking_WebProxyChanged; this.Http.Dispose(); this.HttpUpload.Dispose(); - this.HttpStreaming.Dispose(); } } @@ -614,17 +490,13 @@ await TwitterApiConnection.CheckStatusCode(response) } } - private static HttpClient InitializeHttpClient(ITwitterCredential credential, bool disableGzip = false) + private static HttpClient InitializeHttpClient(ITwitterCredential credential) { var builder = Networking.CreateHttpClientBuilder(); - builder.SetupHttpClientHandler(x => - { - x.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache); - - if (disableGzip) - x.AutomaticDecompression = DecompressionMethods.None; - }); + builder.SetupHttpClientHandler( + x => x.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache) + ); builder.AddHandler(x => credential.CreateHttpHandler(x)); diff --git a/OpenTween/ErrorReportHandler.cs b/OpenTween/ErrorReportHandler.cs index af9d572f6..166b1727b 100644 --- a/OpenTween/ErrorReportHandler.cs +++ b/OpenTween/ErrorReportHandler.cs @@ -106,18 +106,6 @@ public static bool IsExceptionIgnorable(Exception ex) return true; } - if (ex is TaskCanceledException cancelEx) - { - // ton.twitter.com の画像でタイムアウトした場合、try-catch で例外がキャッチできない - // https://osdn.net/ticket/browse.php?group_id=6526&tid=37433 - var stackTrace = new StackTrace(cancelEx); - var lastFrameMethod = stackTrace.GetFrame(stackTrace.FrameCount - 1).GetMethod(); - var matchClass = lastFrameMethod.ReflectedType == typeof(TwitterApiConnection); - var matchMethod = lastFrameMethod.Name == nameof(TwitterApiConnection.GetStreamAsync); - if (matchClass && matchMethod) - return true; - } - return false; } }