Skip to content

Commit

Permalink
Merge branch 'develop' into release
Browse files Browse the repository at this point in the history
  • Loading branch information
upsilon committed Jan 6, 2024
2 parents d13b769 + 42c2934 commit 20c402b
Show file tree
Hide file tree
Showing 16 changed files with 340 additions and 93 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
更新履歴

==== Ver 3.11.0(2024/01/07)
* NEW: Cookie使用時の関連発言表示に対応
* FIX: APIリクエストのタイムアウト時に接続が切断されない場合がある不具合を修正
* FIX: 存在しないユーザーのプロフィールを取得しようとした場合のエラーが適切に処理されない不具合を修正

==== Ver 3.10.1(2023/12/23)
* FIX: OAuth 1.0a によるAPIアクセスに失敗する不具合を修正

Expand Down
41 changes: 41 additions & 0 deletions OpenTween.Tests/Api/GraphQL/TimelineResponseTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// OpenTween - Client of Twitter
// Copyright (c) 2024 kim_upsilon (@kim_upsilon) <https://upsilo.net/~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 <http://www.gnu.org/licenses/>, or write to
// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
// Boston, MA 02110-1301, USA.

using System.Threading.Tasks;
using Xunit;

namespace OpenTween.Api.GraphQL
{
public class TimelineResponseTest
{
[Fact]
public async Task ToTwitterStatuses_Test()
{
using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/SearchTimeline_SimpleTweet.json");
var tweets = TimelineTweet.ExtractTimelineTweets(await apiResponse.ReadAsJsonXml());
var timelineResponse = new TimelineResponse(tweets, "", "");

var statuses = timelineResponse.ToTwitterStatuses();
Assert.Single(statuses);
Assert.Equal("1619433164757413894", statuses[0].IdStr);
}
}
}
25 changes: 25 additions & 0 deletions OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,30 @@ public async Task Send_UserUnavailableTest()

mock.VerifyAll();
}

[Fact]
public async Task Send_EmptyTest()
{
// ユーザーが存在しない場合にエラー情報を含まない空のオブジェクトが返されることがある
using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/UserByScreenName_Empty.json");

var mock = new Mock<IApiConnection>();
mock.Setup(x =>
x.SendAsync(It.IsAny<IHttpRequest>())
)
.ReturnsAsync(apiResponse);

var request = new UserByScreenNameRequest
{
ScreenName = "==INVALID==",
};

var ex = await Assert.ThrowsAsync<WebApiException>(
() => request.Send(mock.Object)
);
Assert.Equal("User is not available.", ex.Message);

mock.VerifyAll();
}
}
}
128 changes: 128 additions & 0 deletions OpenTween.Tests/AsyncExceptionBoundaryTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// OpenTween - Client of Twitter
// Copyright (c) 2024 kim_upsilon (@kim_upsilon) <https://upsilo.net/~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 <http://www.gnu.org/licenses/>, or write to
// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
// Boston, MA 02110-1301, USA.

using System;
using System.IO;
using System.Threading.Tasks;
using Xunit;

