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 Dec 2, 2023
2 parents 0ef26fd + 3e4f260 commit d479e7f
Show file tree
Hide file tree
Showing 32 changed files with 281 additions and 84 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
更新履歴

==== Ver 3.9.0(2023/12/03)
* NEW: graphqlエンドポイントに対するレートリミットの表示に対応
* CHG: タイムライン更新時に全件ではなく新着投稿のみ差分を取得する動作に変更
* FIX: 設定したタイムアウト時間を超えてAPI接続が持続する場合がある不具合を修正
* FIX: プロフィール情報のURL欄のパースに失敗する場合がある不具合を修正
- この問題が起きるユーザーのツイートが含まれているとタイムラインの読み込みに失敗する問題も改善されます

==== Ver 3.8.0(2023/11/29)
* NEW: graphqlエンドポイントを使用した検索タイムラインの取得に対応
* NEW: graphqlエンドポイントを使用したプロフィール情報の取得に対応
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,15 @@ public async Task Send_Test()

var mock = new Mock<IApiConnection>();
mock.Setup(x =>
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
)
.Callback<Uri, IDictionary<string, string>>((url, param) =>
.Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
{
Assert.Equal(new("https://twitter.com/i/api/graphql/6ClPnsuzQJ1p7-g32GQw9Q/ListLatestTweetsTimeline"), url);
Assert.Equal(2, param.Count);
Assert.Equal("""{"listId":"1675863884757110790","count":20}""", param["variables"]);
Assert.True(param.ContainsKey("features"));
Assert.Equal("ListLatestTweetsTimeline", endpointName);
})
.ReturnsAsync(responseStream);

Expand All @@ -59,6 +60,7 @@ public async Task Send_Test()

var response = await request.Send(mock.Object).ConfigureAwait(false);
Assert.Single(response.Tweets);
Assert.Equal("DAABCgABF0HfRMjAJxEKAAIWes8rE1oQAAgAAwAAAAEAAA", response.CursorTop);
Assert.Equal("DAABCgABF0HfRMi__7QKAAIVAxUYmFWQAwgAAwAAAAIAAA", response.CursorBottom);

mock.VerifyAll();
Expand All @@ -71,14 +73,15 @@ public async Task Send_RequestCursor_Test()

var mock = new Mock<IApiConnection>();
mock.Setup(x =>
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
)
.Callback<Uri, IDictionary<string, string>>((url, param) =>
.Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
{
Assert.Equal(new("https://twitter.com/i/api/graphql/6ClPnsuzQJ1p7-g32GQw9Q/ListLatestTweetsTimeline"), url);
Assert.Equal(2, param.Count);
Assert.Equal("""{"listId":"1675863884757110790","count":20,"cursor":"aaa"}""", param["variables"]);
Assert.True(param.ContainsKey("features"));
Assert.Equal("ListLatestTweetsTimeline", endpointName);
})
.ReturnsAsync(responseStream);

Expand Down
11 changes: 7 additions & 4 deletions OpenTween.Tests/Api/GraphQL/SearchTimelineRequestTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@ public async Task Send_Test()

var mock = new Mock<IApiConnection>();
mock.Setup(x =>
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
)
.Callback<Uri, IDictionary<string, string>>((url, param) =>
.Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
{
Assert.Equal(new("https://twitter.com/i/api/graphql/lZ0GCEojmtQfiUQa5oJSEw/SearchTimeline"), url);
Assert.Equal(2, param.Count);
Assert.Equal("""{"rawQuery":"#OpenTween","count":20,"product":"Latest"}""", param["variables"]);
Assert.True(param.ContainsKey("features"));
Assert.Equal("SearchTimeline", endpointName);
})
.ReturnsAsync(responseStream);

Expand All @@ -58,6 +59,7 @@ public async Task Send_Test()

