Skip to content

Commit

Permalink
Merge pull request #262 from opentween/graphql-ratelimit
Browse files Browse the repository at this point in the history
graphqlエンドポイントに対するレートリミットの表示に対応
  • Loading branch information
upsilon authored Dec 1, 2023
2 parents 8cb4993 + a207970 commit ce72f69
Show file tree
Hide file tree
Showing 14 changed files with 73 additions and 53 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
更新履歴

==== Unreleased
* NEW: graphqlエンドポイントに対するレートリミットの表示に対応
* CHG: タイムライン更新時に全件ではなく新着投稿のみ差分を取得する動作に変更
* FIX: 設定したタイムアウト時間を超えてAPI接続が持続する場合がある不具合を修正

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 @@ -72,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
10 changes: 6 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 @@ -71,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
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
10 changes: 6 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 @@ -71,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
4 changes: 3 additions & 1 deletion 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 Down
4 changes: 3 additions & 1 deletion OpenTween/Api/GraphQL/SearchTimelineRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -91,7 +93,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 Down
4 changes: 3 additions & 1 deletion OpenTween/Api/GraphQL/TweetDetailRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -65,7 +67,7 @@ public async Task<TimelineTweet[]> 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 Down
4 changes: 3 additions & 1 deletion OpenTween/Api/GraphQL/UserByScreenNameRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -64,7 +66,7 @@ public async Task<TwitterGraphqlUser> 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 Down
4 changes: 3 additions & 1 deletion OpenTween/Api/GraphQL/UserTweetsAndRepliesRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -74,7 +76,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 Down
2 changes: 2 additions & 0 deletions OpenTween/Connection/IApiConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public interface IApiConnection : IDisposable

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);
Expand Down
20 changes: 18 additions & 2 deletions OpenTween/Connection/TwitterApiConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,16 +167,32 @@ private void ThrowIfRateLimitExceeded(string endpointName)
}
}

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)
requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));

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)
Expand Down
41 changes: 12 additions & 29 deletions OpenTween/Tween.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
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;
Expand Down Expand Up @@ -7083,49 +7084,31 @@ private void SetApiStatusLabel(string? endpointName = null)

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,
};
this.toolStripApiGauge.ApiEndpoint = endpointName;
}
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;
}
}

Expand Down

0 comments on commit ce72f69

Please sign in to comment.