namespace OpenTween
{
public class AsyncExceptionBoundaryTest
{
[Fact]
public async Task Wrap_SynchronousTest()
{
static Task AsyncFunc()
=> throw new OperationCanceledException();

// 例外を無視して終了する
await AsyncExceptionBoundary.Wrap(AsyncFunc);
}

[Fact]
public async Task Wrap_AsynchronousTest()
{
static async Task AsyncFunc()
{
await Task.Yield();
throw new OperationCanceledException();
}

// 例外を無視して終了する
await AsyncExceptionBoundary.Wrap(AsyncFunc);
}

[Fact]
public async Task Wrap_IgnoredTest()
{
static Task AsyncFunc()
=> throw new OperationCanceledException();

static bool IgnoreException(Exception ex)
=> ex is OperationCanceledException;

// 例外を無視して終了する
await AsyncExceptionBoundary.Wrap(AsyncFunc, IgnoreException);
}

[Fact]
public async Task Wrap_NotIgnoredTest()
{
static Task AsyncFunc()
=> throw new IOException();

static bool IgnoreException(Exception ex)
=> ex is OperationCanceledException;

// 例外を返して終了する
await Assert.ThrowsAsync<IOException>(
() => AsyncExceptionBoundary.Wrap(AsyncFunc, IgnoreException)
);
}

[Fact]
public async Task IgnoreException_Test()
{
var task = Task.FromException(new OperationCanceledException());

// 例外を無視して終了する
await AsyncExceptionBoundary.IgnoreException(task);
}

[Fact]
public async Task IgnoreExceptionAndDispose_Test()
{
var task = Task.FromException<int>(new OperationCanceledException());

// 例外を無視して終了する
await AsyncExceptionBoundary.IgnoreExceptionAndDispose(task);
}

[Fact]
public async Task IgnoreExceptionAndDispose_DisposeTest()
{
using var image = TestUtils.CreateDummyImage();
var task = Task.FromResult<object>(image); // IDisposable であることを静的に判定できない場合も想定

// 正常終了したとき Result が IDisposable だった場合は破棄する
await AsyncExceptionBoundary.IgnoreExceptionAndDispose(task);
Assert.True(image.IsDisposed);
}

[Fact]
public async Task IgnoreExceptionAndDispose_IEnumerableTest()
{
using var image1 = TestUtils.CreateDummyImage();
using var image2 = TestUtils.CreateDummyImage();
var tasks = new[]
{
Task.FromResult<object>(image1), // IDisposable であることを静的に判定できない場合も想定
Task.FromResult<object>(image2),
};

// 正常終了したとき Result が IDisposable だった場合は破棄する
await AsyncExceptionBoundary.IgnoreExceptionAndDispose(tasks);
Assert.True(image1.IsDisposed);
Assert.True(image2.IsDisposed);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"data": {}
}
14 changes: 9 additions & 5 deletions OpenTween/Api/GraphQL/TimelineResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@

#nullable enable

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenTween.Api.DataModel;

namespace OpenTween.Api.GraphQL
{
public record TimelineResponse(
TimelineTweet[] Tweets,
string? CursorTop,
string? CursorBottom
);
)
{
public TwitterStatus[] ToTwitterStatuses()
=> this.Tweets
.Where(x => !x.IsTombstone)
.Select(x => x.ToTwitterStatus())
.ToArray();
}
}
25 changes: 19 additions & 6 deletions OpenTween/Api/GraphQL/UserByScreenNameRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,33 @@ public async Task<TwitterGraphqlUser> Send(IApiConnection apiConnection)

ErrorResponse.ThrowIfError(rootElm);

var userElm = rootElm.XPathSelectElement("/data/user/result");
this.ThrowIfUserUnavailable(userElm);
try
{
var userElm = rootElm.XPathSelectElement("/data/user/result");
this.ThrowIfUserUnavailable(userElm);

return new(userElm);
return new(userElm);
}
catch (WebApiException ex)
{
ex.ResponseText = JsonUtils.JsonXmlToString(rootElm);
throw;
}
}

private void ThrowIfUserUnavailable(XElement userElm)
private void ThrowIfUserUnavailable(XElement? userElm)
{
if (userElm == null)
{
var errorText = "User is not available.";
throw new WebApiException(errorText);
}

var typeName = userElm.Element("__typename")?.Value;
if (typeName == "UserUnavailable")
{
var errorText = userElm.Element("message")?.Value ?? "User is not available.";
var json = JsonUtils.JsonXmlToString(userElm);
throw new WebApiException(errorText, json);
throw new WebApiException(errorText);
}
}
}
Expand Down
73 changes: 73 additions & 0 deletions OpenTween/AsyncExceptionBoundary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// OpenTween - Client of Twitter
// Copyright (c) 2024 kim_upsilon (@kim_upsilon) <https://upsilo.net/~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 <http://www.gnu.org/licenses/>, 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.Linq;
using System.Threading.Tasks;

namespace OpenTween
{
public static class AsyncExceptionBoundary
{
public static Task Wrap(Func<Task> func)
=> Wrap(func, _ => true);

public static async Task Wrap(Func<Task> func, Func<Exception, bool> ignoreException)
{
try
{
await func();
}
catch (Exception ex) when (ignoreException(ex))
{
}
}

public static async Task IgnoreException(Task task)
{
try
{
await task.ConfigureAwait(false);
}
catch
{
}
}

public static async Task IgnoreExceptionAndDispose<T>(Task<T> task)
{
try
{
var ret = await task.ConfigureAwait(false);
(ret as IDisposable)?.Dispose();
}
catch
{
}
}

public static Task IgnoreExceptionAndDispose<T>(IEnumerable<Task<T>> tasks)
=> Task.WhenAll(tasks.Select(x => IgnoreExceptionAndDispose(x)));
}
}
12 changes: 1 addition & 11 deletions OpenTween/Connection/TwitterApiConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,7 @@ public static async Task<T> HandleTimeout<T>(Func<CancellationToken, Task<T>> fu
// タイムアウト

// キャンセル後のタスクで発生した例外は無視する
static async Task IgnoreExceptions(Task task)
{
try
{
await task.ConfigureAwait(false);
}
catch
{
}
}
_ = IgnoreExceptions(task);
_ = AsyncExceptionBoundary.IgnoreExceptionAndDispose(task);
cts.Cancel();

throw new OperationCanceledException("Timeout", cancellactionToken);
Expand Down
Loading

0 comments on commit 20c402b

Please sign in to comment.