var response = await request.Send(mock.Object).ConfigureAwait(false);
Assert.Single(response.Tweets);
Assert.Equal("DAADDAABCgABFnlh4hraMAYKAAIOTm0DEhTAAQAIAAIAAAABCAADAAAAAAgABAAAAAAKAAUX8j3ezIAnEAoABhfyPd7Mf9jwAAA", response.CursorTop);
Assert.Equal("DAADDAABCgABFnlh4hraMAYKAAIOTm0DEhTAAQAIAAIAAAACCAADAAAAAAgABAAAAAAKAAUX8j3ezIAnEAoABhfyPd7Mf9jwAAA", response.CursorBottom);

mock.VerifyAll();
Expand All @@ -70,14 +72,15 @@ public async Task Send_RequestCursor_Test()

var mock = new Mock<IApiConnection>();
mock.Setup(x =>
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
)
.Callback<Uri, IDictionary<string, string>>((url, param) =>
.Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
{
Assert.Equal(new("https://twitter.com/i/api/graphql/lZ0GCEojmtQfiUQa5oJSEw/SearchTimeline"), url);
Assert.Equal(2, param.Count);
Assert.Equal("""{"rawQuery":"#OpenTween","count":20,"product":"Latest","cursor":"aaa"}""", param["variables"]);
Assert.True(param.ContainsKey("features"));
Assert.Equal("SearchTimeline", endpointName);
})
.ReturnsAsync(responseStream);

Expand Down
5 changes: 3 additions & 2 deletions OpenTween.Tests/Api/GraphQL/TweetDetailRequestTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ public async Task Send_Test()

var mock = new Mock<IApiConnection>();
mock.Setup(x =>
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
)
.Callback<Uri, IDictionary<string, string>>((url, param) =>
.Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
{
Assert.Equal(new("https://twitter.com/i/api/graphql/-Ls3CrSQNo2fRKH6i6Na1A/TweetDetail"), url);
Assert.Contains(@"""focalTweetId"":""1619433164757413894""", param["variables"]);
Assert.Equal("TweetDetail", endpointName);
})
.ReturnsAsync(responseStream);

Expand Down
15 changes: 15 additions & 0 deletions OpenTween.Tests/Api/GraphQL/TwitterGraphqlUserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,20 @@ public void ToTwitterUser_Test()
Assert.Equal("514241801", user.IdStr);
Assert.Equal("opentween", user.ScreenName);
}

[Fact]
public void ToTwitterUser_EntityWithoutDisplayUrlTest()
{
var userElm = this.LoadResponseDocument("User_EntityWithoutDisplayUrl.json");
var graphqlUser = new TwitterGraphqlUser(userElm);
var user = graphqlUser.ToTwitterUser();

Assert.Equal("4104111", user.IdStr);
var urlEntity = user.Entities?.Url?.Urls.First()!;
Assert.Equal("http://earthquake.transrain.net/", urlEntity.Url);
Assert.Equal(new[] { 0, 32 }, urlEntity.Indices);
Assert.Null(urlEntity.DisplayUrl);
Assert.Null(urlEntity.ExpandedUrl);
}
}
}
7 changes: 4 additions & 3 deletions OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@ public async Task Send_Test()

var mock = new Mock<IApiConnection>();
mock.Setup(x =>
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
)
.Callback<Uri, IDictionary<string, string>>((url, param) =>
.Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
{
Assert.Equal(new("https://twitter.com/i/api/graphql/xc8f1g7BYqr6VTzTbvNlGw/UserByScreenName"), url);
Assert.Contains(@"""screen_name"":""opentween""", param["variables"]);
Assert.Equal("UserByScreenName", endpointName);
})
.ReturnsAsync(responseStream);

Expand All @@ -67,7 +68,7 @@ public async Task Send_UserUnavailableTest()

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

Expand Down
11 changes: 7 additions & 4 deletions OpenTween.Tests/Api/GraphQL/UserTweetsAndRepliesRequestTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@ public async Task Send_Test()

