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