OSDN Git Service

graphqlエンドポイントに対するレートリミットの表示に対応
authorKimura Youichi <kim.upsilon@bucyou.net>
Fri, 1 Dec 2023 15:59:35 +0000 (00:59 +0900)
committerKimura Youichi <kim.upsilon@bucyou.net>
Fri, 1 Dec 2023 16:00:54 +0000 (01:00 +0900)
14 files changed:
CHANGELOG.txt
OpenTween.Tests/Api/GraphQL/ListLatestTweetsTimelineRequestTest.cs
OpenTween.Tests/Api/GraphQL/SearchTimelineRequestTest.cs
OpenTween.Tests/Api/GraphQL/TweetDetailRequestTest.cs
OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs
OpenTween.Tests/Api/GraphQL/UserTweetsAndRepliesRequestTest.cs
OpenTween/Api/GraphQL/ListLatestTweetsTimelineRequest.cs
OpenTween/Api/GraphQL/SearchTimelineRequest.cs
OpenTween/Api/GraphQL/TweetDetailRequest.cs
OpenTween/Api/GraphQL/UserByScreenNameRequest.cs
OpenTween/Api/GraphQL/UserTweetsAndRepliesRequest.cs
OpenTween/Connection/IApiConnection.cs
OpenTween/Connection/TwitterApiConnection.cs
OpenTween/Tween.cs

index 7c7386f..278e8e4 100644 (file)
@@ -1,6 +1,7 @@
 更新履歴
 
 ==== Unreleased
+ * NEW: graphqlエンドポイントに対するレートリミットの表示に対応
  * CHG: タイムライン更新時に全件ではなく新着投稿のみ差分を取得する動作に変更
  * FIX: 設定したタイムアウト時間を超えてAPI接続が持続する場合がある不具合を修正
 
index 58bf7f7..dc24d7c 100644 (file)
@@ -41,14 +41,15 @@ namespace OpenTween.Api.GraphQL
 
             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);
 
@@ -72,14 +73,15 @@ namespace OpenTween.Api.GraphQL
 
             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);
 
index c0f5dad..97459b1 100644 (file)
@@ -40,14 +40,15 @@ namespace OpenTween.Api.GraphQL
 
             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);
 
@@ -71,14 +72,15 @@ namespace OpenTween.Api.GraphQL
 
             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);
 
index 0045f4e..69b7c16 100644 (file)
@@ -41,12 +41,13 @@ namespace OpenTween.Api.GraphQL
 
             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);
 
index 81f03d9..77786fe 100644 (file)
@@ -40,12 +40,13 @@ namespace OpenTween.Api.GraphQL
 
             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);
 
@@ -67,7 +68,7 @@ namespace OpenTween.Api.GraphQL
 
             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);
 
index b7a9f7c..ea1c51e 100644 (file)
@@ -40,14 +40,15 @@ namespace OpenTween.Api.GraphQL
 
             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);
 
@@ -71,14 +72,15 @@ namespace OpenTween.Api.GraphQL
 
             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);
 
index 6545086..a241e4a 100644 (file)
@@ -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; }
@@ -89,7 +91,7 @@ namespace OpenTween.Api.GraphQL
             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);
             }
index 22cb7f8..9e76ca0 100644 (file)
@@ -37,6 +37,8 @@ namespace OpenTween.Api.GraphQL
 {
     public class SearchTimelineRequest
     {
+        public static readonly string EndpointName = "SearchTimeline";
+
         private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/lZ0GCEojmtQfiUQa5oJSEw/SearchTimeline");
 
         public string RawQuery { get; set; }
@@ -91,7 +93,7 @@ namespace OpenTween.Api.GraphQL
             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);
             }
index 86edb40..009e104 100644 (file)
@@ -38,6 +38,8 @@ namespace OpenTween.Api.GraphQL
 {
     public class TweetDetailRequest
     {
+        public static readonly string EndpointName = "TweetDetail";
+
         private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/-Ls3CrSQNo2fRKH6i6Na1A/TweetDetail");
 
         required public TwitterStatusId FocalTweetId { get; set; }
@@ -65,7 +67,7 @@ namespace OpenTween.Api.GraphQL
             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);
             }
index c72764b..f261924 100644 (file)
@@ -37,6 +37,8 @@ namespace OpenTween.Api.GraphQL
 {
     public class UserByScreenNameRequest
     {
+        public static readonly string EndpointName = "UserByScreenName";
+
         private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/xc8f1g7BYqr6VTzTbvNlGw/UserByScreenName");
 
         required public string ScreenName { get; set; }
@@ -64,7 +66,7 @@ namespace OpenTween.Api.GraphQL
             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);
             }