var mock = new Mock<IApiConnection>();
mock.Setup(x =>
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
)
.Callback<Uri, IDictionary<string, string>>((url, param) =>
.Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
{
Assert.Equal(new("https://twitter.com/i/api/graphql/YlkSUg0mRBx7-EkxCvc-bw/UserTweetsAndReplies"), url);
Assert.Equal(2, param.Count);
Assert.Equal("""{"userId":"40480664","count":20,"includePromotedContent":true,"withCommunity":true,"withVoice":true,"withV2Timeline":true}""", param["variables"]);
Assert.True(param.ContainsKey("features"));
Assert.Equal("UserTweetsAndReplies", endpointName);
})
.ReturnsAsync(responseStream);

Expand All @@ -58,6 +59,7 @@ public async Task Send_Test()

var response = await request.Send(mock.Object).ConfigureAwait(false);
Assert.Single(response.Tweets);
Assert.Equal("DAABCgABF_tTnZvAJxEKAAIWes8rE1oQAAgAAwAAAAEAAA", response.CursorTop);
Assert.Equal("DAABCgABF_tTnZu__-0KAAIWZa6KTRoAAwgAAwAAAAIAAA", response.CursorBottom);

mock.VerifyAll();
Expand All @@ -70,14 +72,15 @@ public async Task Send_RequestCursor_Test()

var mock = new Mock<IApiConnection>();
mock.Setup(x =>
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
)
.Callback<Uri, IDictionary<string, string>>((url, param) =>
.Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
{
Assert.Equal(new("https://twitter.com/i/api/graphql/YlkSUg0mRBx7-EkxCvc-bw/UserTweetsAndReplies"), url);
Assert.Equal(2, param.Count);
Assert.Equal("""{"userId":"40480664","count":20,"includePromotedContent":true,"withCommunity":true,"withVoice":true,"withV2Timeline":true,"cursor":"aaa"}""", param["variables"]);
Assert.True(param.ContainsKey("features"));
Assert.Equal("UserTweetsAndReplies", endpointName);
})
.ReturnsAsync(responseStream);

