• R/O
  • HTTP
  • SSH
  • HTTPS

open-tween: 提交

開発に使用するリポジトリ


Commit MetaInfo

修订版d479e7f2401495cb197ec6699c9eb262ccc7c26a (tree)
时间2023-12-02 17:14:55
作者Kimura Youichi <kim.upsilon@bucy...>
CommiterKimura Youichi

Log Message

Merge branch 'develop' into release

更改概述

差异

--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,5 +1,12 @@
11 更新履歴
22
3+==== Ver 3.9.0(2023/12/03)
4+ * NEW: graphqlエンドポイントに対するレートリミットの表示に対応
5+ * CHG: タイムライン更新時に全件ではなく新着投稿のみ差分を取得する動作に変更
6+ * FIX: 設定したタイムアウト時間を超えてAPI接続が持続する場合がある不具合を修正
7+ * FIX: プロフィール情報のURL欄のパースに失敗する場合がある不具合を修正
8+ - この問題が起きるユーザーのツイートが含まれているとタイムラインの読み込みに失敗する問題も改善されます
9+
310 ==== Ver 3.8.0(2023/11/29)
411 * NEW: graphqlエンドポイントを使用した検索タイムラインの取得に対応
512 * NEW: graphqlエンドポイントを使用したプロフィール情報の取得に対応
--- a/OpenTween.Tests/Api/GraphQL/ListLatestTweetsTimelineRequestTest.cs
+++ b/OpenTween.Tests/Api/GraphQL/ListLatestTweetsTimelineRequestTest.cs
@@ -41,14 +41,15 @@ namespace OpenTween.Api.GraphQL
4141
4242 var mock = new Mock<IApiConnection>();
4343 mock.Setup(x =>
44- x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
44+ x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
4545 )
46- .Callback<Uri, IDictionary<string, string>>((url, param) =>
46+ .Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
4747 {
4848 Assert.Equal(new("https://twitter.com/i/api/graphql/6ClPnsuzQJ1p7-g32GQw9Q/ListLatestTweetsTimeline"), url);
4949 Assert.Equal(2, param.Count);
5050 Assert.Equal("""{"listId":"1675863884757110790","count":20}""", param["variables"]);
5151 Assert.True(param.ContainsKey("features"));
52+ Assert.Equal("ListLatestTweetsTimeline", endpointName);
5253 })
5354 .ReturnsAsync(responseStream);
5455
@@ -59,6 +60,7 @@ namespace OpenTween.Api.GraphQL
5960
6061 var response = await request.Send(mock.Object).ConfigureAwait(false);
6162 Assert.Single(response.Tweets);
63+ Assert.Equal("DAABCgABF0HfRMjAJxEKAAIWes8rE1oQAAgAAwAAAAEAAA", response.CursorTop);
6264 Assert.Equal("DAABCgABF0HfRMi__7QKAAIVAxUYmFWQAwgAAwAAAAIAAA", response.CursorBottom);
6365
6466 mock.VerifyAll();
@@ -71,14 +73,15 @@ namespace OpenTween.Api.GraphQL
7173
7274 var mock = new Mock<IApiConnection>();
7375 mock.Setup(x =>
74- x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
76+ x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
7577 )
76- .Callback<Uri, IDictionary<string, string>>((url, param) =>
78+ .Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
7779 {
7880 Assert.Equal(new("https://twitter.com/i/api/graphql/6ClPnsuzQJ1p7-g32GQw9Q/ListLatestTweetsTimeline"), url);
7981 Assert.Equal(2, param.Count);
8082 Assert.Equal("""{"listId":"1675863884757110790","count":20,"cursor":"aaa"}""", param["variables"]);
8183 Assert.True(param.ContainsKey("features"));
84+ Assert.Equal("ListLatestTweetsTimeline", endpointName);
8285 })
8386 .ReturnsAsync(responseStream);
8487
--- a/OpenTween.Tests/Api/GraphQL/SearchTimelineRequestTest.cs
+++ b/OpenTween.Tests/Api/GraphQL/SearchTimelineRequestTest.cs
@@ -40,14 +40,15 @@ namespace OpenTween.Api.GraphQL
4040
4141 var mock = new Mock<IApiConnection>();
4242 mock.Setup(x =>
43- x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
43+ x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
4444 )
45- .Callback<Uri, IDictionary<string, string>>((url, param) =>
45+ .Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
4646 {
4747 Assert.Equal(new("https://twitter.com/i/api/graphql/lZ0GCEojmtQfiUQa5oJSEw/SearchTimeline"), url);
4848 Assert.Equal(2, param.Count);
4949 Assert.Equal("""{"rawQuery":"#OpenTween","count":20,"product":"Latest"}""", param["variables"]);
5050 Assert.True(param.ContainsKey("features"));
51+ Assert.Equal("SearchTimeline", endpointName);
5152 })
5253 .ReturnsAsync(responseStream);
5354
@@ -58,6 +59,7 @@ namespace OpenTween.Api.GraphQL
5859
5960 var response = await request.Send(mock.Object).ConfigureAwait(false);
6061 Assert.Single(response.Tweets);
62+ Assert.Equal("DAADDAABCgABFnlh4hraMAYKAAIOTm0DEhTAAQAIAAIAAAABCAADAAAAAAgABAAAAAAKAAUX8j3ezIAnEAoABhfyPd7Mf9jwAAA", response.CursorTop);
6163 Assert.Equal("DAADDAABCgABFnlh4hraMAYKAAIOTm0DEhTAAQAIAAIAAAACCAADAAAAAAgABAAAAAAKAAUX8j3ezIAnEAoABhfyPd7Mf9jwAAA", response.CursorBottom);
6264
6365 mock.VerifyAll();
@@ -70,14 +72,15 @@ namespace OpenTween.Api.GraphQL
7072
7173 var mock = new Mock<IApiConnection>();
7274 mock.Setup(x =>
73- x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
75+ x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
7476 )
75- .Callback<Uri, IDictionary<string, string>>((url, param) =>
77+ .Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
7678 {
7779 Assert.Equal(new("https://twitter.com/i/api/graphql/lZ0GCEojmtQfiUQa5oJSEw/SearchTimeline"), url);
7880 Assert.Equal(2, param.Count);
7981 Assert.Equal("""{"rawQuery":"#OpenTween","count":20,"product":"Latest","cursor":"aaa"}""", param["variables"]);
8082 Assert.True(param.ContainsKey("features"));
83+ Assert.Equal("SearchTimeline", endpointName);
8184 })
8285 .ReturnsAsync(responseStream);
8386
--- a/OpenTween.Tests/Api/GraphQL/TweetDetailRequestTest.cs
+++ b/OpenTween.Tests/Api/GraphQL/TweetDetailRequestTest.cs
@@ -41,12 +41,13 @@ namespace OpenTween.Api.GraphQL
4141
4242 var mock = new Mock<IApiConnection>();
4343 mock.Setup(x =>
44- x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
44+ x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
4545 )
46- .Callback<Uri, IDictionary<string, string>>((url, param) =>
46+ .Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
4747 {
4848 Assert.Equal(new("https://twitter.com/i/api/graphql/-Ls3CrSQNo2fRKH6i6Na1A/TweetDetail"), url);
4949 Assert.Contains(@"""focalTweetId"":""1619433164757413894""", param["variables"]);
50+ Assert.Equal("TweetDetail", endpointName);
5051 })
5152 .ReturnsAsync(responseStream);
5253
--- a/OpenTween.Tests/Api/GraphQL/TwitterGraphqlUserTest.cs
+++ b/OpenTween.Tests/Api/GraphQL/TwitterGraphqlUserTest.cs
@@ -51,5 +51,20 @@ namespace OpenTween.Api.GraphQL
5151 Assert.Equal("514241801", user.IdStr);
5252 Assert.Equal("opentween", user.ScreenName);
5353 }
54+
55+ [Fact]
56+ public void ToTwitterUser_EntityWithoutDisplayUrlTest()
57+ {
58+ var userElm = this.LoadResponseDocument("User_EntityWithoutDisplayUrl.json");
59+ var graphqlUser = new TwitterGraphqlUser(userElm);
60+ var user = graphqlUser.ToTwitterUser();
61+
62+ Assert.Equal("4104111", user.IdStr);
63+ var urlEntity = user.Entities?.Url?.Urls.First()!;
64+ Assert.Equal("http://earthquake.transrain.net/", urlEntity.Url);
65+ Assert.Equal(new[] { 0, 32 }, urlEntity.Indices);
66+ Assert.Null(urlEntity.DisplayUrl);
67+ Assert.Null(urlEntity.ExpandedUrl);
68+ }
5469 }
5570 }
--- a/OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs
+++ b/OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs
@@ -40,12 +40,13 @@ namespace OpenTween.Api.GraphQL
4040
4141 var mock = new Mock<IApiConnection>();
4242 mock.Setup(x =>
43- x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
43+ x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
4444 )
45- .Callback<Uri, IDictionary<string, string>>((url, param) =>
45+ .Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
4646 {
4747 Assert.Equal(new("https://twitter.com/i/api/graphql/xc8f1g7BYqr6VTzTbvNlGw/UserByScreenName"), url);
4848 Assert.Contains(@"""screen_name"":""opentween""", param["variables"]);
49+ Assert.Equal("UserByScreenName", endpointName);
4950 })
5051 .ReturnsAsync(responseStream);
5152
@@ -67,7 +68,7 @@ namespace OpenTween.Api.GraphQL
6768
6869 var mock = new Mock<IApiConnection>();
6970 mock.Setup(x =>
70- x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
71+ x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
7172 )
7273 .ReturnsAsync(responseStream);
7374
--- a/OpenTween.Tests/Api/GraphQL/UserTweetsAndRepliesRequestTest.cs
+++ b/OpenTween.Tests/Api/GraphQL/UserTweetsAndRepliesRequestTest.cs
@@ -40,14 +40,15 @@ namespace OpenTween.Api.GraphQL
4040
4141 var mock = new Mock<IApiConnection>();
4242 mock.Setup(x =>
43- x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
43+ x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
4444 )
45- .Callback<Uri, IDictionary<string, string>>((url, param) =>
45+ .Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
4646 {
4747 Assert.Equal(new("https://twitter.com/i/api/graphql/YlkSUg0mRBx7-EkxCvc-bw/UserTweetsAndReplies"), url);
4848 Assert.Equal(2, param.Count);
4949 Assert.Equal("""{"userId":"40480664","count":20,"includePromotedContent":true,"withCommunity":true,"withVoice":true,"withV2Timeline":true}""", param["variables"]);
5050 Assert.True(param.ContainsKey("features"));
51+ Assert.Equal("UserTweetsAndReplies", endpointName);
5152 })
5253 .ReturnsAsync(responseStream);
5354
@@ -58,6 +59,7 @@ namespace OpenTween.Api.GraphQL
5859
5960 var response = await request.Send(mock.Object).ConfigureAwait(false);
6061 Assert.Single(response.Tweets);
62+ Assert.Equal("DAABCgABF_tTnZvAJxEKAAIWes8rE1oQAAgAAwAAAAEAAA", response.CursorTop);
6163 Assert.Equal("DAABCgABF_tTnZu__-0KAAIWZa6KTRoAAwgAAwAAAAIAAA", response.CursorBottom);
6264
6365 mock.VerifyAll();
@@ -70,14 +72,15 @@ namespace OpenTween.Api.GraphQL
7072
7173 var mock = new Mock<IApiConnection>();
7274 mock.Setup(x =>
73- x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
75+ x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<string>())
7476 )
75- .Callback<Uri, IDictionary<string, string>>((url, param) =>
77+ .Callback<Uri, IDictionary<string, string>, string>((url, param, endpointName) =>
7678 {
7779 Assert.Equal(new("https://twitter.com/i/api/graphql/YlkSUg0mRBx7-EkxCvc-bw/UserTweetsAndReplies"), url);
7880 Assert.Equal(2, param.Count);
7981 Assert.Equal("""{"userId":"40480664","count":20,"includePromotedContent":true,"withCommunity":true,"withVoice":true,"withV2Timeline":true,"cursor":"aaa"}""", param["variables"]);
8082 Assert.True(param.ContainsKey("features"));
83+ Assert.Equal("UserTweetsAndReplies", endpointName);
8184 })
8285 .ReturnsAsync(responseStream);
8386
--- a/OpenTween.Tests/OpenTween.Tests.csproj
+++ b/OpenTween.Tests/OpenTween.Tests.csproj
@@ -100,6 +100,9 @@
100100 <None Update="Resources\Responses\UserByScreenName_Suspended.json">
101101 <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
102102 </None>
103+ <None Update="Resources\Responses\User_EntityWithoutDisplayUrl.json">
104+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
105+ </None>
103106 <None Update="Resources\Responses\User_Simple.json">
104107 <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
105108 </None>
--- /dev/null
+++ b/OpenTween.Tests/Resources/Responses/User_EntityWithoutDisplayUrl.json
@@ -0,0 +1,125 @@
1+{
2+ "__typename": "User",
3+ "id": "VXNlcjo0MTA0MTEx",
4+ "rest_id": "4104111",
5+ "affiliates_highlighted_label": {
6+ "label": {
7+ "badge": {
8+ "url": "https://pbs.twimg.com/semantic_core_img/1428827730364096519/4ZXpTBhS?format=png&name=orig"
9+ },
10+ "description": "Automated",
11+ "longDescription": {
12+ "text": "Automated by @ariela",
13+ "entities": [
14+ {
15+ "fromIndex": 13,
16+ "toIndex": 20,
17+ "ref": {
18+ "type": "TimelineRichTextMention",
19+ "screen_name": "ariela",
20+ "mention_results": {
21+ "result": {
22+ "__typename": "User",
23+ "legacy": {
24+ "screen_name": "ariela"
25+ },
26+ "rest_id": "3486871"
27+ }
28+ }
29+ }
30+ }
31+ ]
32+ },
33+ "userLabelType": "AutomatedLabel"
34+ }
35+ },
36+ "has_graduated_access": true,
37+ "is_blue_verified": true,
38+ "profile_image_shape": "Circle",
39+ "legacy": {
40+ "can_dm": false,
41+ "can_media_tag": false,
42+ "created_at": "Wed Apr 11 01:33:52 +0000 2007",
43+ "default_profile": false,
44+ "default_profile_image": false,
45+ "description": "警戒:震度1以上 もしくは M3以上の地震情報を提供しています。 基本的に返事は行いません。問い合わせは@ariela もしくはyuki at https://t.co/DrMBNu9mAfにどうぞ。 非公式RTを繰り返すBOTはブロックします。",
46+ "entities": {
47+ "description": {
48+ "urls": [
49+ {
50+ "display_url": "transrain.net",
51+ "expanded_url": "http://transrain.net",
52+ "url": "https://t.co/DrMBNu9mAf",
53+ "indices": [
54+ 72,
55+ 95
56+ ]
57+ }
58+ ]
59+ },
60+ "url": {
61+ "urls": [
62+ {
63+ "url": "http://earthquake.transrain.net/",
64+ "indices": [
65+ 0,
66+ 32
67+ ]
68+ }
69+ ]
70+ }
71+ },
72+ "fast_followers_count": 0,
73+ "favourites_count": 1,
74+ "followers_count": 3219441,
75+ "friends_count": 5,
76+ "has_custom_timelines": false,
77+ "is_translator": false,
78+ "listed_count": 44208,
79+ "location": "",
80+ "media_count": 0,
81+ "name": "地震速報",
82+ "normal_followers_count": 3219441,
83+ "pinned_tweet_ids_str": [
84+ "1623494931666046977"
85+ ],
86+ "possibly_sensitive": false,
87+ "profile_image_url_https": "https://pbs.twimg.com/profile_images/368358807/eqjp_normal.png",
88+ "profile_interstitial_type": "",
89+ "screen_name": "earthquake_jp",
90+ "statuses_count": 59090,
91+ "translator_type": "none",
92+ "url": "http://earthquake.transrain.net/",
93+ "verified": false,
94+ "want_retweets": false,
95+ "withheld_in_countries": []
96+ },
97+ "smart_blocked_by": false,
98+ "smart_blocking": false,
99+ "legacy_extended_profile": {},
100+ "is_profile_translatable": true,
101+ "verification_info": {
102+ "reason": {
103+ "description": {
104+ "text": "This account is verified. Learn more",
105+ "entities": [
106+ {
107+ "from_index": 26,
108+ "to_index": 36,
109+ "ref": {
110+ "url": "https://help.twitter.com/managing-your-account/about-twitter-verified-accounts",
111+ "url_type": "ExternalUrl"
112+ }
113+ }
114+ ]
115+ },
116+ "verified_since_msec": "1682244679134"
117+ }
118+ },
119+ "highlights_info": {
120+ "can_highlight_tweets": true,
121+ "highlighted_tweets": "0"
122+ },
123+ "business_account": {},
124+ "creator_subscriptions_count": 0
125+}
--- a/OpenTween/Api/DataModel/TwitterEntity.cs
+++ b/OpenTween/Api/DataModel/TwitterEntity.cs
@@ -162,11 +162,11 @@ namespace OpenTween.Api.DataModel
162162 [DataContract]
163163 public class TwitterEntityUrl : TwitterEntity
164164 {
165- [DataMember(Name = "display_url")]
166- public string DisplayUrl { get; set; }
165+ [DataMember(Name = "display_url", IsRequired = false)]
166+ public string? DisplayUrl { get; set; }
167167
168- [DataMember(Name = "expanded_url")]
169- public string ExpandedUrl { get; set; }
168+ [DataMember(Name = "expanded_url", IsRequired = false)]
169+ public string? ExpandedUrl { get; set; }
170170
171171 [DataMember(Name = "url")]
172172 public string Url { get; set; }
--- a/OpenTween/Api/GraphQL/ListLatestTweetsTimelineRequest.cs
+++ b/OpenTween/Api/GraphQL/ListLatestTweetsTimelineRequest.cs
@@ -37,6 +37,8 @@ namespace OpenTween.Api.GraphQL
3737 {
3838 public class ListLatestTweetsTimelineRequest
3939 {
40+ public static readonly string EndpointName = "ListLatestTweetsTimeline";
41+
4042 private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/6ClPnsuzQJ1p7-g32GQw9Q/ListLatestTweetsTimeline");
4143
4244 public string ListId { get; set; }
@@ -89,7 +91,7 @@ namespace OpenTween.Api.GraphQL
8991 XElement rootElm;
9092 try
9193 {
92- using var stream = await apiConnection.GetStreamAsync(EndpointUri, param);
94+ using var stream = await apiConnection.GetStreamAsync(EndpointUri, param, EndpointName);
9395 using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(stream, XmlDictionaryReaderQuotas.Max);
9496 rootElm = XElement.Load(jsonReader);
9597 }
@@ -106,9 +108,10 @@ namespace OpenTween.Api.GraphQL
106108 ErrorResponse.ThrowIfError(rootElm);
107109
108110 var tweets = TimelineTweet.ExtractTimelineTweets(rootElm);
111+ var cursorTop = rootElm.XPathSelectElement("//content[__typename[text()='TimelineTimelineCursor']][cursorType[text()='Top']]/value")?.Value;
109112 var cursorBottom = rootElm.XPathSelectElement("//content[__typename[text()='TimelineTimelineCursor']][cursorType[text()='Bottom']]/value")?.Value;
110113
111- return new(tweets, cursorBottom);
114+ return new(tweets, cursorTop, cursorBottom);
112115 }
113116 }
114117 }
--- a/OpenTween/Api/GraphQL/SearchTimelineRequest.cs
+++ b/OpenTween/Api/GraphQL/SearchTimelineRequest.cs
@@ -37,6 +37,8 @@ namespace OpenTween.Api.GraphQL
3737 {
3838 public class SearchTimelineRequest
3939 {
40+ public static readonly string EndpointName = "SearchTimeline";
41+
4042 private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/lZ0GCEojmtQfiUQa5oJSEw/SearchTimeline");
4143
4244 public string RawQuery { get; set; }
@@ -91,7 +93,7 @@ namespace OpenTween.Api.GraphQL
9193 XElement rootElm;
9294 try
9395 {
94- using var stream = await apiConnection.GetStreamAsync(EndpointUri, param);
96+ using var stream = await apiConnection.GetStreamAsync(EndpointUri, param, EndpointName);
9597 using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(stream, XmlDictionaryReaderQuotas.Max);
9698 rootElm = XElement.Load(jsonReader);
9799 }
@@ -108,9 +110,10 @@ namespace OpenTween.Api.GraphQL
108110 ErrorResponse.ThrowIfError(rootElm);
109111
110112 var tweets = TimelineTweet.ExtractTimelineTweets(rootElm);
113+ var cursorTop = rootElm.XPathSelectElement("//content[__typename[text()='TimelineTimelineCursor']][cursorType[text()='Top']]/value")?.Value;
111114 var cursorBottom = rootElm.XPathSelectElement("//content[__typename[text()='TimelineTimelineCursor']][cursorType[text()='Bottom']]/value")?.Value;
112115
113- return new(tweets, cursorBottom);
116+ return new(tweets, cursorTop, cursorBottom);
114117 }
115118 }
116119 }
--- a/OpenTween/Api/GraphQL/TimelineResponse.cs
+++ b/OpenTween/Api/GraphQL/TimelineResponse.cs
@@ -31,6 +31,7 @@ namespace OpenTween.Api.GraphQL
3131 {
3232 public record TimelineResponse(
3333 TimelineTweet[] Tweets,
34+ string? CursorTop,
3435 string? CursorBottom
3536 );
3637 }
--- a/OpenTween/Api/GraphQL/TimelineTweet.cs
+++ b/OpenTween/Api/GraphQL/TimelineTweet.cs
@@ -138,8 +138,8 @@ namespace OpenTween.Api.GraphQL
138138 .Select(x => new TwitterEntityUrl()
139139 {
140140 Indices = x.XPathSelectElements("indices/item").Select(x => int.Parse(x.Value)).ToArray(),
141- DisplayUrl = GetText(x, "display_url"),
142- ExpandedUrl = GetText(x, "expanded_url"),
141+ DisplayUrl = GetTextOrNull(x, "display_url"),
142+ ExpandedUrl = GetTextOrNull(x, "expanded_url"),
143143 Url = GetText(x, "url"),
144144 })
145145 .ToArray(),
--- a/OpenTween/Api/GraphQL/TweetDetailRequest.cs
+++ b/OpenTween/Api/GraphQL/TweetDetailRequest.cs
@@ -38,6 +38,8 @@ namespace OpenTween.Api.GraphQL
3838 {
3939 public class TweetDetailRequest
4040 {
41+ public static readonly string EndpointName = "TweetDetail";
42+
4143 private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/-Ls3CrSQNo2fRKH6i6Na1A/TweetDetail");
4244
4345 required public TwitterStatusId FocalTweetId { get; set; }
@@ -65,7 +67,7 @@ namespace OpenTween.Api.GraphQL
6567 XElement rootElm;
6668 try
6769 {
68- using var stream = await apiConnection.GetStreamAsync(EndpointUri, param);
70+ using var stream = await apiConnection.GetStreamAsync(EndpointUri, param, EndpointName);
6971 using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(stream, XmlDictionaryReaderQuotas.Max);
7072 rootElm = XElement.Load(jsonReader);
7173 }
--- a/OpenTween/Api/GraphQL/TwitterGraphqlUser.cs
+++ b/OpenTween/Api/GraphQL/TwitterGraphqlUser.cs
@@ -96,8 +96,8 @@ namespace OpenTween.Api.GraphQL
9696 .Select(x => new TwitterEntityUrl()
9797 {
9898 Indices = x.XPathSelectElements("indices/item").Select(x => int.Parse(x.Value)).ToArray(),
99- DisplayUrl = GetText(x, "display_url"),
100- ExpandedUrl = GetText(x, "expanded_url"),
99+ DisplayUrl = GetTextOrNull(x, "display_url"),
100+ ExpandedUrl = GetTextOrNull(x, "expanded_url"),
101101 Url = GetText(x, "url"),
102102 })
103103 .ToArray(),
@@ -108,8 +108,8 @@ namespace OpenTween.Api.GraphQL
108108 .Select(x => new TwitterEntityUrl()
109109 {
110110 Indices = x.XPathSelectElements("indices/item").Select(x => int.Parse(x.Value)).ToArray(),
111- DisplayUrl = GetText(x, "display_url"),
112- ExpandedUrl = GetText(x, "expanded_url"),
111+ DisplayUrl = GetTextOrNull(x, "display_url"),
112+ ExpandedUrl = GetTextOrNull(x, "expanded_url"),
113113 Url = GetText(x, "url"),
114114 })
115115 .ToArray(),
--- a/OpenTween/Api/GraphQL/UserByScreenNameRequest.cs
+++ b/OpenTween/Api/GraphQL/UserByScreenNameRequest.cs
@@ -37,6 +37,8 @@ namespace OpenTween.Api.GraphQL
3737 {
3838 public class UserByScreenNameRequest
3939 {
40+ public static readonly string EndpointName = "UserByScreenName";
41+
4042 private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/xc8f1g7BYqr6VTzTbvNlGw/UserByScreenName");
4143
4244 required public string ScreenName { get; set; }
@@ -64,7 +66,7 @@ namespace OpenTween.Api.GraphQL
6466 XElement rootElm;
6567 try
6668 {
67- using var stream = await apiConnection.GetStreamAsync(EndpointUri, param);
69+ using var stream = await apiConnection.GetStreamAsync(EndpointUri, param, EndpointName);
6870 using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(stream, XmlDictionaryReaderQuotas.Max);
6971 rootElm = XElement.Load(jsonReader);
7072 }
--- a/OpenTween/Api/GraphQL/UserTweetsAndRepliesRequest.cs
+++ b/OpenTween/Api/GraphQL/UserTweetsAndRepliesRequest.cs
@@ -37,6 +37,8 @@ namespace OpenTween.Api.GraphQL
3737 {
3838 public class UserTweetsAndRepliesRequest
3939 {
40+ public static readonly string EndpointName = "UserTweetsAndReplies";
41+
4042 private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/YlkSUg0mRBx7-EkxCvc-bw/UserTweetsAndReplies");
4143
4244 public string UserId { get; set; }
@@ -74,7 +76,7 @@ namespace OpenTween.Api.GraphQL
7476 XElement rootElm;
7577 try
7678 {
77- using var stream = await apiConnection.GetStreamAsync(EndpointUri, param);
79+ using var stream = await apiConnection.GetStreamAsync(EndpointUri, param, EndpointName);
7880 using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(stream, XmlDictionaryReaderQuotas.Max);
7981 rootElm = XElement.Load(jsonReader);
8082 }
@@ -91,9 +93,10 @@ namespace OpenTween.Api.GraphQL
9193 ErrorResponse.ThrowIfError(rootElm);
9294
9395 var tweets = TimelineTweet.ExtractTimelineTweets(rootElm);
96+ var cursorTop = rootElm.XPathSelectElement("//content[__typename[text()='TimelineTimelineCursor']][cursorType[text()='Top']]/value")?.Value;
9497 var cursorBottom = rootElm.XPathSelectElement("//content[__typename[text()='TimelineTimelineCursor']][cursorType[text()='Bottom']]/value")?.Value;
9598
96- return new(tweets, cursorBottom);
99+ return new(tweets, cursorTop, cursorBottom);
97100 }
98101 }
99102 }
--- a/OpenTween/Connection/IApiConnection.cs
+++ b/OpenTween/Connection/IApiConnection.cs
@@ -36,6 +36,8 @@ namespace OpenTween.Connection
3636
3737 Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param);
3838
39+ Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param, string? endpointName);
40+
3941 Task<Stream> GetStreamingStreamAsync(Uri uri, IDictionary<string, string>? param);
4042
4143 Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string>? param);
--- a/OpenTween/Connection/Networking.cs
+++ b/OpenTween/Connection/Networking.cs
@@ -143,6 +143,7 @@ namespace OpenTween.Connection
143143 {
144144 UseCookies = false,
145145 AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
146+ ReadWriteTimeout = (int)DefaultTimeout.TotalMilliseconds,
146147 };
147148
148149 if (Networking.Proxy != null)
--- a/OpenTween/Connection/TwitterApiConnection.cs
+++ b/OpenTween/Connection/TwitterApiConnection.cs
@@ -167,8 +167,15 @@ namespace OpenTween.Connection
167167 }
168168 }
169169
170- public async Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param)
170+ public Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param)
171+ => this.GetStreamAsync(uri, param, null);
172+
173+ public async Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param, string? endpointName)
171174 {
175+ // レートリミット規制中はAPIリクエストを送信せずに直ちにエラーを発生させる
176+ if (endpointName != null)
177+ this.ThrowIfRateLimitExceeded(endpointName);
178+
172179 var requestUri = new Uri(RestApiBase, uri);
173180
174181 if (param != null)
@@ -176,7 +183,16 @@ namespace OpenTween.Connection
176183
177184 try
178185 {
179- return await this.Http.GetStreamAsync(requestUri)
186+ var response = await this.Http.GetAsync(requestUri)
187+ .ConfigureAwait(false);
188+
189+ if (endpointName != null)
190+ MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName);
191+
192+ await TwitterApiConnection.CheckStatusCode(response)
193+ .ConfigureAwait(false);
194+
195+ return await response.Content.ReadAsStreamAsync()
180196 .ConfigureAwait(false);
181197 }
182198 catch (HttpRequestException ex)
--- a/OpenTween/Models/ListTimelineTabModel.cs
+++ b/OpenTween/Models/ListTimelineTabModel.cs
@@ -45,6 +45,8 @@ namespace OpenTween.Models
4545
4646 public PostId? OldestId { get; set; }
4747
48+ public string? CursorTop { get; set; }
49+
4850 public string? CursorBottom { get; set; }
4951
5052 public ListTimelineTabModel(string tabName, ListElement list)
--- a/OpenTween/Models/PublicSearchTabModel.cs
+++ b/OpenTween/Models/PublicSearchTabModel.cs
@@ -45,6 +45,8 @@ namespace OpenTween.Models
4545
4646 public PostId? SinceId { get; set; }
4747
48+ public string? CursorTop { get; set; }
49+
4850 public string? CursorBottom { get; set; }
4951
5052 public string SearchWords
--- a/OpenTween/Models/TwitterPostFactory.cs
+++ b/OpenTween/Models/TwitterPostFactory.cs
@@ -114,7 +114,7 @@ namespace OpenTween.Models
114114 .ToArray();
115115
116116 var expandedUrls = entities.OfType<TwitterEntityUrl>()
117- .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
117+ .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl ?? x.Url))
118118 .ToArray();
119119
120120 // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる)
@@ -229,7 +229,7 @@ namespace OpenTween.Models
229229 .ToArray();
230230
231231 var expandedUrls = entities.OfType<TwitterEntityUrl>()
232- .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
232+ .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl ?? x.Url))
233233 .ToArray();
234234
235235 // 以下、ユーザー情報
@@ -512,7 +512,7 @@ namespace OpenTween.Models
512512 {
513513 entities ??= Enumerable.Empty<TwitterEntity>();
514514
515- var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
515+ var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl ?? x.Url);
516516
517517 if (quotedStatusLink != null)
518518 urls = urls.Append(quotedStatusLink.Expanded);
--- a/OpenTween/Models/UserTimelineTabModel.cs
+++ b/OpenTween/Models/UserTimelineTabModel.cs
@@ -47,6 +47,8 @@ namespace OpenTween.Models
4747
4848 public PostId? OldestId { get; set; }
4949
50+ public string? CursorTop { get; set; }
51+
5052 public string? CursorBottom { get; set; }
5153
5254 public UserTimelineTabModel(string tabName, string screenName)
--- a/OpenTween/Properties/AssemblyInfo.cs
+++ b/OpenTween/Properties/AssemblyInfo.cs
@@ -22,7 +22,7 @@ using System.Runtime.InteropServices;
2222 // 次の GUID は、このプロジェクトが COM に公開される場合の、typelib の ID です
2323 [assembly: Guid("2d0ae0ba-adac-49a2-9b10-26fd69e695bf")]
2424
25-[assembly: AssemblyVersion("3.8.0.0")]
25+[assembly: AssemblyVersion("3.9.0.0")]
2626
2727 [assembly: InternalsVisibleTo("OpenTween.Tests")]
2828 [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // for Moq
--- a/OpenTween/Properties/Resources.Designer.cs
+++ b/OpenTween/Properties/Resources.Designer.cs
@@ -580,19 +580,19 @@ namespace OpenTween.Properties {
580580 /// <summary>
581581 /// 更新履歴
582582 ///
583+ ///==== Ver 3.9.0(2023/12/03)
584+ /// * NEW: graphqlエンドポイントに対するレートリミットの表示に対応
585+ /// * CHG: タイムライン更新時に全件ではなく新着投稿のみ差分を取得する動作に変更
586+ /// * FIX: 設定したタイムアウト時間を超えてAPI接続が持続する場合がある不具合を修正
587+ /// * FIX: プロフィール情報のURL欄のパースに失敗する場合がある不具合を修正
588+ /// - この問題が起きるユーザーのツイートが含まれているとタイムラインの読み込みに失敗する問題も改善されます
589+ ///
583590 ///==== Ver 3.8.0(2023/11/29)
584591 /// * NEW: graphqlエンドポイントを使用した検索タイムラインの取得に対応
585592 /// * NEW: graphqlエンドポイントを使用したプロフィール情報の取得に対応
586593 /// * NEW: graphqlエンドポイントを使用したユーザータイムラインの取得に対応
587594 /// * CHG: タイムライン更新が停止する不具合が報告される件への暫定的な対処
588- /// - タイムライン更新に30秒以上掛かっている場合は完了を待機せず次のタイマーを開始させる
589- /// - タイムライン更新の次回実行が1時間以上先になる場合は異常値としてタイマーをリセットする
590- /// * FIX: 動画のサムネイル表示時に再生可能であることを示すアイコンが表示されない不具合を修正
591- /// * FIX: リスト更新時に発生したネットワークエラーが適切に処理されない不具合を修正
592- /// * FIX: 起動直後にタイムラインの取得が重複して行われる不具合を修正
593- ///
594- ///==== Ver 3.7.1(2023/07/20)
595- /// * FIX: Cookie使用時に複数回ツイートを投稿するとDelaying [残りの文字列は切り詰められました]&quot;; に類似しているローカライズされた文字列を検索します。
595+ /// - タイムライン更新に30秒以上掛かっている場合は完了を待機せず次のタイマーを [残りの文字列は切り詰められました]&quot;; に類似しているローカライズされた文字列を検索します。
596596 /// </summary>
597597 internal static string ChangeLog {
598598 get {
--- a/OpenTween/Tween.cs
+++ b/OpenTween/Tween.cs
@@ -52,6 +52,7 @@ using System.Threading.Tasks;
5252 using System.Windows.Forms;
5353 using OpenTween.Api;
5454 using OpenTween.Api.DataModel;
55+using OpenTween.Api.GraphQL;
5556 using OpenTween.Api.TwitterV2;
5657 using OpenTween.Connection;
5758 using OpenTween.MediaUploadServices;
@@ -7083,17 +7084,22 @@ namespace OpenTween
70837084
70847085 if (endpointName == null)
70857086 {
7087+ var authByCookie = this.tw.Api.AppToken.AuthType == APIAuthType.TwitterComCookie;
7088+
70867089 // 表示中のタブに応じて更新
70877090 endpointName = tabType switch
70887091 {
7089- MyCommon.TabUsageType.Home => GetTimelineRequest.EndpointName,
7092+ MyCommon.TabUsageType.Home => "/statuses/home_timeline",
70907093 MyCommon.TabUsageType.UserDefined => "/statuses/home_timeline",
70917094 MyCommon.TabUsageType.Mentions => "/statuses/mentions_timeline",
70927095 MyCommon.TabUsageType.Favorites => "/favorites/list",
70937096 MyCommon.TabUsageType.DirectMessage => "/direct_messages/events/list",
7094- MyCommon.TabUsageType.UserTimeline => "/statuses/user_timeline",
7095- MyCommon.TabUsageType.Lists => "/lists/statuses",
7096- MyCommon.TabUsageType.PublicSearch => "/search/tweets",
7097+ MyCommon.TabUsageType.UserTimeline =>
7098+ authByCookie ? UserTweetsAndRepliesRequest.EndpointName : "/statuses/user_timeline",
7099+ MyCommon.TabUsageType.Lists =>
7100+ authByCookie ? ListLatestTweetsTimelineRequest.EndpointName : "/lists/statuses",
7101+ MyCommon.TabUsageType.PublicSearch =>
7102+ authByCookie ? SearchTimelineRequest.EndpointName : "/search/tweets",
70977103 MyCommon.TabUsageType.Related => "/statuses/show/:id",
70987104 _ => null,
70997105 };
@@ -7101,31 +7107,8 @@ namespace OpenTween
71017107 }
71027108 else
71037109 {
7104- // 表示中のタブに関連する endpoint であれば更新
7105- bool update;
7106- if (endpointName == GetTimelineRequest.EndpointName)
7107- {
7108- update = tabType == MyCommon.TabUsageType.Home || tabType == MyCommon.TabUsageType.UserDefined;
7109- }
7110- else
7111- {
7112- update = endpointName switch
7113- {
7114- "/statuses/mentions_timeline" => tabType == MyCommon.TabUsageType.Mentions,
7115- "/favorites/list" => tabType == MyCommon.TabUsageType.Favorites,
7116- "/direct_messages/events/list" => tabType == MyCommon.TabUsageType.DirectMessage,
7117- "/statuses/user_timeline" => tabType == MyCommon.TabUsageType.UserTimeline,
7118- "/lists/statuses" => tabType == MyCommon.TabUsageType.Lists,
7119- "/search/tweets" => tabType == MyCommon.TabUsageType.PublicSearch,
7120- "/statuses/show/:id" => tabType == MyCommon.TabUsageType.Related,
7121- _ => false,
7122- };
7123- }
7124-
7125- if (update)
7126- {
7127- this.toolStripApiGauge.ApiEndpoint = endpointName;
7128- }
7110+ var currentEndpointName = this.toolStripApiGauge.ApiEndpoint;
7111+ this.toolStripApiGauge.ApiEndpoint = currentEndpointName;
71297112 }
71307113 }
71317114
--- a/OpenTween/TweetFormatter.cs
+++ b/OpenTween/TweetFormatter.cs
@@ -132,7 +132,7 @@ namespace OpenTween
132132
133133 // 過去に存在した壊れたエンティティの対策
134134 // 参照: https://dev.twitter.com/discussions/12628
135- if (entity.DisplayUrl == null)
135+ if (entity.DisplayUrl == null || entity.ExpandedUrl == null)
136136 {
137137 expandedUrl = MyCommon.ConvertToReadableUrl(targetText);
138138 return $"""<a href="{E(entity.Url)}" title="{E(expandedUrl)}">{T(E(targetText))}</a>""";
--- a/OpenTween/Twitter.cs
+++ b/OpenTween/Twitter.cs
@@ -682,7 +682,7 @@ namespace OpenTween
682682 var request = new UserTweetsAndRepliesRequest(userId)
683683 {
684684 Count = count,
685- Cursor = more ? tab.CursorBottom : null,
685+ Cursor = more ? tab.CursorBottom : tab.CursorTop,
686686 };
687687 var response = await request.Send(this.Api.Connection)
688688 .ConfigureAwait(false);
@@ -694,6 +694,9 @@ namespace OpenTween
694694 .ToArray();
695695
696696 tab.CursorBottom = response.CursorBottom;
697+
698+ if (!more)
699+ tab.CursorTop = response.CursorTop;
697700 }
698701 else
699702 {
@@ -881,7 +884,7 @@ namespace OpenTween
881884 var request = new ListLatestTweetsTimelineRequest(tab.ListInfo.Id.ToString())
882885 {
883886 Count = count,
884- Cursor = more ? tab.CursorBottom : null,
887+ Cursor = more ? tab.CursorBottom : tab.CursorTop,
885888 };
886889 var response = await request.Send(this.Api.Connection)
887890 .ConfigureAwait(false);
@@ -895,6 +898,9 @@ namespace OpenTween
895898
896899 statuses = convertedStatuses.ToArray();
897900 tab.CursorBottom = response.CursorBottom;
901+
902+ if (!more)
903+ tab.CursorTop = response.CursorTop;
898904 }
899905 else if (more)
900906 {
@@ -1088,7 +1094,7 @@ namespace OpenTween
10881094 var request = new SearchTimelineRequest(tab.SearchWords)
10891095 {
10901096 Count = count,
1091- Cursor = more ? tab.CursorBottom : null,
1097+ Cursor = more ? tab.CursorBottom : tab.CursorTop,
10921098 };
10931099 var response = await request.Send(this.Api.Connection)
10941100 .ConfigureAwait(false);
@@ -1099,6 +1105,9 @@ namespace OpenTween
10991105 .ToArray();
11001106
11011107 tab.CursorBottom = response.CursorBottom;
1108+
1109+ if (!more)
1110+ tab.CursorTop = response.CursorTop;
11021111 }
11031112 else
11041113 {
--- a/OpenTween/UserInfoDialog.cs
+++ b/OpenTween/UserInfoDialog.cs
@@ -169,7 +169,12 @@ namespace OpenTween
169169 var urlEntities = entities?.Urls ?? Array.Empty<TwitterEntityUrl>();
170170
171171 foreach (var entity in urlEntities)
172+ {
173+ if (entity.ExpandedUrl == null)
174+ continue;
175+
172176 entity.ExpandedUrl = await ShortUrl.Instance.ExpandUrlAsync(entity.ExpandedUrl);
177+ }
173178
174179 // user.entities には urls 以外のエンティティが含まれていないため、テキストをもとに生成する
175180 var mergedEntities = urlEntities.AsEnumerable<TwitterEntity>()
@@ -253,7 +258,7 @@ namespace OpenTween
253258 var urlEntities = entities.Urls ?? Array.Empty<TwitterEntityUrl>();
254259
255260 foreach (var entity in urlEntities)
256- entity.ExpandedUrl = await ShortUrl.Instance.ExpandUrlAsync(entity.ExpandedUrl);
261+ entity.ExpandedUrl = await ShortUrl.Instance.ExpandUrlAsync(entity.ExpandedUrl ?? entity.Url);
257262
258263 var mergedEntities = entities.Concat(TweetExtractor.ExtractEmojiEntities(status.FullText));
259264
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
1-version: 3.7.1.{build}
1+version: 3.8.0.{build}
22
33 os: Visual Studio 2022
44
Show on old repository browser