Skip to content

Commit

Permalink
Merge pull request #254 from opentween/handle-suspended-error
Browse files Browse the repository at this point in the history
凍結されたユーザーのプロフィール情報取得時のエラー表示に対応
  • Loading branch information
upsilon authored Nov 26, 2023
2 parents 4097f14 + 1842e5b commit 60d2716
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 3 deletions.
13 changes: 13 additions & 0 deletions OpenTween.Tests/Api/GraphQL/TimelineTweetTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,18 @@ public void ToStatus_WithTwitterPostFactory_PromotedTweet_Test()
Assert.True(post.IsPromoted);
Assert.Matches(new Regex(@"^\[Promoted\]\n"), post.TextFromApi);
}

[Fact]
public void ToStatus_TweetTombstone_Test()
{
var rootElm = this.LoadResponseDocument("TimelineTweet_TweetTombstone.json");
var timelineTweet = new TimelineTweet(rootElm);

Assert.True(timelineTweet.IsTombstone);
var ex = Assert.Throws<WebApiException>(
() => timelineTweet.ToTwitterStatus()
);
Assert.Equal("This Post is from a suspended account. Learn more", ex.Message);
}
}
}
24 changes: 24 additions & 0 deletions OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,29 @@ public async Task Send_Test()

mock.VerifyAll();
}

[Fact]
public async Task Send_UserUnavailableTest()
{
using var responseStream = File.OpenRead("Resources/Responses/UserByScreenName_Suspended.json");

var mock = new Mock<IApiConnection>();
mock.Setup(x =>
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
)
.ReturnsAsync(responseStream);

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

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

mock.VerifyAll();
}
}
}
6 changes: 6 additions & 0 deletions OpenTween.Tests/OpenTween.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
<None Update="Resources\Responses\TimelineTweet_RetweetedTweet.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\Responses\TimelineTweet_TweetTombstone.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\Responses\TimelineTweet_TweetWithVisibility.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand All @@ -94,6 +97,9 @@
<None Update="Resources\Responses\TimelineTweet_SelfThread.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\Responses\UserByScreenName_Suspended.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\Responses\User_Simple.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"itemType": "TimelineTweet",
"__typename": "TimelineTweet",
"tweet_results": {
"result": {
"__typename": "TweetTombstone",
"tombstone": {
"__typename": "TextTombstone",
"text": {
"rtl": false,
"text": "This Post is from a suspended account. Learn more",
"entities": [
{
"fromIndex": 39,
"toIndex": 49,
"ref": {
"type": "TimelineUrl",
"url": "https://help.twitter.com/rules-and-policies/notices-on-twitter",
"urlType": "ExternalUrl"
}
}
]
}
}
}
},
"tweetDisplayType": "Tweet",
"hasModeratedReplies": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"data": {
"user": {
"result": {
"__typename": "UserUnavailable",
"message": "User is suspended",
"reason": "Suspended"
}
}
}
}
23 changes: 23 additions & 0 deletions OpenTween/Api/GraphQL/TimelineTweet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,28 @@ public class TimelineTweet

public XElement Element { get; }

public bool IsTombstone
=> this.tombstoneElm != null;

private readonly XElement? tombstoneElm;

public TimelineTweet(XElement element)
{
var typeName = element.Element("itemType")?.Value;
if (typeName != TypeName)
throw new ArgumentException($"Invalid itemType: {typeName}", nameof(element));

this.Element = element;
this.tombstoneElm = this.TryGetTombstoneElm();
}

private XElement? TryGetTombstoneElm()
=> this.Element.XPathSelectElement("tweet_results/result[__typename[text()='TweetTombstone']]");

public TwitterStatus ToTwitterStatus()
{
this.ThrowIfTweetIsTombstone();

try
{
var resultElm = this.Element.Element("tweet_results")?.Element("result") ?? throw CreateParseError();
Expand All @@ -67,6 +78,18 @@ public TwitterStatus ToTwitterStatus()
}
}

public void ThrowIfTweetIsTombstone()
{
if (this.tombstoneElm == null)
return;

var tombstoneText = this.tombstoneElm.XPathSelectElement("tombstone/text/text")?.Value;
var message = tombstoneText ?? "Tweet is not available";
var json = JsonUtils.JsonXmlToString(this.Element);

throw new WebApiException(message, json);
}

public static TwitterStatus ParseTweetUnion(XElement tweetUnionElm)
{
var tweetElm = tweetUnionElm.Element("__typename")?.Value switch
Expand Down
12 changes: 12 additions & 0 deletions OpenTween/Api/GraphQL/UserByScreenNameRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,20 @@ public async Task<TwitterGraphqlUser> Send(IApiConnection apiConnection)
ErrorResponse.ThrowIfError(rootElm);

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

return new(userElm);
}

private void ThrowIfUserUnavailable(XElement userElm)
{
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);
}
}
}
}
17 changes: 14 additions & 3 deletions OpenTween/Twitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,11 @@ public async Task GetUserTimelineApi(bool read, UserTimelineTabModel tab, bool m
var response = await request.Send(this.Api.Connection)
.ConfigureAwait(false);

statuses = response.Tweets.Select(x => x.ToTwitterStatus()).ToArray();
statuses = response.Tweets
.Where(x => !x.IsTombstone)
.Select(x => x.ToTwitterStatus())
.ToArray();

tab.CursorBottom = response.CursorBottom;
}
else
Expand Down Expand Up @@ -881,7 +885,10 @@ public async Task GetListStatus(bool read, ListTimelineTabModel tab, bool more,
var response = await request.Send(this.Api.Connection)
.ConfigureAwait(false);

var convertedStatuses = response.Tweets.Select(x => x.ToTwitterStatus());
var convertedStatuses = response.Tweets
.Where(x => !x.IsTombstone)
.Select(x => x.ToTwitterStatus());

if (!SettingManager.Instance.Common.IsListsIncludeRts)
convertedStatuses = convertedStatuses.Where(x => x.RetweetedStatus == null);

Expand Down Expand Up @@ -1085,7 +1092,11 @@ public async Task GetSearch(bool read, PublicSearchTabModel tab, bool more)
var response = await request.Send(this.Api.Connection)
.ConfigureAwait(false);

statuses = response.Tweets.Select(x => x.ToTwitterStatus()).ToArray();
statuses = response.Tweets
.Where(x => !x.IsTombstone)
.Select(x => x.ToTwitterStatus())
.ToArray();

tab.CursorBottom = response.CursorBottom;
}
else
Expand Down

0 comments on commit 60d2716

Please sign in to comment.