Expand Down
3 changes: 3 additions & 0 deletions OpenTween.Tests/OpenTween.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@
<None Update="Resources\Responses\UserByScreenName_Suspended.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\Responses\User_EntityWithoutDisplayUrl.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\Responses\User_Simple.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
125 changes: 125 additions & 0 deletions OpenTween.Tests/Resources/Responses/User_EntityWithoutDisplayUrl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
{
"__typename": "User",
"id": "VXNlcjo0MTA0MTEx",
"rest_id": "4104111",
"affiliates_highlighted_label": {
"label": {
"badge": {
"url": "https://pbs.twimg.com/semantic_core_img/1428827730364096519/4ZXpTBhS?format=png&name=orig"
},
"description": "Automated",
"longDescription": {
"text": "Automated by @ariela",
"entities": [
{
"fromIndex": 13,
"toIndex": 20,
"ref": {
"type": "TimelineRichTextMention",
"screen_name": "ariela",
"mention_results": {
"result": {
"__typename": "User",
"legacy": {
"screen_name": "ariela"
},
"rest_id": "3486871"
}
}
}
}
]
},
"userLabelType": "AutomatedLabel"
}
},
"has_graduated_access": true,
"is_blue_verified": true,
"profile_image_shape": "Circle",
"legacy": {
"can_dm": false,
"can_media_tag": false,
"created_at": "Wed Apr 11 01:33:52 +0000 2007",
"default_profile": false,
"default_profile_image": false,
"description": "警戒:震度1以上 もしくは M3以上の地震情報を提供しています。 基本的に返事は行いません。問い合わせは@ariela もしくはyuki at https://t.co/DrMBNu9mAfにどうぞ。 非公式RTを繰り返すBOTはブロックします。",
"entities": {
"description": {
"urls": [
{
"display_url": "transrain.net",
"expanded_url": "http://transrain.net",
"url": "https://t.co/DrMBNu9mAf",
"indices": [
72,
95
]
}
]
},
"url": {
"urls": [
{
"url": "http://earthquake.transrain.net/",
"indices": [
0,
32
]
}
]
}
},
"fast_followers_count": 0,
"favourites_count": 1,
"followers_count": 3219441,
"friends_count": 5,
"has_custom_timelines": false,
"is_translator": false,
"listed_count": 44208,
"location": "",
"media_count": 0,
"name": "地震速報",
"normal_followers_count": 3219441,
"pinned_tweet_ids_str": [
"1623494931666046977"
],
"possibly_sensitive": false,
"profile_image_url_https": "https://pbs.twimg.com/profile_images/368358807/eqjp_normal.png",
"profile_interstitial_type": "",
"screen_name": "earthquake_jp",
"statuses_count": 59090,
"translator_type": "none",
"url": "http://earthquake.transrain.net/",
"verified": false,
"want_retweets": false,
"withheld_in_countries": []
},
"smart_blocked_by": false,
"smart_blocking": false,
"legacy_extended_profile": {},
"is_profile_translatable": true,
"verification_info": {
"reason": {
"description": {
"text": "This account is verified. Learn more",
"entities": [
{
"from_index": 26,
"to_index": 36,
"ref": {
"url": "https://help.twitter.com/managing-your-account/about-twitter-verified-accounts",
"url_type": "ExternalUrl"
}
}
]
},
"verified_since_msec": "1682244679134"
}
},
"highlights_info": {
"can_highlight_tweets": true,
"highlighted_tweets": "0"
},
"business_account": {},
"creator_subscriptions_count": 0
}
8 changes: 4 additions & 4 deletions OpenTween/Api/DataModel/TwitterEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,11 @@ public class TwitterEntitySymbol : TwitterEntity
[DataContract]
public class TwitterEntityUrl : TwitterEntity
{
[DataMember(Name = "display_url")]
public string DisplayUrl { get; set; }
[DataMember(Name = "display_url", IsRequired = false)]
public string? DisplayUrl { get; set; }

[DataMember(Name = "expanded_url")]
public string ExpandedUrl { get; set; }
[DataMember(Name = "expanded_url", IsRequired = false)]
public string? ExpandedUrl { get; set; }

[DataMember(Name = "url")]
public string Url { get; set; }
Expand Down
7 changes: 5 additions & 2 deletions OpenTween/Api/GraphQL/ListLatestTweetsTimelineRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ namespace OpenTween.Api.GraphQL
{
public class ListLatestTweetsTimelineRequest
{
public static readonly string EndpointName = "ListLatestTweetsTimeline";

private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/6ClPnsuzQJ1p7-g32GQw9Q/ListLatestTweetsTimeline");

public string ListId { get; set; }
Expand Down Expand Up @@ -89,7 +91,7 @@ public async Task<TimelineResponse> Send(IApiConnection apiConnection)
XElement rootElm;
try
{
using var stream = await apiConnection.GetStreamAsync(EndpointUri, param);
using var stream = await apiConnection.GetStreamAsync(EndpointUri, param, EndpointName);
using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(stream, XmlDictionaryReaderQuotas.Max);
rootElm = XElement.Load(jsonReader);
}
Expand All @@ -106,9 +108,10 @@ public async Task<TimelineResponse> Send(IApiConnection apiConnection)
ErrorResponse.ThrowIfError(rootElm);

var tweets = TimelineTweet.ExtractTimelineTweets(rootElm);
var cursorTop = rootElm.XPathSelectElement("//content[__typename[text()='TimelineTimelineCursor']][cursorType[text()='Top']]/value")?.Value;
var cursorBottom = rootElm.XPathSelectElement("//content[__typename[text()='TimelineTimelineCursor']][cursorType[text()='Bottom']]/value")?.Value;

return new(tweets, cursorBottom);
return new(tweets, cursorTop, cursorBottom);
}
}
}
Loading

0 comments on commit d479e7f

Please sign in to comment.