index 940d4d2..916a914 100644 (file)
@@ -37,6 +37,8 @@ namespace OpenTween.Api.GraphQL
 {
     public class UserTweetsAndRepliesRequest
     {
+        public static readonly string EndpointName = "UserTweetsAndReplies";
+
         private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/YlkSUg0mRBx7-EkxCvc-bw/UserTweetsAndReplies");
 
         public string UserId { get; set; }
@@ -74,7 +76,7 @@ namespace OpenTween.Api.GraphQL
             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);
             }
index e541263..b7c866f 100644 (file)
@@ -36,6 +36,8 @@ namespace OpenTween.Connection
 
         Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param);
 
+        Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param, string? endpointName);
+
         Task<Stream> GetStreamingStreamAsync(Uri uri, IDictionary<string, string>? param);
 
         Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string>? param);
index d76e088..16e2c34 100644 (file)
@@ -167,8 +167,15 @@ namespace OpenTween.Connection
             }
         }
 
-        public async Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param)
+        public Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param)
+            => this.GetStreamAsync(uri, param, null);
+
+        public async Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string>? param, string? endpointName)
         {
+            // レートリミット規制中はAPIリクエストを送信せずに直ちにエラーを発生させる
+            if (endpointName != null)
+                this.ThrowIfRateLimitExceeded(endpointName);
+
             var requestUri = new Uri(RestApiBase, uri);
 
             if (param != null)
@@ -176,7 +183,16 @@ namespace OpenTween.Connection
 
             try
             {
-                return await this.Http.GetStreamAsync(requestUri)
+                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)
index f879d58..969462a 100644 (file)
@@ -52,6 +52,7 @@ using System.Threading.Tasks;
 using System.Windows.Forms;
 using OpenTween.Api;
 using OpenTween.Api.DataModel;
+using OpenTween.Api.GraphQL;
 using OpenTween.Api.TwitterV2;
 using OpenTween.Connection;
 using OpenTween.MediaUploadServices;
@@ -7083,17 +7084,22 @@ namespace OpenTween
 
             if (endpointName == null)
             {
+                var authByCookie = this.tw.Api.AppToken.AuthType == APIAuthType.TwitterComCookie;
+
                 // 表示中のタブに応じて更新
                 endpointName = tabType switch
                 {
-                    MyCommon.TabUsageType.Home => GetTimelineRequest.EndpointName,
+                    MyCommon.TabUsageType.Home => "/statuses/home_timeline",
                     MyCommon.TabUsageType.UserDefined => "/statuses/home_timeline",
                     MyCommon.TabUsageType.Mentions => "/statuses/mentions_timeline",
                     MyCommon.TabUsageType.Favorites => "/favorites/list",
                     MyCommon.TabUsageType.DirectMessage => "/direct_messages/events/list",
-                    MyCommon.TabUsageType.UserTimeline => "/statuses/user_timeline",
-                    MyCommon.TabUsageType.Lists => "/lists/statuses",
-                    MyCommon.TabUsageType.PublicSearch => "/search/tweets",
+                    MyCommon.TabUsageType.UserTimeline =>
+                        authByCookie ? UserTweetsAndRepliesRequest.EndpointName : "/statuses/user_timeline",
+                    MyCommon.TabUsageType.Lists =>
+                        authByCookie ? ListLatestTweetsTimelineRequest.EndpointName : "/lists/statuses",
+                    MyCommon.TabUsageType.PublicSearch =>
+                        authByCookie ? SearchTimelineRequest.EndpointName : "/search/tweets",
                     MyCommon.TabUsageType.Related => "/statuses/show/:id",
                     _ => null,
                 };
@@ -7101,31 +7107,8 @@ namespace OpenTween
             }
             else
             {
-                // 表示中のタブに関連する endpoint であれば更新
-                bool update;
-                if (endpointName == GetTimelineRequest.EndpointName)
-                {
-                    update = tabType == MyCommon.TabUsageType.Home || tabType == MyCommon.TabUsageType.UserDefined;
-                }
-                else
-                {
-                    update = endpointName switch
-                    {
-                        "/statuses/mentions_timeline" => tabType == MyCommon.TabUsageType.Mentions,
-                        "/favorites/list" => tabType == MyCommon.TabUsageType.Favorites,
-                        "/direct_messages/events/list" => tabType == MyCommon.TabUsageType.DirectMessage,
-                        "/statuses/user_timeline" => tabType == MyCommon.TabUsageType.UserTimeline,
-                        "/lists/statuses" => tabType == MyCommon.TabUsageType.Lists,
-                        "/search/tweets" => tabType == MyCommon.TabUsageType.PublicSearch,
-                        "/statuses/show/:id" => tabType == MyCommon.TabUsageType.Related,
-                        _ => false,
-                    };
-                }
-
-                if (update)
-                {
-                    this.toolStripApiGauge.ApiEndpoint = endpointName;
-                }
+                var currentEndpointName = this.toolStripApiGauge.ApiEndpoint;
+                this.toolStripApiGauge.ApiEndpoint = currentEndpointName;
             }
         }